老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

python常见问题

发表于 2018-01-16 | 分类于 python
OSError: [Errno 1] Operation not permitted: '/tmp/pip-g3bg0s-uninstall/System/Library/Frameworks/Pyt

在用下列名字安装时

1
2
sudo -H pip install Scrapy
sudo pip install virtualenvwrapper

出现下列错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Found existing installation: six 1.4.1

DEPRECATION: Uninstalling a distutils installed project (six) has been deprecated and will be removed in a future version. This is due to the fact that uninstalling a distutils project will only partially uninstall the project.

Uninstalling six-1.4.1:

Exception:

Traceback (most recent call last):

File "/Library/Python/2.7/site-packages/pip-9.0.1-py2.7.egg/pip/basecommand.py", line 215, in main

status = self.run(options, args)

File "/Library/python/2.7/site-packages/pip-9.0.1-py2.7.egg/pip/commands/install.py", line 342, in run

prefix=options.prefix_path,

File "/Library/Python/2.7/site-packages/pip-9.0.1-py2.7.egg/pip/req/req_set.py", line 778, in install

requirement.uninstall(auto_confirm=True)

File "/Library/Python/2.7/site-packages/pip-9.0.1-py2.7.egg/pip/req/req_install.py", line 754, in uninstall

paths_to_remove.remove(auto_confirm)

File "/Library/Python/2.7/site-packages/pip-9.0.1-py2.7.egg/pip/req/req_uninstall.py", line 115, in remove

renames(path, new_path)

File "/Library/Python/2.7/site-packages/pip-9.0.1-py2.7.egg/pip/utils/__init__.py", line 267, in renames

shutil.move(old, new)

File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 302, in move

copy2(src, real_dst)

File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 131, in copy2

copystat(src, dst)

File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 103, in copystat

os.chflags(dst, st.st_flags)

OSError: [Errno 1] Operation not permitted: '/tmp/pip-g3bg0s-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/six-1.4.1-py2.7.egg-info'

Scrapy,virtualenvwrapper需要依赖six,在安装six的时候发现系统已经有一个six-1.4.1,但是virtualenvwrapper需要six-1.9.0,于是想先卸载老版本的six,此时问题来了,发现没有权限卸载,此时我就纳闷,加上sudo,还是没权限。于是Google之,最终还是在万能的GitHub找到答案。six-1.4.1是系统内置的packages,因 系统集成保护 你是没有权限去修改/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/six-1.4.1-py2.7.egg-info目录的。因此在安装virtualenvwrapper的时候需要选择忽略six的安装:

1
2
sudo pip install virtualenvwrapper --upgrade --ignore-installed six
sudo pip install Scrapy --upgrade --ignore-installed six

如果使用requirements安装,也使用:sudo -H pip install -r requirements.txt --upgrade --ignore-installed six

android逆向(1)之root方式注入apk

发表于 2018-01-16 | 分类于 Android

第一步,注入动态库到目标进程

  1. 启动注入程序,入口main函数中输入将被注入的进程名称以及欲注入动态库路径;
  2. 根据进程名获取进程id:打开/proc目录,读取/proc/%d/cmdline;
  3. attach到目标进程:ptrace(PTRACE_ATTACH, pid, NULL, 0)并暂停目前进程;
  4. 读取寄存器值:ptrace(PTRACE_GETREGS, pid, NULL, regs),并保持读取到的寄存器;
  5. 获取被加载的动态库中mmap函数地址,获取被加载动态库中函数地址的方法是,获取本地libc.so模块和被加载动态库中libc.so模块中各自起始地址,local_addr-local_handle的值为指定函数(如mmap)在该模块中的偏移量,然后再加上remote_handle,结果就为指定函数在目的进程的虚拟地址,获取模块起始地址的方法是读取进程对应的/proc/self/maps或/proc/%d/maps文件;
  6. 调用被加载库的mmap方法申请空间:ptrace_call(target_pid, (uint32_t)func_addr, parameters, param_num, regs);
  7. 获取申请空间时的R0寄存器,arm中R0~R3作为传递参数的寄存器,R0可以获取到申请空间的起始地址;
  8. 获取被加载库中dlopen,dlsym,dlclose,dlerror函数地址;
  9. 将被加载动态库的路径写入到mmap分配的栈空间:ptrace(PTRACE_PEEKTEXT, pid, dest, 0);;
  10. 调用远程函数的dlopen函数打开要被注入的动态库;
  11. 调用dlsym获取加载动态库目标函数符号对应地址,并调用目标函数,完成注入;
  12. 调用dlclose关闭被注入库;
  13. 完成注入;

动态库中加载目标apk(aar)

  1. 入口函数中启动新线程,并调用pthread_detach(tid)将新线程设置为detach状态,可自动回收资源;
  2. 新线程中调用AndroidRuntime::getJavaVM();获取JavaVM对象并Attach当前线程到JavaVM:jvm->AttachCurrentThread(&jni_env, NULL);获取到jni env;
  3. 找到dalvik/system/DexClassLoader类,并获取其构造方法public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)的methodid及public Class<?> loadClass(String name) throws ClassNotFoundException的methodid;
  4. 找到类java/lang/ClassLoader,获取其静态方法Gpublic static ClassLoader getSystemClassLoader()的MethodID,调用静态方法getSystemClassLoader获取系统ClassLoader;
  5. 基于系统ClassLoader,以及DexClassClassLoader构造方法MethodID创建DexClassLoader对象:jni_env->NewObject(dexloader_claxx, dexloader_init_method, apk_path, dex_out_path, lib_path, class_loader);,要传入DexClassloader jclass,DexClassLoader构造方法id,要被加载的apk路径,dex输出路径,动态库路径,以及父(即系统)ClassLoader;
  6. 调用DexClassLoader的loadClass方法id加载apk中被调起的类:jclass entry_class = static_cast<jclass>(jni_env->CallObjectMethod(dex_loader_obj, loadClass_method, class_name));;
  7. 调用动态加载到的apk中类的方法:jclass entry_class = static_cast<jclass>(jni_env->CallObjectMethod(dex_loader_obj, loadClass_method, class_name));;
  8. 线程与JavaVM脱离:jvm->DetachCurrentThread();;

java中获取context

docker简介及环境搭建

发表于 2018-01-15 | 分类于 docker

docker是什么

Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。 Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。

传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。

Docker 三个基本概念

  • 镜像(Image):Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
  • 容器(Container):镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
  • 仓库(Repository):镜像构建完成后,可以很容易的在当前宿主上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest作为默认标签。用 Docker 的时候,需要经常从官方获取镜像,但是由于显而易见的网络原因,拉取镜像的过程非常耗时,严重影响使用 Docker 的体验。因此 DaoCloud等加速器服务商 推出了加速器工具解决这个难题,通过智能路由和缓存机制,极大提升了国内网络访问 Docker Hub 的速度,目前已经拥有了广泛的用户群体,并得到了 Docker 官方的大力推荐。如果您是在国内的网络环境使用 Docker,那么 Docker 加速器一定能帮助到您。docker常用仓库:https://hub.docker.com/explore/

docker daocloud加速器:

  • linux:curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://311e425f.m.daocloud.io,该脚本可以将 –registry-mirror 加入到你的 Docker 配置文件 /etc/docker/daemon.json 中。适用于 Ubuntu14.04、Debian、CentOS6 、CentOS7、Fedora、Arch Linux、openSUSE Leap 42.1,其他版本可能有细微不同
  • Mac:右键点击桌面顶栏的 docker 图标,选择 Preferences ,在 Daemon 标签(Docker 17.03 之前版本为 Advanced 标签)下的 Registry mirrors 列表中加入下面的镜像地址:http://311e425f.m.daocloud.io,点击 Apply & Restart 按钮使设置生效。
  • Windows: 在桌面右下角状态栏中右键 docker 图标,修改在 Docker Daemon 标签页中的 json ,把下面的地址:http://311e425f.m.daocloud.io,加到”registry-mirrors”的数组里。点击 Apply 。

环境搭建

mac下安装docker

官方mac的安装步骤 下载docker.dmg文件,然后点击,一步步操作

检测Docker Engine, Docker Compose, 和Docker Machine的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
docker version
Client:
Version: 17.03.1-ce
API version: 1.27
Go version: go1.7.5
Git commit: c6d412e
Built: Tue Mar 28 00:40:02 2017
OS/Arch: darwin/amd64

Server:
Version: 17.03.1-ce
API version: 1.27 (minimum version 1.12)
Go version: go1.7.5
Git commit: c6d412e
Built: Fri Mar 24 00:00:50 2017
OS/Arch: linux/amd64
Experimental: true

docker-compose --version
docker-compose version 1.11.2, build dfed245

docker-machine --version
docker-machine version 0.10.0, build 76ed2a6

运行官网提供的二个简单列子

拉取hello-world镜像

1
docker pull hello-world

查看hello-world镜像信息:

1
2
3
docker images hello-world
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest 1815c82652c0 5 days ago 1.84 kB

根据镜像生成对应容器

1
docker run hello-world

ps不加参数只会把当前运行的Community打印出来,查看当前所有的Community,加上-a参数。

1
docker ps -a

当然也可以根据镜像生成一个具体名称的镜像,先删除当前容器

1
docker rm 60

当然如果当前容器正在运行,要删除这个容器,则使用命令

1
docker rm -f 60

使用镜像生成具体名称的容器:

1
docker run --name miaozhihao hello-world

于更多的docker run的命令可以使用来查看

1
docker run --help

第二个examples,启动docker的web服务

1
2
docker pull nginx
docker run -d -p 80:80 --name webserver nginx

-p参数是使用宿主机的80映射容器的80端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

交互式终端方式进入 webserver容器,

1
docker exec -it webserver bash

修改nginx的显示页面:

1
2
3
root@41b6804c716e:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@41b6804c716e:/# exit
exit

修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff

1
2
3
4
5
6
7
8
9
10
11
12
docker diff webserver
C /root
A /root/.bash_history
C /run
A /run/nginx.pid
C /usr/share/nginx/html/index.html
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp

使用docker commit生成镜像

1
2
docker commit --author "zhihao.miao <1026145686@qq.com>" --message "修改了默认网页" webserver nginx:v2
sha256:0a34c054b8a826d85dddf4d1dbdd3028ab890feff4c8a0844e9b98dd146c2e07

–autho 指定作者 –message表示容器的一些信息 查看当前nginx镜像:

1
2
3
4
docker images nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 0a34c054b8a8 11 seconds ago 109 MB
nginx latest 958a7ae9e569 4 weeks ago 109 MB

查看当前所有的容器,包括运行的和停止的

1
2
3
4
5
6
7
docker ps -a
\CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
41b6804c716e nginx "nginx -g 'daemon ..." 3 minutes ago Up 3 minutes 0.0.0.0:80->80/tcp webserver
85612b405cda miaozhihao001dockerhub/commit_test1 "nginx -g 'daemon ..." 20 hours ago Exited (0) 3 hours ago nginx_web
9ef1fb35d7aa ubuntu:14.04 "/bin/bash" 20 hours ago Exited (0) 20 hours ago commit_test
bf8320b9e445 ubuntu:14.04 "/bin/bash" 24 hours ago Exited (0) 24 hours ago sharp_curie
9f9767eb8aaf hello-world "/hello" 10 days ago Exited (0) 10 days ago miaozhihao

启动新的容器

1
2
3
4
docker run --name newwebserver -d -p 80:80 nginx:v2
3619b34ed347cf1ae2ee3ab32c419140871f3084b9a1325ab5d8c6155d43bf06
➜ curl localhost:80
<h1>Hello, Docker!</h1>

创建自己的docker镜像

编辑Dockerfile文件,填入以下内容:

1
2
3
FROM docker/whalesay:latest
RUN apt-get -y update && apt-get install -y fortunes
CMD /usr/games/fortune -a | cowsay

运行以下命令创建名为docker-whale的镜像:

1
docker build -t docker-whale .
  • -t: 给tag命令
  • .: Dockerfile文件所在路径

docker tag push pull

1
docker tag 8e15421920b1 xulingfeng/docker-whale:latest

上传操作

1
docker push xulingfeng/docker-whale

下载操作

1
docker pull centos

交互式的操作

1
docker run -t -i ubuntu /bin/bash
  • t 分配了一个终端在新的容器中
  • -i 允许你和容器进行交互操作
  • /bin/bash 启动容器中的Bash shell

docker的守护状态,也就是后台运行

1
docker run -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"
  • docker run 运行容器
  • -d 让容器在后台运行
  • ubuntu 你希望运行容器的镜像

查看docker容器运行日志

1
docker logs -f 容器名
  • -f 类似与 tail -f

使用docker运行web应用

1
docker run -d -P training/webapp python app.py
  • -d:代表后台运行该容器
  • -P:映射容器中的web应用端口号到你的主机上32768-61000中的某一个端口。这样你可以访问该容器中的web应用
  • training/webapp: 一个已经构建好的镜像,包含一个简单的python flask框架web应用
  • python app.py:这个命令用来启动容器中的web

成功运行以上命令后,运行: docker ps 查看到容器的5000端口号映射到了本地的32768,浏览器访问http://127.0.0.1:32768 看到helloworld 成功提示

自定义主机端口号

1
docker run -d -p 80:5000 training/webapp python app.py
  • -p 80:5000 将本机的80端口绑定容器内的5000端口,本地直接访问 http://127.0.0.1 即可

查看容器的进程

1
docker top 容器名

检查容器的状态信息

1
docker inspect 容器名

镜像搜索

1
docker search 内容

创建一个给pycharm开发用的镜像,包含python3,Django, Flask, requests, PyMySQL, ldap3, jira,celery, simplejson

centos镜像,分解步骤如下

  • 首先添加额外源:yum install -y epel-release
  • 安装编译环境:yum install -y gcc automake autoconf libtool make gcc-c++
  • 安装wget命令:yum install -y wget
  • 安装openssl-devel python的pip命令依赖:yum install -y openssl-devel
  • 下载python3.5.2最新包:wget https://www.python.org/ftp/python/3.5.2/Python-3.5.2.tgz -P /software/
  • 解压python3.5.2压缩包并编译安装:tar -zxvf /software/Python-3.5.2.tgz -C /software/与./configure && make -j2&& make install -j2
  • 更新pip:pip install --upgrade pip与pip install --upgrade setuptools
  • 安装所需的第三方包:pip install Django Flask requests PyMySQL ldap3 jira celery simplejson

通过Dockerfile构建镜像

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
FROM centos:latest
RUN yum install -y epel-release
RUN yum install -y gcc automake autoconf libtool make gcc-c++
RUN yum install -y wget
RUN yum install -y openssl-devel
RUN wget https://www.python.org/ftp/python/3.5.2/Python-3.5.2.tgz -P /software/
RUN tar -zxvf /software/Python-3.5.2.tgz -C /software/
RUN cd /software/Python-3.5.2/ && ./configure python3 && make -j2&& make install -j2
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
RUN pip install Django Flask requests PyMySQL ldap3 jira celery simplejson

在Dockerfile目录中执行:docker build -t 名字:版本 .

兼容docker for mac 和 pycharm

1
2
brew install socat
socat TCP-LISTEN:2376,reuseaddr,fork,bind=127.0.0.1 UNIX-CLIENT:/var/run/docker.sock

一些docker命令总结

  • docker images :查看当前宿主机的所有镜像。
  • docker images ubuntu:根据仓库名列出镜像
  • docker images ubuntu:14.04:指定仓库名和标签
  • docker build -t webservice .:表示使用当前目录下的DockerFile来生成镜像,-t参数的值表示镜像的tagname,如果DockerFile在当前路径下则使用.,如果不在当前路径下则使用相对路径。
  • docker ps -a: 没有-a参数表示显示当前宿主机的正在运行的容器,加上-a表示显示当前宿主机所有的容器,包括已经退出的容器。
  • docker run -d -p 2222:22 –name base centos:7.1 表示根据指定的镜像后台运行容器,容器的名字是base(–name就是指定容器的名字),centos:7.1表示镜像的名字,-p参数表示当前宿主机的2222端口对应容器的22端口。-d参数表示(Run container in background and print container ID)
  • docker exec -it base /bin/bash 以交互式命令进入base容器并且执行/bin/bash命令
  • docker rmi webservice:删除webservice镜像
  • docker rm base: 删除base容器,如果base正在运行,则可以使用docker rm -f base进行强行删除
  • docker start 启动容器
  • docker stop 停止容器

Android插件化(六)基础之Binder

发表于 2018-01-14 | 分类于 Android

Android插件化(五)基础之用到Android源码类探讨

发表于 2018-01-12 | 分类于 Android
PackageManager

获取:通过Context的getPackageManager() 常用方法:

  1. getInstalledPackages(int flags):可以得到所有安装在机器上的程序的包信息类对象List,PackageInfo类中有一值applicationInfo可以得到Application的对象。
  2. getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES):取自身应用以外其他apk的信息方法
  3. getInstalledApplications(int flags):得到所有安装在机器上的程序的application对象List;
  4. getApplicationIcon(String packageName),getApplicationIcon(ApplicationInfo info):获得应用程序的图片
  5. getApplicationLabel(ApplicationInfo info):方法可以获得应用程序的名字

Android插件化(四)基础之文件存储

发表于 2018-01-12 | 分类于 Android

android文件存储解析

安卓中提供了Context中的方法与Environment类来操作文件。

Context文件操作方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public File getFileStreamPath(String name)
public String[] fileList()
public File getFilesDir()
public File getNoBackupFilesDir()
public File getExternalFilesDir(String type)
public File[] getExternalFilesDirs(String type)
public File getObbDir()
public File[] getObbDirs()
public File getCacheDir()
public File getCodeCacheDir()
public File getExternalCacheDir()
public File[] getExternalCacheDirs()
public File[] getExternalMediaDirs()
public File getDir(String name, int mode)

用Log把它们都显示出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Log.d("context", "context.getFileStreamPath-->" +
this.getFileStreamPath("test").toString());
Log.d("context", "context.getDir-->" +
this.getDir("test", Context.MODE_PRIVATE).toString());
Log.d("context", "context.getFilesDir-->" +
this.getFilesDir().toString());
Log.d("context", "context.getNoBackupFilesDir" +
this.getNoBackupFilesDir().toString());
Log.d("context", "context.getCacheDir-->" +
this.getCacheDir().toString());
Log.d("context", "context.getCodeCacheDir" +
this.getCodeCacheDir().toString());
Log.d("context", "context.getDatabasePath-->" +
this.getDatabasePath("test").toString());
Log.d("context", "context.getObbDir-->" +
this.getObbDir().toString());

File[] files1 = this.getObbDirs();
for (File file : files1) {
Log.d("context", "context.getObbDirs-->" + file.toString());
}
File[] files2 = this.getExternalMediaDirs();
for (File file : files2) {
Log.d("context", "context.getExternalMediaDirs" + file.toString());
}

Log.d("context", "context.getExternalCacheDir-->" + this.getExternalCacheDir().toString());
File[] files3 = this.getExternalCacheDirs();
for (File file : files3) {
Log.d("context", "context.getExternalCacheDirs-->" + file.toString());
}

Log.d("context", "context.getExternalFilesDir-->" + this.getExternalFilesDir(Environment.DIRECTORY_ALARMS).toString());

File[] files4 = this.getExternalFilesDirs(Environment.DIRECTORY_ALARMS);
for (File file : files4) {
Log.d("context", "context.getExternalFilesDirs-->" + file.toString());
}

og输出结果(不同版本的安卓系统,目录可能也不相同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
context.getFileStreamPath-->/data/data/cn.hufeifei.environmenttest/files/test
context.getDir-->/data/data/cn.hufeifei.environmenttest/app_test
context.getFilesDir-->/data/data/cn.hufeifei.environmenttest/files
context.getNoBackupFilesDir/data/data/cn.hufeifei.environmenttest/no_backup
context.getCacheDir-->/data/data/cn.hufeifei.environmenttest/cache
context.getCodeCacheDir/data/data/cn.hufeifei.environmenttest/code_cache
context.getDatabasePath-->/data/data/cn.hufeifei.environmenttest/databases/test
context.getObbDir-->/storage/emulated/0/Android/obb/cn.hufeifei.environmenttest
context.getObbDirs-->/storage/emulated/0/Android/obb/cn.hufeifei.environmenttest
context.getExternalMediaDirs/storage/emulated/0/Android/media/cn.hufeifei.environmenttest
context.getExternalCacheDir-->/storage/emulated/0/Android/data/cn.hufeifei.environmenttest/cache
context.getExternalCacheDirs-->/storage/emulated/0/Android/data/cn.hufeifei.environmenttest/cache
context.getExternalFilesDir-->/storage/emulated/0/Android/data/cn.hufeifei.environmenttest/files/Alarms
context.getExternalFilesDirs-->/storage/emulated/0/Android/data/cn.hufeifei.environmenttest/files/Alarms

Environment工具类中提供了以下几个方法:

1
2
3
4
5
6
7
8
Environment.getDataDirectory();
Environment.getRootDirectory();
Environment.getDownloadCacheDirectory();
Environment.getExternalStoragePublicDirectory(String type);
Environment.getExternalStorageDirectory();
Environment.getExternalStorageState();
Environment.getExternalStorageState(File path)
Environment.getStorageState();//已被getExternalStorageState取代
1.前三个方法

用Log输出来:

1
2
3
4
//IS标识内部存储
Log.d("Environment-IS", Environment.getDataDirectory().toString());
Log.d("Environment-IS", Environment.getDownloadCacheDirectory().toString());
Log.d("Environment-IS", Environment.getRootDirectory().toString());

输出结果为:

1
2
3
D/Environment-IS: /data
D/Environment-IS: /cache
D/Environment-IS: /system
2.getExternalStoragePublicDirectory方法

getExternalStoragePublicDirectory方法用来获取安卓外部存储中系统应用经常用到的公共文件夹, 在Environment中定义了这些文件夹的名字:

1
2
3
4
5
6
7
8
9
10
Environment.DIRECTORY_MUSIC = "Music"
Environment.DIRECTORY_PODCASTS = "Podcasts"
Environment.DIRECTORY_RINGTONES = "Ringtones"
Environment.DIRECTORY_ALARMS = "Alarms"
Environment.DIRECTORY_NOTIFICATIONS = "Notifications"
Environment.DIRECTORY_PICTURES = "Pictures"
Environment.DIRECTORY_MOVIES = "Movies"
Environment.DIRECTORY_DOWNLOADS = "Download"
Environment.DIRECTORY_DCIM = "DCIM"
Environment.DIRECTORY_DOCUMENTS = "Documents"

它们的目录一般在/storage/emulated/0/ (dir_name就是Environment中定义的这些字符串常量)

3.最后的三个方法

最后面三个方法是用来获取挂载点的状态(在Linux中把一些特殊目录称为所谓的挂载点,有点类似于Windows中的分区):

1
2
3
4
5
6
7
8
9
10
11
Environment.MEDIA_REMOVED;//媒体存储已经移除了
Environment.MEDIA_UNMOUNTED;//存储媒体没有挂载
Environment.MEDIA_CHECKING;//正在检查存储媒体
Environment.MEDIA_NOFS;//存储媒体是空白或是不支持的文件系统no_file_system
Environment.MEDIA_MOUNTED;//存储媒体已经挂载,并且挂载点可读/写
Environment.MEDIA_MOUNTED_READ_ONLY;//存储媒体已经挂载,挂载点只读
Environment.MEDIA_SHARED;//存储媒体正在通过USB共享
Environment.MEDIA_BAD_REMOVAL;//在没有挂载前存储媒体已经被移除
Environment.MEDIA_UNMOUNTABLE;//存储媒体无法挂载,可能是文件系统损坏了
Environment.MEDIA_EJECTING;//存储媒体正在移除
Environment.MEDIA_UNKNOWN;//未知的存储状态

下面图片大概地概括了上面的方法 image

总结:

  • Context中的方法或得到的路径都与应用包名相关*
  • Environment中的方法与整个系统有关*

/storage/sdcard0, /sdcard, /mnt/sdcard ,/storage/emulated/legacy 的区别

关于android的4.2的0文件夹的详解

android 4.0

在galaxy nexus(GN)手机上userdata分区很大,被挂在/data目录,用户的数据通常是放在sd卡上,然而gn是没有sd卡的,所以google想了一个办法,就是虚拟一个。

所以,在userdata分区下有个目录叫media,是内置sd卡的数据存储位置,使用fuse技术将/data/media虚拟成为一个叫做/dev/fuse的设备,为了让程序能认出来,被同时挂载在 /mnt/sdcard 目录, 又为了兼容以前的程序,做了一个快捷方式(linux系统里叫软连接) /sdcard指向的是 /mnt/sdcard .

当然,这些都是4.0的做法。

android 4.1

在4.1里,同样也会使用fuse技术,/dev/fuse 会被同时挂载到/storage/sdcard0 目录,这个sdcard0表示第一个sd卡(如果有外置sd卡,那会多一个 /storage/sdcard1,比如我的xoom), /sdcard 软连接会指向 /storage/sdcard0 ,此时/mnt/sdcard 也是个软连接,会指向/storage/sdcard0。 如果你通过otg线接U盘,会被挂载到 /storage/usb0目录,stickmount这个软件为了让图库、快图、mx player等软件,能看到u盘里的数据,又同时挂载到 /storage/sdcard0/usStorage/sda1.

也许你会问,为什么不是usb0,而是sda1,这是linux的对硬盘的命名方式,如果你的u盘有多个分区,就分别是sda1,sda2这样一直排下去了。

android 4.2

谷歌是不是没事干啊,非要给android搞个多用户,你想想啊,在中国,可能因为经济问题,家里不是每人一个电脑,在美国,几乎需要用电脑的人,都会自己有一台或多台,一台电脑多人用的情况少之又少,这就是为什么叫PC了,顾名思义,个人电脑。像手机和平板这些东西,更加私人化了,很少公用了吧,我想在中国也是如此吧。

当然,谷歌也不完全是抽风,因为他有更大的战略部署,而且平板也的确有多人用的可能。

所以谷歌搞出来一个多用户,那每个人的应用、数据、个性配置都要分开吧。 应用和个性配置好弄,想想啊,通过权限控制,每人只能看自己的应用就行了,桌面也可以用自己的。

那数据怎么办????

好吧,调整用户数据的挂载结构。android 4.2,同样也会使用fuse技术/dev/fuse 会被挂载到/storage/emulated/0 目录,为什么是0呢,你还记得上边的sdcard0吧,第一个的意思。(如果有第二个,应该就是/storage/emulated/1,我们的三儿子没有外置sd卡,所以没法验证)

为了兼容以前,同时挂载到 /storage/emulated/legacy (故名思议,传统的),还建立三个软连接 /storage/sdcard0 ,/sdcard,/mnt/sdcard ,都指向 /storage/emulated/legacy

还有值得一提的是,4.2刚出来,这块变动又比较大,所以stickmount要升级到2.2之后,才可以通过otg挂载u盘了。

也许你会问,这个0和多用户有什么关系呢,那是因为多用户这个新特性,只在平板上才启用,在手机上会被禁用的。但是底层实现是一致的。 /mnt/shell/emulated 目录和 /storage/emulated 下的文件夹是一样的。(注意,这个/mnt/shell/emulated 不是挂载出来的)

/mnt/shell/是为了多用户准备的,因为linux的多用户是基于shell实现的。

4.2在平板上的多用户 我前一段时间给XOOM Wifi刷上了CM10.1的4.2.1,成功开启多用户特性。新建的用户id从10开始。

  • 默认用户的sdcard目录: /storage/emulated/0
  • 新建的第一个用户的sdcard目录: /storage/emulated/10
  • 新建的第二个用户的sdcard目录: /storage/emulated/11

Android插件化(四)基础之Hook

发表于 2018-01-12 | 分类于 Android

1、寻找Hook点的原则

Android中主要是依靠分析系统源码类来做到的,首先我们得找到被Hook的对象,我称之为Hook点;什么样的对象比较好Hook呢?一般来说,静态变量和单例变量是相对不容易改变,是一个比较好的hook点,而普通的对象有易变的可能,每个版本都不一样,处理难度比较大。我们根据这个原则找到所谓的Hook点。

2、寻找Hook点

通常点击一个Button就开始Activity跳转了,这中间发生了什么,我们如何Hook,来实现Activity启动的拦截呢?

1
2
3
4
public void start(View view) {
Intent intent = new Intent(this, OtherActivity.class);
startActivity(intent);
}

我们的目的是要拦截startActivity方法,跟踪源码,发现最后启动Activity是由Instrumentation类的execStartActivity做到的。其实这个类相当于启动Activity的中间者,启动Activity中间都是由它来操作的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
....
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);

//通过ActivityManagerNative.getDefault()获取一个对象,开始启动新的Activity
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);


checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}

对于ActivityManagerNative这个东东,熟悉Activity/Service启动过程的都不陌生

1
public abstract class ActivityManagerNative extends Binder implements IActivityManager

继承了Binder,实现了一个IActivityManager接口,这就是为了远程服务通信做准备的”Stub”类,一个完整的AID L有两部分,一个是个跟服务端通信的Stub,一个是跟客户端通信的Proxy。ActivityManagerNative就是Stub,阅读源码发现在ActivityManagerNative 文件中还有个ActivityManagerProxy,这里就多不扯了。

1
2
3
static public IActivityManager getDefault() {
return gDefault.get();
}

ActivityManagerNative.getDefault()获取的是一个IActivityManager对象,由IActivityManager去启动Activity,IActivityManager的实现类是ActivityManagerService,ActivityManagerService是在另外一个进程之中,所有Activity 启动是一个跨进程的通信的过程,所以真正启动Activity的是通过远端服务ActivityManagerService来启动的。

1
2
3
4
5
6
7
8
9
10
11
12
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}

实gDefalut借助Singleton实现的单例模式,而在内部可以看到先从ServiceManager中获取到AMS远端服务的Binder对象,然后使用asInterface方法转化成本地化对象,我们目的是拦截startActivity,所以改变IActivityManager对象可以做到这个一点,这里gDefault又是静态的,根据Hook原则,这是一个比较好的Hook点。

3、Hook掉startActivity,输出日志

我们先实现一个小需求,启动Activity的时候打印一条日志,写一个工具类HookUtil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class HookUtil {

private Class<?> proxyActivity;

private Context context;

public HookUtil(Class<?> proxyActivity, Context context) {
this.proxyActivity = proxyActivity;
this.context = context;
}

public void hookAms() {

//一路反射,直到拿到IActivityManager的对象
try {
Class<?> ActivityManagerNativeClss = Class.forName("android.app.ActivityManagerNative");
Field defaultFiled = ActivityManagerNativeClss.getDeclaredField("gDefault");
defaultFiled.setAccessible(true);
Object defaultValue = defaultFiled.get(null);
//反射SingleTon
Class<?> SingletonClass = Class.forName("android.util.Singleton");
Field mInstance = SingletonClass.getDeclaredField("mInstance");
mInstance.setAccessible(true);
//到这里已经拿到ActivityManager对象
Object iActivityManagerObject = mInstance.get(defaultValue);


//开始动态代理,用代理对象替换掉真实的ActivityManager,瞒天过海
Class<?> IActivityManagerIntercept = Class.forName("android.app.IActivityManager");

AmsInvocationHandler handler = new AmsInvocationHandler(iActivityManagerObject);

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{IActivityManagerIntercept}, handler);

//现在替换掉这个对象
mInstance.set(defaultValue, proxy);


} catch (Exception e) {
e.printStackTrace();
}
}


private class AmsInvocationHandler implements InvocationHandler {

private Object iActivityManagerObject;

private AmsInvocationHandler(Object iActivityManagerObject) {
this.iActivityManagerObject = iActivityManagerObject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

Log.i("HookUtil", method.getName());
//我要在这里搞点事情
if ("startActivity".contains(method.getName())) {
Log.e("HookUtil","Activity已经开始启动");
Log.e("HookUtil","小弟到此一游!!!");
}
return method.invoke(iActivityManagerObject, args);
}
}
}

结合注释应该很容易看懂,在Application中配置一下

1
2
3
4
5
6
7
8
9
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
HookUtil hookUtil=new HookUtil(SecondActivity.class, this);
hookUtil.hookAms();
}
}

可以看到,我们成功的Hook掉了startActivity,输出了一条日志。有了上面的基础,现在我们开始来点有用的东西,Activity不用在清单文件中注册,就可以启动起来,这个怎么搞呢?

4、无需注册,启动Activity

如下,TargetActivity没有在清单文件中注册,怎么去启动TargetActivity?

1
2
3
4
public void start(View view) {
Intent intent = new Intent(this, TargetActivity.class);
startActivity(intent);
}

这个思路可以是这样,上面已经拦截了启动Activity流程,在invoke中我们可以得到启动参数intent信息,那么就在这里,我们可以自己构造一个假的Activity信息的intent,这个Intent启动的Activity是在清单文件中注册的,当真正启动的时候(ActivityManagerService校验清单文件之后),用真实的Intent把代理的Intent在调换过来,然后启动即可。

首先获取真实启动参数intent信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".contains(method.getName())) {
//换掉
Intent intent = null;
int index = 0;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Intent) {
//说明找到了startActivity的Intent参数
intent = (Intent) args[i];
//这个意图是不能被启动的,因为Acitivity没有在清单文件中注册
index = i;
}
}

//伪造一个代理的Intent,代理Intent启动的是proxyActivity
Intent proxyIntent = new Intent();
ComponentName componentName = new ComponentName(context, proxyActivity);
proxyIntent.setComponent(componentName);
proxyIntent.putExtra("oldIntent", intent);
args[index] = proxyIntent;
}

return method.invoke(iActivityManagerObject, args);
}

有了上面的两个步骤,这个代理的Intent是可以通过ActivityManagerService检验的,因为我在清单文件中注册过

1
<activity android:name=".ProxyActivity" />

为了不启动ProxyActivity,现在我们需要找一个合适的时机,把真实的Intent换过了来,启动我们真正想启动的Activity。看过Activity的启动流程的朋友,我们都知道这个过程是由Handler发送消息来实现的,可是通过Handler处理消息的代码来看,消息的分发处理是有顺序的,下面是Handler处理消息的代码:

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

handler处理消息的时候,首先去检查是否实现了callback接口,如果有实现的话,那么会直接执行接口方法,然后才是handleMessage方法,最后才是执行重写的handleMessage方法,我们一般大部分时候都是重写了handleMessage方法,而ActivityThread主线程用的正是重写的方法,这种方法的优先级是最低的,我们完全可以实现接口来替换掉系统Handler的处理过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void hookSystemHandler() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//获取主线程对象
Object activityThread = currentActivityThreadMethod.invoke(null);
//获取mH字段
Field mH = activityThreadClass.getDeclaredField("mH");
mH.setAccessible(true);
//获取Handler
Handler handler = (Handler) mH.get(activityThread);
//获取原始的mCallBack字段
Field mCallBack = Handler.class.getDeclaredField("mCallback");
mCallBack.setAccessible(true);
//这里设置了我们自己实现了接口的CallBack对象
mCallBack.set(handler, new ActivityThreadHandlerCallback(handler)) ;

} catch (Exception e) {
e.printStackTrace();
}
}

自定义Callback类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private class ActivityThreadHandlerCallback implements Handler.Callback {

private Handler handler;

private ActivityThreadHandlerCallback(Handler handler) {
this.handler = handler;
}

@Override
public boolean handleMessage(Message msg) {
Log.i("HookAmsUtil", "handleMessage");
//替换之前的Intent
if (msg.what ==100) {
Log.i("HookAmsUtil","lauchActivity");
handleLauchActivity(msg);
}

handler.handleMessage(msg);
return true;
}

private void handleLauchActivity(Message msg) {
Object obj = msg.obj;//ActivityClientRecord
try{
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent proxyInent = (Intent) intentField.get(obj);
Intent realIntent = proxyInent.getParcelableExtra("oldIntent");
if (realIntent != null) {
proxyInent.setComponent(realIntent.getComponent());
}
}catch (Exception e){
Log.i("HookAmsUtil","lauchActivity falied");
}

}
}

最后在application中注入

1
2
3
4
5
6
7
8
9
10
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//这个ProxyActivity在清单文件中注册过,以后所有的Activitiy都可以用ProxyActivity无需声明,绕过监测
HookAmsUtil hookAmsUtil = new HookAmsUtil(ProxyActivity.class, this);
hookAmsUtil.hookSystemHandler();
hookAmsUtil.hookAms();
}
}

Android插件化(三)基础之Android应用程序资源的编译和打包过程分析

发表于 2018-01-12 | 分类于 Android

Android资源加载常规思路

getResourcesForApplication

1
2
3
4
5
6
//首先,通过包名获取该包名的Resources对象
Resources res= pm.getResourcesForApplication(packageName);
//根据约定好的名字,去取资源id;
int id=res.getIdentifier("a","drawable",packageName);//根据名字取id
//根据资源id,取出资源
Drawable drawable=res.getDrawable(id)

Android Apk打包流程

  1. 打包资源文件,生成R.java文件;
  2. 处理aidl文件,生成相应java文件;
  3. 编译工程源文件,生成相应class文件;
  4. 转换所有class文件,生成classes.dex文件;
  5. 打包生成apk文件;
  6. 对apk文件进行签名;
  7. 对签名后的apk文件进行对齐处理;

image

打包过程使用的工具

名称 功能介绍 在操作系统中的路径 源码路径
aapt(Android Asset Package Tool) Android资源打包工具 ${ANDROID_SDK_HOME} /build-tools/ ANDROID_VERSION/aapt frameworks\base\tools\aap
aidl(android interface definition language) Android接口描述语言,将aidl转化为.java文件的工具 ${ANDROID_SDK_HOME}/build-tools/ ANDROID_VERSION/aidl frameworks\base\tools\aidl
javac Java Compiler ${JDK_HOME}/javac或/usr/bin/javac
dex 转化.class文件为Davik VM能识别的.dex文件 ${ANDROID_SDK_HOME}/build-tools/ ANDROID_VERSION/dx
apkbuilder 生成apk包 ${ANDROID_SDK_HOME}/tools/apkbuilder sdk\sdkmanager\libs\sdklib\ src\com\android\sdklib\build\ApkBuilderMain.java
jarsigner .jar文件的签名工具 ${JDK_HOME}/jarsigner或/usr/bin/jarsigner
zipalign 字节码对齐工具 ${ANDROID_SDK_HOME}/tools和/zipalign
第一步: 打包资源文件,生成R.java文件

【输入】Resource文件(就是工程中res中的文件)、Assets文件(相当于另外一种资源,这种资源Android系统并不像对res中的文件那样优化它)、AndroidManifest.xml文件(包名就是从这里读取的,因为生成R.java文件需要包名)、Android基础类库(Android.jar文件) 【工具】aapt工具 【输出】打包好的资源(bin目录中的resources.ap_文件)、R.java文件(gen目录中) 打包资源的工具aapt,大部分文本格式的XML资源文件会被编译成二进制格式的XML资源文件,除了assets和res/raw资源被原装不动地打包进APK之外,其它的资源都会被编译或者处理。 。 生成过程主要是调用了aapt源码目录下的Resource.cpp文件中的buildResource()函数,该函数首先检查AndroidManifest.xml的合法性,然后对res目录下的资源子目录进行处理,处理的函数为makeFileResource(),处理的内容包括资源文件名的合法性检查,向资源表table添加条目等,处理完后调用compileResourceFile()函数编译res与asserts目录下的资源并生成resources.arsc文件,compileResourceFile()函数位于aapt源码目录的ResourceTable.cpp文件中,该函数最后会调用parseAndAddEntry()函数生成R.java文件,完成资源编译后,接下来调用compileXmlfile()函数对res目录的子目录下的xml文件分别进行编译,这样处理过的xml文件就简单的被“加密”了,最后将所有的资源与编译生成的resorces.arsc文件以及“加密”过的AndroidManifest.xml文件打包压缩成resources.ap_文件(使用Ant工具命令行编译则会生成与build.xml中“project name”指定的属性同名的ap_文件)。 关于这一步更详细的流程可阅读http://blog.csdn.net/luoshengyang/article/details/8744683

res目录有9种目录

  • –animator。这类资源以XML文件保存在res/animator目录下,用来描述属性动画。
  • –anim。这类资源以XML文件保存在res/anim目录下,用来描述补间动画。
  • –color。这类资源以XML文件保存在res/color目录下,用描述对象颜色状态选择子。
  • –drawable。这类资源以XML或者Bitmap文件保存在res/drawable目录下,用来描述可绘制对象。例如,我们可以在里面放置一些图片(.png, .9.png, .jpg, .gif),来作为程序界面视图的背景图。注意,保存在这个目录中的Bitmap文件在打包的过程中,可能会被优化的。例如,一个不需要多于256色的真彩色PNG文件可能会被转换成一个只有8位调色板的PNG面板,这样就可以无损地压缩图片,以减少图片所占用的内存资源。
  • –layout。这类资源以XML文件保存在res/layout目录下,用来描述应用程序界面布局。
  • –menu。这类资源以XML文件保存在res/menu目录下,用来描述应用程序菜单。
  • –raw。这类资源以任意格式的文件保存在res/raw目录下,它们和assets类资源一样,都是原装不动地打包在apk文件中的,不过它们会被赋予资源ID,这样我们就可以在程序中通过ID来访问它们。例如,假设在res/raw目录下有一个名称为filename的文件,并且它在编译的过程,被赋予的资源ID为R.raw.filename,那么就可以使用以下代码来访问它:Resources res = getResources(); InputStream is = res .openRawResource(R.raw.filename);
  • –values。这类资源以XML文件保存在res/values目录下,用来描述一些简单值,例如,数组、颜色、尺寸、字符串和样式值等,一般来说,这六种不同的值分别保存在名称为arrays.xml、colors.xml、dimens.xml、strings.xml和styles.xml文件中。
  • –xml。这类资源以XML文件保存在res/xml目录下,一般就是用来描述应用程序的配置信息。
第二步:处理aidl文件,生成相应的java文件。

输入】源码文件、aidl文件、framework.aidl文件 【工具】aidl工具 【输出】对应的.java文件 对于没有使用到aidl的android工程,这一步可以跳过。aidl工具解析接口定义文件并生成相应的java代码供程序调用。

第三步:编译工程源代码,生成下相应的class文件。

【输入】源码文件(包括R.java和AIDL生成的.java文件)、库文件(.jar文件) 【工具】javac工具 【输出】.class文件 这一步调用了javac编译工程src目录下所有的java源文件,生成的class文件位于工程的bin\classes目录下,上图假定编译工程源代码时程序是基于android SDK开发的,实际开发过程中,也有可能会使用android NDK来编译native代码,因此,如果可能的话,这一步还需要使用android NDK编译C/C++代码,当然,编译C/C++代码的步骤也可以提前到第一步或第二步。

第四步:转换所有的class文件,生成classes.dex文件。

【输入】 .class文件(包括Aidl生成.class文件,R生成的.class文件,源文件生成的.class文件),库文件(.jar文件) 【工具】javac工具 【输出】.dex文件 前面多次提到,android系统dalvik虚拟机的可执行文件为dex格式,程序运行所需的classes.dex文件就是在这一步生成的,使用的工具为dx,dx工具主要的工作是将java字节码转换为dalvik字节码、压缩常量池、消除冗余信息等。

第五步:打包生成apk。

【输入】打包后的资源文件、打包后类文件(.dex文件)、libs文件(包括.so文件,当然很多工程都没有这样的文件,如果你不使用C/C++开发的话) 【工具】apkbuilder工具 【输出】未签名的.apk文件 打包工具为apkbuilder,apkbuilder为一个脚本文件,实际调用的是android-sdk\tools\lib\sdklib.jar文件中的com.android.sdklib.build.ApkBuilderMain类。它的代码实现位于android系统源码的sdk\sdkmanager\libs\sdklib\src\com\android\sdklib\build\ApkBuilderMain.java文件,代码构建了一个ApkBuilder类,然后以包含resources.arsc的文件为基础生成apk文件,这个文件一般为ap_结尾,接着调用addSourceFolder()函数添加工程资源,addSourceFolder()会调用processFileForResource()函数往apk文件中添加资源,处理的内容包括res目录与asserts目录中的文件,添加完资源后调用addResourceFromJar()函数往apk文件中写入依赖库,接着调用addNativeLibraries()函数添加工程libs目录下的Native库(通过android NDK编译生成的so或bin文件),最后调用sealApk()关闭apk文件。

第六步:对apk文件进行签名。

【输入】未签名的.apk文件 【工具】jarsigner 【输出】签名的.apk文件 android的应用程序需要签名才能在android设备上安装,签名apk文件有两种情况:一种是在调试程序时进行签名,使用eclipse开发android程序时,在编译调试程序时会自己使用一个debug.keystore对apk进行签名;另一种是打包发布时对程序进行签名,这种情况下需要提供一个符合android开发文档中要求的签名文件。签名的方法也分两种:一种是使用jdk中提供的jarsigner工具签名;另一种是使用android源码中提供的signapk工具,它的代码位于android系统源码build\tools\signapk目录下。

第七步:对签名后的apk文件进行对齐处理。

【输入】签名后的.apk文件 【工具】zipalign工具 【输出】对齐后的.apk文件 这一步需要使用的工具为zipalign,它位于android-sdk\tools目录,源码位于android系统源码的build\tools\zipalign目录,它的主要工作是将spk包进行对齐处理,使spk包中的所有资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时速度会更快,验证apk文件是否对齐过的工作由ZipAlign.cpp文件的verify()函数完成,处理对齐的工作则由process()函数完成。

以一个具体项目中包含的具体文件为例作图如下: image

APK文件内容解析

android的项目经过编译和打包,形成了:

  • .dex 文件
  • resources.arsc
  • uncompiled resources
  • AndroidManifest.xml

解压一个普通的apk文件,解压出来的文件如下:

  • META-INF文件夹
  • res文件夹
  • AndroidManifest.xml
  • classes.dex
  • resources.arsc

classes.dex 是.dex文件。 resources.arsc是resources resources文件。 AndroidManifest.xml是AndroidManifest.xml文件。 res是uncompiled resources。 META-INF是签名文件夹。

META-INF其中有三个文件:

  • CERT.RSA
  • CERT.SF
  • MANIFEST.MF

MANIFEST.MF文件 版本号以及每一个文件的哈希值(BASE64)。包括资源文件。这个是对每个文件的整体进行SHA1(hash)。

1
2
3
4
5
6
7
8
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 2.2.0
Name: res/drawable-xhdpi-v4/abc_scrubber_control_to_pressed_mtrl_005.png
SHA1-Digest: I9s6aQ5VyOLrNo4odqSij549Oyo=
Name: res/drawable-mdpi-v4/abc_textfield_search_default_mtrl_alpha.9.png
SHA1-Digest: D6dilO+UMcglambujyMOhNbLZuY=
……

CERT.SF 这个是对每个文件的头3行进行SHA1 hash。

1
2
3
4
5
6
7
8
9
Signature-Version: 1.0
X-Android-APK-Signed: 2
SHA1-Digest-Manifest: QxOfCCAuQtZnHh0YRNnoxmiHT80=
Created-By: 1.0 (Android)
Name: res/drawable-xhdpi-v4/abc_scrubber_control_to_pressed_mtrl_005.png
SHA1-Digest: I9s6aQ5VyOLrNo4odqSij549Oyo=
Name: res/drawable-mdpi-v4/abc_textfield_search_default_mtrl_alpha.9.png
SHA1-Digest: D6dilO+UMcglambujyMOhNbLZuY=
……

CERT.RSA 这个文件保存了签名和公钥证书。

插件化中资源冲突解决

如果需要宿主、插件之间使用同一套资源管理器,那么我们需要将插件的资源路径添加到宿主的AssetManager中。

我们知道,apk包括代码和资源,在apk编译过程中,dex工具将代码打包成.dex文件,资源文件会由aapt工具生成对应的ID,aapt在打包的时候组织成resources.arsc文件,resources.arsc文件是用来描述资源ID和资源位置配置信息,从18个维度描述了一个资源ID的配置信息(语言、分辨率等),就是资源ID和资源的索引表。资源的ID生成是有规则的,规则:0xPPTTNNNN,由8位16进制组成,其中: PP段:表示资源的包空间:0x01表示系统资源空间,0x7f表示应用资源空间。 TT段:表示资源类型。 NNNN段:4个16进制表示资源id,一个apk中同一类型资源从0000开始递增。 例如:

1
2
3
4
5
6
7
8
9
nt anim pop_dialog_in 0x7f040000
int anim pop_dialog_out 0x7f040001
int anim slide_left_in 0x7f040002
int anim slide_left_out 0x7f040003
int anim slide_right_in 0x7f040004
int anim slide_right_out 0x7f040005
int anim update_loading_progressbar_anim 0x7f040006
int array indicator_tab_icon 0x7f050001
int array indicator_tab_titlt 0x7f050000

现在问题来了,宿主apk和插件apk是独立编译出来的两个独立的apk,那么其中就有资源ID相同的情况出现,从而产生资源ID冲突。如何解决这个问题?看了一些开源框架,解决的办法就是修改资源ID的PP段,大体有两种做法:

  1. 修改aapt源码,定制aapt工具编译期间修改PP段。 DynamicAPK的做法就是如此,定制aapt,替换google的原始aapt,在编译的时候可以传入参数修改PP段:例如传入0x05编译得到的资源的PP段就是0x05。个人觉得这个做法不是太灵活,入侵了原有的开发编译流程,不好维护。
  2. 修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID。 前面说过apk编译之后会生成ID以及对应的索引表resorce.arsc,那么我们能不能后期修改相关ID及索引表呢?答案是肯定的,个人比较赞同这种思路,不用入侵原有编译流程。

插件可能是 Apk 也可能是 so 格式,不管哪一种,都不会生成 R.id ,从而没办法使用。这个问题有好几种解决方案。一种是是重写 Context 的 getAsset 、 getResource 之类的方法,偷换概念,让插件读取插件里的资源,但缺点就是宿主和插件的资源 id 会冲突,需要重写 AAPT 。另一种是重写 AMS中保存的插件列表,从而让宿主和插件分别去加载各自的资源而不会冲突。第三种方法,就是打包后,执行一个脚本,修改生成包中资源id。

Android插件化(三)基础之反射与代理

发表于 2018-01-12 | 分类于 Android

1.反射

反射机制中的类:

  • java.lang.Class;
  • java.lang.reflect.Constructor;
  • java.lang.reflect.Field;
  • java.lang.reflect.Method;
  • java.lang.reflect.Modifier;
  1. 获取Class的三种方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /第一种方式:  
    Classc1 = Class.forName("Employee");
    //第二种方式:
    //java中每个类型都有class 属性.
    Classc2 = Employee.class;

    //第三种方式:
    //java语言中任何一个java对象都有getClass 方法
    Employeee = new Employee();
    Classc3 = e.getClass(); //c3是运行时类 (e的运行时类是Employee)
  2. 创建对象

    1
    2
    3
    4
    Class c =Class.forName("Employee");  

    //创建此Class 对象所表示的类的一个新实例
    Objecto = c.newInstance(); //调用了Employee的无参数构造方法.
  3. 获取属性:分为所有的属性和指定的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    //获取整个类  
    Class c = Class.forName("java.lang.Integer");
    //获取所有的属性?
    Field[] fs = c.getDeclaredFields();

    //定义可变长的字符串,用来存储属性
    StringBuffer sb = new StringBuffer();
    //通过追加的方法,将每个属性拼接到此字符串中
    //最外边的public定义
    sb.append(Modifier.toString(c.getModifiers()) + " class " + c.getSimpleName() +"{\n");
    //里边的每一个属性
    for(Field field:fs){
    sb.append("\t");//空格
    sb.append(Modifier.toString(field.getModifiers())+" ");//获得属性的修饰符,例如public,static等等
    sb.append(field.getType().getSimpleName() + " ");//属性的类型的名字
    sb.append(field.getName()+";\n");//属性的名字+回车
    }
    sb.append("}");
    System.out.println(sb);


    //获取特定属性
    //获取类
    Class c = Class.forName("User");
    //获取id属性
    Field idF = c.getDeclaredField("id");
    //实例化这个类赋给o
    Object o = c.newInstance();
    //打破封装
    idF.setAccessible(true); //使用反射机制可以打破封装性,导致了java对象的属性不安全。
    //给o对象的id属性赋值"110"
    idF.set(o, "110"); //set
    //get
    System.out.println(idF.get(o));
  4. 关键字

方法关键字 含义
getDeclaredMethods() 获取所有的方法
getReturnType() 获得方法的放回类型
getParameterTypes() 获得方法的传入参数类型
getDeclaredMethod(“方法名”,参数类型.class,……) 获得特定的方法
构造方法关键字 含义
getDeclaredConstructors() 获取所有的构造方法
getDeclaredConstructor(参数类型.class,……) 获取特定的构造方法
父类和父接口 含义
getSuperclass() 获取某类的父类
getInterfaces() 获取某类实现的接口
### 2.代理模式
定义:给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。
image
  • RealSubject 是原对象(本文把原对象称为”委托对象”),Proxy 是代理对象。
  • Subject 是委托对象和代理对象都共同实现的接口。
  • Request() 是委托对象和代理对象共同拥有的方法。

Java 实现上面的UML图的代码(即实现静态代理)为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ProxyDemo {
public static void main(String args[]){
RealSubject subject = new RealSubject();
Proxy p = new Proxy(subject);
p.request();
}
}

interface Subject{
void request();
}

class RealSubject implements Subject{
public void request(){
System.out.println("request");
}
}

class Proxy implements Subject{
private Subject subject;
public Proxy(Subject subject){
this.subject = subject;
}
public void request(){
System.out.println("PreProcess");
subject.request();
System.out.println("PostProcess");
}
}

代理的实现分为:

  • 静态代理:代理类是在编译时就实现好的。也就是说 Java 编译完成后代理类是一个实际的 class 文件。
  • 动态代理:代理类是在运行时生成的。也就是说 Java 编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到JVM中。

2.1Java 实现动态代理

首先先说明几个词:

  • 委托类和委托对象:委托类是一个类,委托对象是委托类的实例。
  • 代理类和代理对象:代理类是一个类,代理对象是代理类的实例。

Java实现动态代理的大致步骤如下:

  1. 定义一个委托类和公共接口。
  2. 自己定义一个类(调用处理器类,即实现 InvocationHandler 接口),这个类的目的是指定运行时将生成的代理类需要完成的具体任务(包括Preprocess和Postprocess),即代理类调用任何方法都会经过这个调用处理器类(在本文最后一节对此进行解释)。
  3. 生成代理对象(当然也会生成代理类),需要为他指定(1)委托对象(2)实现的一系列接口(3)调用处理器类的实例。因此可以看出一个代理对象对应一个委托对象,对应一个调用处理器实例。

Java 实现动态代理主要涉及以下几个类: java.lang.reflect.Proxy: 这是生成代理类的主类,通过 Proxy 类生成的代理类都继承了 Proxy 类,即 DynamicProxyClass extends Proxy。 java.lang.reflect.InvocationHandler: 这里称他为”调用处理器”,他是一个接口,我们动态生成的代理类需要完成的具体内容需要自己定义一个类,而这个类必须实现 InvocationHandler 接口。

Proxy 类主要方法为:

1
2
//创建代理对象  
static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

这个静态函数的第一个参数是类加载器对象(即哪个类加载器来加载这个代理类到 JVM 的方法区),第二个参数是接口(表明你这个代理类需要实现哪些接口),第三个参数是调用处理器类实例(指定代理类中具体要干什么)。这个函数是 JDK 为了程序员方便创建代理对象而封装的一个函数,因此你调用newProxyInstance()时直接创建了代理对象(略去了创建代理类的代码)。其实他主要完成了以下几个工作:

1
2
3
4
5
6
7
8
9
static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler handler)
{
//1. 根据类加载器和接口创建代理类
Class clazz = Proxy.getProxyClass(loader, interfaces);
//2. 获得代理类的带参数的构造函数
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class });
//3. 创建代理对象,并制定调用处理器实例为参数传入
Interface Proxy = (Interface)constructor.newInstance(new Object[] {handler});
}

Proxy 类还有一些静态方法,比如:

1
2
InvocationHandler getInvocationHandler(Object proxy): 获得代理对象对应的调用处理器对象。
Class getProxyClass(ClassLoader loader, Class[] interfaces): 根据类加载器和实现的接口获得代理类。

Proxy 类中有一个映射表,映射关系为:(,(,) ),可以看出一级key为类加载器,根据这个一级key获得二级映射表,二级key为接口数组,因此可以看出:一个类加载器对象和一个接口数组确定了一个代理类。

我们写一个简单的例子来阐述 Java 实现动态代理的整个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class DynamicProxyDemo01 {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject(); //1.创建委托对象
ProxyHandler handler = new ProxyHandler(realSubject); //2.创建调用处理器对象
Subject proxySubject = (Subject)Proxy.newProxyInstance(RealSubject.class.getClassLoader(),
RealSubject.class.getInterfaces(), handler); //3.动态生成代理对象
proxySubject.request(); //4.通过代理对象调用方法
}
}

/**
* 接口
*/
interface Subject{
void request();
}

/**
* 委托类
*/
class RealSubject implements Subject{
public void request(){
System.out.println("====RealSubject Request====");
}
}
/**
* 代理类的调用处理器
*/
class ProxyHandler implements InvocationHandler{
private Subject subject;
public ProxyHandler(Subject subject){
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("====before====");//定义预处理的工作,当然你也可以根据 method 的不同进行不同的预处理工作
Object result = method.invoke(subject, args);
System.out.println("====after====");
return result;
}
}

InvocationHandler 接口中有方法:

1
invoke(Object proxy, Method method, Object[] args)

这个函数是在代理对象调用任何一个方法时都会调用的,方法不同会导致第二个参数method不同,第一个参数是代理对象(表示哪个代理对象调用了method方法),第二个参数是 Method 对象(表示哪个方法被调用了),第三个参数是指定调用方法的参数。

动态生成的代理类具有几个特点:

  • 继承 Proxy 类,并实现了在Proxy.newProxyInstance()中提供的接口数组。
  • public final。
  • 命名方式为 $ProxyN,其中N会慢慢增加,一开始是 $Proxy1,接下来是$Proxy2…
  • 有一个参数为 InvocationHandler 的构造函数。这个从 Proxy.newProxyInstance() 函数内部的clazz.getConstructor(new Class[] { InvocationHandler.class }) 可以看出。

Java 实现动态代理的缺点:因为 Java 的单继承特性(每个代理类都继承了 Proxy 类),只能针对接口创建代理类,不能针对类创建代理类。

不难发现,代理类的实现是有很多共性的(重复代码),动态代理的好处在于避免了这些重复代码,只需要关注操作。

2.2Java 动态代理的内部实现

现在我们就会有一个问题: Java 是怎么保证代理对象调用的任何方法都会调用 InvocationHandler 的 invoke() 方法的?

这就涉及到动态代理的内部实现。假设有一个接口 Subject,且里面有 int request(int i) 方法,则生成的代理类大致如下:

1
2
3
4
5
6
7
8
9
10
11
public final class $Proxy1 extends Proxy implements Subject{
private InvocationHandler h;
private $Proxy1(){}
public $Proxy1(InvocationHandler h){
this.h = h;
}
public int request(int i){
Method method = Subject.class.getMethod("request", new Class[]{int.class}); //创建method对象
return (Integer)h.invoke(this, method, new Object[]{new Integer(i)}); //调用了invoke方法
}
}

通过上面的方法就成功调用了 invoke() 方法。

Android插件化(二)基础之类加载器

发表于 2018-01-12 | 分类于 Android

1.什么是ClassLoader

当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

2.Java ClassLoader

2.1.Java默认提供的三个ClassLoader

  1. BootStrap ClassLoader: 称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
    1
    2
    3
    4
    URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();  
    for (int i = 0; i < urls.length; i++) {
    System.out.println(urls[i].toExternalForm());
    }

以下内容是上述程序从本机JDK环境所获得的结果:

1
2
3
4
5
6
7
8
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/resources.jar
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/rt.jar
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jsse.jar
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jce.jar
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/charsets.jar
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jfr.jar
file:/Applications/Android%20Studio.app/Contents/jre/jdk/Contents/Home/jre/classes

其实上述结果也是通过查找sun.boot.class.path这个系统属性所得知的。

1
System.out.println(System.getProperty("sun.boot.class.path"));

打印结果:

1
/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/resources.jar:/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/rt.jar:/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/sunrsasign.jar:/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jsse.jar:/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jce.jar:/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/charsets.jar:/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/jfr.jar:/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/classes
  1. Extension ClassLoader: 称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。
  2. App ClassLoader: 称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

2.2ClassLoader加载类的原理

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:

  1. 在web服务器上建一个org.classloader.simple.NetClassLoaderSimple.java类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package org.classloader.simple;  

    public class NetClassLoaderSimple {

    private NetClassLoaderSimple instance;

    public void setNetClassLoaderSimple(Object obj) {
    this.instance = (NetClassLoaderSimple)obj;
    }
    }

org.classloader.simple.NetClassLoaderSimple类的setNetClassLoaderSimple方法接收一个Object类型参数,并将它强制转换成org.classloader.simple.NetClassLoaderSimple类型。

  1. 测试两个class是否相同 NetWorkClassLoader.java:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    package classloader;

    import java.io.ByteArrayOutputStream;
    import java.io.InputStream;
    import java.net.URL;

    /**
    * 加载网络class的ClassLoader
    */
    public class NetworkClassLoader extends ClassLoader {

    private String rootUrl;

    public NetworkClassLoader(String rootUrl) {
    this.rootUrl = rootUrl;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = null;//this.findLoadedClass(name); // 父类已加载
    //if (clazz == null) { //检查该类是否已被加载过
    byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组
    if (classData == null) {
    throw new ClassNotFoundException();
    }
    clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例
    //}
    return clazz;
    }

    private byte[] getClassData(String name) {
    InputStream is = null;
    try {
    String path = classNameToPath(name);
    URL url = new URL(path);
    byte[] buff = new byte[1024*4];
    int len = -1;
    is = url.openStream();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    while((len = is.read(buff)) != -1) {
    baos.write(buff,0,len);
    }
    return baos.toByteArray();
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (is != null) {
    try {
    is.close();
    } catch(IOException e) {
    e.printStackTrace();
    }
    }
    }
    return null;
    }

    private String classNameToPath(String name) {
    return rootUrl + "/" + name.replace(".", "/") + ".class";
    }

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package classloader;  

public class NewworkClassLoaderTest {

public static void main(String[] args) {
try {
//测试加载网络中的class文件
String rootUrl = "http://localhost:8080/httpweb/classes";
String className = "org.classloader.simple.NetClassLoaderSimple";
NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
Class<?> clazz1 = ncl1.loadClass(className);
Class<?> clazz2 = ncl2.loadClass(className);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}

}

首先获得网络上一个class文件的二进制名称,然后通过自定义的类加载器NetworkClassLoader创建两个实例,并根据网络地址分别加载这份class,并得到这两个ClassLoader实例加载后生成的Class实例clazz1和clazz2,最后将这两个Class实例分别生成具体的实例对象obj1和obj2,再通过反射调用clazz1中的setNetClassLoaderSimple方法。

结果抛出java.lang.ClassCastgException,虽然是同一份class字节码文件,但是由于被两个不同的ClassLoader实例所加载,所以JVM认为它们就是两个不同的类。

2.3ClassLoader的体系架构:

iamge

打印ClassLoader类的层次结构:

1
2
3
4
5
6
ClassLoader loader = ClassLoaderTest.class.getClassLoader();    //获得加载ClassLoaderTest.class这个类的类加载器  
while(loader != null) {
System.out.println(loader);
loader = loader.getParent(); //获得父类加载器的引用
}
System.out.println(loader);

输出:

1
2
3
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null

第一行结果说明:ClassLoaderTest的类加载器是AppClassLoader。

第二行结果说明:AppClassLoader的类加器是ExtClassLoader,即parent=ExtClassLoader。

第三行结果说明:ExtClassLoader的类加器是Bootstrap ClassLoader,因为Bootstrap ClassLoader不是一个普通的Java类,所以ExtClassLoader的parent=null,所以第三行的打印结果为null就是这个原因。

  • 将ClassLoaderTest.class打包成ClassLoaderTest.jar,放到Extension ClassLoader的加载目录下(JAVA_HOME/jre/lib/ext)可以测试Extension ClassLoader
  • 在jvm中添加-Xbootclasspath参数,指定Bootstrcp ClassLoader加载类的路径,并追加我们自已的jar(ClassTestLoader.jar)或 将class文件放到JAVA_HOME/jre/classes/目录下测试用Bootstrcp ClassLoader加载ClassLoaderTest.class.

2.4定义自己的ClassLoader:

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。

定义自已的类加载器分为两步:

  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法

参考:深入探讨 Java 类加载器

3.Android ClassLoader

Android ClassLoader种类:

  • DexClassLoader:可以加载文件系统上的jar、dex、apk
  • PathClassLoader:可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
  • URLClassLoader:可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用

关于jar、dex和apk,dex和apk是可以直接加载的,因为它们都是或者内部有dex文件,而原始的jar是不行的,必须转换成dalvik所能识别的字节码文件,转换工具可以使用android sdk中platform-tools目录下的dx 转换命令 :

1
dx --dex --output=dest.jar src.jar

Android开发和普通的java开发不同的地方是把class文件再重新打包成dex类型的文件,这种重新打包会对Class文件内部的各种函数表、变量表等进行优化。dex文件是一种经过android打包工具优化后的Class文件,因此加载这样特殊的Class文件就需要特殊的类装载器,所以android中提供了DexClassLoader类。加载流程如下:

  1. 通过PacageMangager获得指定的apk的安装的目录,dex的解压缩目录,c/c++库的目录
  2. 创建一个 DexClassLoader实例
  3. 加载指定的类返回一个Class
  4. 然后使用反射调用这个Class
1…101112…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次