reoger的记录

--以后的你会感激现在那么努力的自己

0%

LinkedHashMap实现LRUCache


LinkedHashMap实现原理

简单介绍一下LinkedHasMap的实现原理,针对JDK 8.0,在不同的版本上其实现可能有所区别。

原理概括

LinkedHasMap就是基于HashMap,通过维护一个双向链表,达到在使用HashMap存储的情况下,记录其顺序。
linkedHashMap结构图的示意图如图所示:
linkedHashMap结构图

水平所限,本篇不会把所有的实现都呈现出来,只对其其中的几个关键性的方法函数进行解析。

构造方法

其构造方法主要有三个,只介绍其中的一个。

1
2
3
4
5
6
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

这里的 initialCapacity表示初始的长度,loadFactor表示加载因子,accessOrder表示访问顺序,当其值为true时,表示当前的LinkedHashMap的顺序由访问数据时决定,即数据访问之后就会将这个数据放在LinkedHashMap数据项的前面来,而accessOrder为false时,则表示数据的访问数据由插入时就决定好了。

put方法

LinkedHashMap里面添加数据的方法就是通过put方法实现,在jdk 8.0中LinkedHashMap并没有自己实现put方法,而是由HashMap一同实现了。下面是具体的实现:

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
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

代码略长,长话短说,前面的大部分都是hashMap的实现,到了最后afterNodeInsertion,这个方法就是留给LinkedHashMap去实现其调换顺序的。我们直接看LinkedHashMap中这个方法的实现:

1
2
3
4
5
6
7
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

这个方法主要实现在某些情况下,需要将老的数据移除掉,移除就是通过removeNode实现的,我们不往下继续看,只关注其条件。我们观察到 需要移除老数据的添加有三个.

  1. evict的boolean为true
  2. head != null
  3. removeEldestEntry(first) 返回true
    其中,evict在调用时就是传入的值true,而headLinkedHashMap中初始化并添加数据后,就不会为null了,所以这里需要直接移除老数据的关键条件就是removeEldestEntry这个方法了,而这个方法在LinkedHashMap的实现中默认是范围false的,即默认不用移除掉老的数据。那么当我们需要LinkedHashMap存储的数据达到一定量的时候,移除掉老数据就需要重写removeEldestEntry这个方法了。

get

get的实现在LinkedHashMap重写了,实现也很简单,首先判断有没有这个key,如果有,在判断当前的accessOrder是不是为true,如果为true,则需要将顺序按照访问顺序调整一下,然后将数据返回回去。

1
2
3
4
5
6
7
8
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}

利用HashMap实现LRUCache

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
66
67
68
69
70
71
72
73
74
75
76
77
class LRUCache {

class Node {
int key;
int value;
Node pre;
Node next;
}

private int capacity;

private Node head, tail;

private Map<Integer, Node> map = null;

public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>(capacity);
head = new Node();
tail = new Node();
head.pre = null;
head.next = tail;
tail.pre = head;
tail.next = null;
}

public int get(int key) {
Node node = map.get(key);
if (node == null) {
return -1;
}
moveToHead(node);
return node.value;
}

private void moveToHead(Node node) {
removeNode(node);
addNode(node);
}

private void removeNode(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}

private void addNode(Node node) {
node.pre = head;
node.next = head.next;
head.next.pre = node;
head.next = node;
}

public void put(int key, int value) {
Node node = map.get(key);
if (node == null) {
Node newNode = new Node();
newNode.key = key;
newNode.value = value;
map.put(key, newNode);
addNode(newNode);
if (map.size() > capacity) {
Node tail = popTail();
map.remove(tail.key);
}
} else {
node.value = value;
moveToHead(node);
}
}

private Node popTail() {
Node node = tail.pre;
removeNode(node);
return node;
}

}

虚拟机类加载阶段


类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verifcation)、准备(Preparation)、解析(Resolution)、初始化(Initiializtion)、使用(Using)和卸载(Unloading) 7个阶段。
下面简单介绍这7个阶段。

加载

加载是类加载过程的一个阶段,加载阶段他需要完成以下3件事情:

  1. 通过一个类的全限定名来获得定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法去的这个类的各种数据的访问入口。
    在类的生命周期中,开发人员可控性最强的过程就是加载过程。我们可以通过控制类加载器和加载不同class文件,来实现对加载过程的控制。

验证

为了保证程序的可执行和保证程序是无害的,虚拟机在类加载完了之后,有必要对其进行验证。当然,如果确保了类是安全可执行的,这一步也可以通过指定-X
验证阶段主要有四个验证动作:

  1. 文件格式验证。
    主要验证文件格式是否符合要求,例如class文件是否以oxCAFEBAEF开头,版本号时候在有效范围内,常量池是否有不被支持的类型等等。
  2. 元数据验证。
    这一步是对字节码描述的信息记性语义分析,保证其描述符合java语言规范。主要验证内容包括类是否有父类,是否继承了final类;是否不是抽象类,但没有实现其继承的接口或者抽象类的方法。
  3. 字节码验证。
    这一步主要是验证数据流和控制流是合法的,是符合逻辑的。这一个主要验证的内容包括,方法体中的类型转换时有效的,跳转的方法是有效的等等。
  4. 符号应用验证。
    最后一步是校验符号引用是否能转化成直接引用,为下一个阶段的解析做准备。这一步主要校验的内容包括符号应用中通过字符串描述的全限定名是否能找到对应的类;在指定类中是否存在符合房费的字段描述符已经加单名称所描述的方法和字段,符号引用中的类、字段、方法的访问性是否能被当前类访问等等。

准备

准备阶段是正式为类变量分享内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。值得注意的一点是,这里只对其类变量(被static修饰)进行内存申请并是初始化,而成员变量的内存申请和初始化则在类初始化的时候一同进行。
还有一点,这个只对类变量进行初始化,并不对应赋值,例如:

1
public static int value = 123;

在准备之后,其初始化的值为0,而不是123,只有staitc final修饰的常亮才会在准备的时候指定值。例如

1
public static final int value = 123;

在准备之后其值就是123了。

解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。解析主要包括三种:

  1. 类或接口的解析。
  2. 字段解析。
  3. 类方法解析。
  4. 接口方法解析。

初始化

类初始化是类加载过程的最后一步,在初始化阶段,才真正开始执行类中定义的java程序代码。
在初始化阶段,会对类变量赋程序给定的值。

<clinit> 方法是由编辑器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编辑器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量。在前面的静态语句快可以赋值,但是不能访问。


end

括号生成

难度 中等

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

例如,给出 n =3,生成结果为:

1
2
3
4
5
6
7
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]

Solution

Language: Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
    public List<String> generateParenthesis(int n) {
       List<String> res = new ArrayList<>();
       traverse(res,"", n, n);
       return res;
  }

   private void traverse(List<String> res,String single, int left, int right) {
       if (left == 0 && right == 0){
           res.add(single);
           return;
      }
           
       if (left > 0) {
           traverse(res, single+"(", left - 1 , right);
      }
       if (right > 0 && right > left) {
           traverse(res, single+")", left, right - 1);
      }
  }
}

思路:

想一想这种生成括号的规则,其实隐含了一条信息:那就是始终左括号的数量要大于或等于右括号的数量。也就是说,剩余的左括号的数量要始终小于等于右括号。左括号只要有,就可以打印;而只有当剩余的右括号数量比左括号大时,才能打印右括号。为了方便理解,我现在假设n = 2,那么根据刚才我说的隐含信息,逻辑应该是这样的:

  1. 肯定要先取一个左括号,此时左括号剩余的数量变为1,右括号剩余数量还是2

  2. 第二位取左括号也行,取右括号也行。如果取左括号,那么此时右括号剩余数量为2,左括号剩余数量为0,长成了这个样子”((“;如果取右括号,左右剩余数量都是1,长成这个样子”()”

  3. 第三位,如果剩余左括号没了,就只能跟进右括号了,连续两个,最终变成”(())”;而如果现在是”()”的,那么要先左后右,最终变成”()()”.

发现,每一步都有两种选择:左或者右,当然不一定都可行,如果可行,那么往后继续,不可行,终止。

这是什么,二叉树。对于n = 2的情况,他的二叉树应该是这样的:
二叉树

这棵二叉树表达的东西其实跟我刚才说的是一样的,而我们所要的结果就是这棵二叉树遍历所有路径的结果。由此,不妨先来回忆一下之前“二叉树的所有路径”这道题目(详见:点击打开链接)可以仿照这个方法来解决我们当前的问题。所不同的是,这里我们没有一棵已经给出的二叉树,但是,此处,我们相当于知道了这棵二叉树的左右节点是否为空的条件(就是一开始的隐含信息,左括号要始终大于等于右括号,也就是说剩余的左括号要始终小于等于剩余的右括号),以及不为空时,节点的值(左括号为”(“,右括号为”)”)。

来源: <https://blog.csdn.net/guoziqing506/article/details/51198069 >

二叉搜索树的最近公共祖先

难度 简单

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]

1
2
3
4
5
6
7
     _______6______
/ \
___2__ ___8__
/ \ / \
0 _4 7 9
/ \
3 5

示例 1:

1
2
3
**输入:** root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
**输出:** 6
**解释:** 节点 2 和节点 8 的最近公共祖先是 6。

示例 2:

1
2
3
**输入:** root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
**输出:** 2
**解释:** 节点 `2` 和节点 `4` 的最近公共祖先是 `2`, 因为根据定义最近公共祖先节点可以为节点本身。

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉搜索树中。

Solution

解法1,自顶向下遍历

我们可以从根结点出发,判断当前结点的左右子树是否包含这两个结点。如果左子树包含两个结点,则它们的最低公共祖先结点也一定在左子树中。如果右子树包含两个结点,则它们的最低公共祖先结点也一定在右子树中。如果一个结点在左子树,而另一个结点在右子树中,则当前结点就是它们的最低公共祖先结点。根据该思路写出代码如下,注意这里已经假定p和q是二叉树中的结点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null)
return null;
if (containTargetTreeNode(root.left, p) && containTargetTreeNode(root.left, q)){
return lowestCommonAncestor(root.left, p, q);
}else if (containTargetTreeNode(root.right, p) && containTargetTreeNode(root.right, q)){
return lowestCommonAncestor(root.right, p, q);
}
return root;
}

private boolean containTargetTreeNode(TreeNode root, TreeNode targetNode) {
if (root == null)
return false;
if (root == targetNode)
return true;
return containTargetTreeNode(root.left, targetNode) || containTargetTreeNode(root.right, targetNode);
}

思路二:自顶向上

由于自顶向下的方法需要重复遍历结点,使用自底向上的方法可以避免这种情况。

自底向上遍历结点,一旦遇到结点等于p或者q,则将其向上传递给它的父结点。父结点会判断它的左右子树是否都包含其中一个结点,如果是,则父结点一定是这两个节点p和q的LCA,传递父结点到root。如果不是,我们向上传递其中的包含结点p或者q的子结点,或者NULL(如果子结点不包含任何一个)。该方法时间复杂度为O(N)。

Language: Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for a binary tree node.
* public class TreeNode {
*     int val;
*     TreeNode left;
*     TreeNode right;
*     TreeNode(int x) { val = x; }
* }
*/
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
       if (root == null)
           return null;
       if (p == root || q == root)
           return root;
       TreeNode l = lowestCommonAncestor(root.left, p, q);
       TreeNode r = lowestCommonAncestor(root.right, p, q);
       if (l != null && r != null)
           return root;
       return l == null ? r : l;
  }
}

思路三:利用搜索树的性质

因为树是二叉树,我们从根节点开始遍历,一定有顶点root的值大于p和q的值时,p、q的最近祖先一定在root的左子树下,当root的值小于p、q的值时,那么p、q的最近祖先一定在root的左子树下,不是上述两种情况的话,说明当前的root已经是最近的祖先了。

1
2
3
4
5
6
7
8
9
public TreeNode lowestCommonAncestor3(TreeNode root, TreeNode p, TreeNode q){
if (root == null)
return null;
if (root.val > p.val && root.val > q.val)
return lowestCommonAncestor3(root.left, p, q);
else if (root.val < p.val && root.val < q.val)
return lowestCommonAncestor3(root.right, p, q);
return root;
}

二叉树中的最大路径和

难度 困难

给定一个非空二叉树,返回其最大路径和。

本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。

示例 1:

1
2
3
4
5
6
7
**输入:** [1,2,3]

**1**
**/ \**
**2** **3**

**输出:** 6

示例 2:

1
2
3
4
5
6
7
8
9
**输入:** [-10,9,20,null,null,15,7]

  -10
   / \
  9  **20**
    **/  \**
   **15   7**

**输出:** 42

解题思路

需找一个最大的路径和,势必会以某一个节点为顶点,我们以某个顶点去寻找以其为顶点的最大的路径和,然后遍历二叉树上所有的节点,选出最大的路径和即可。

Solution

Language: 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
/**
* Definition for a binary tree node.
* public class TreeNode {
*     int val;
*     TreeNode left;
*     TreeNode right;
*     TreeNode(int x) { val = x; }
* }
*/
class Solution {
   private int res = 0;
   public int maxPathSum(TreeNode root) {
        res = Integer.MIN_VALUE;
       oneOther(root);
       return res;
  }
   private int oneOther(TreeNode root){
       if (root == null){
           return 0;
      }
       int left = oneOther(root.left);
       int right = oneOther(root.right);
       res = Math.max(Math.max(left, 0) + Math.max(right, 0) + root.val , res);
          return Math.max(Math.max(left,right), 0)+ root.val;
  }
  }

二叉搜索树中第K小的元素

难度 中等

给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 *k *个最小的元素。

说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。

示例 1:

1
2
3
4
5
6
7
**输入:** root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
  2
**输出:** 1

示例 2:

1
2
3
4
5
6
7
8
9
**输入:** root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
**输出:** 3

进阶:
如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?

解题思路

二叉搜索树 已经就是一个排好序的树了,我们只要按照中序遍历到第k个数,输出第k个数的值即可。

Solution

Language: 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
/**
* Definition for a binary tree node.
* public class TreeNode {
*     int val;
*     TreeNode left;
*     TreeNode right;
*     TreeNode(int x) { val = x; }
* }
*/
class Solution {
   int count = 0;
   public int kthSmallest(TreeNode root, int k) {
        if (root == null || k < 0){
           return 0;
      }
       count = k;
       
       return inOrder(root);
  }
   
   private int inOrder(TreeNode root){
       if(root == null)
           return -1;
       int val = inOrder(root.left);
       if(count == 0)
           return val;
       if(--count == 0)
           return root.val;
       return inOrder(root.right);
  }
   
}

或者使用方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
private int index=0;
public int kthSmallest(TreeNode root, int k) {
if(root==null)
return Integer.MAX_VALUE;
int ret=kthSmallest(root.left,k);
if(ret!=Integer.MAX_VALUE)
return ret;
index++;
if(index==k)
{
return root.val;
}
return kthSmallest(root.right,k);
}
}

Class文件结构–常量池(一)


转载自https://www.jianshu.com/p/d8492e748c57

  • 字节码查看工具:WinHex

前言

  • Java虚拟机实现语言无关性的基石就是Class文件
    Java虚拟机提供的语言无关性
  • 这篇文章讲Class格式文件的的魔数、版本号和常量池。主要内容是常量池。

Class类文件的结构

全局规范

  • 1.任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。本章中,只是通俗地将任意一个有效的类或接口所应当满足的格式称为“Class文件格式”,实际上它并不一定以磁盘文件的形式存在。“Class文件”应当是一串二进制的字节流,无论以何种形式存在。

  • 2.Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前(Big-Endian)的方式分割成若干个8位字节进行存储。无符号数据类型最大占8个字节。

  • 3.Class文件中存储数据的类型:无符号数和表。

  • 无符号数(基本数据类型):以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

  • 表(复合数据类型):是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地“_info”结尾。表用于描述有层次关系的复合结构的数据。表是一个统称,就好比把ArrayList、LinkedList、Set都是称为集合(Collection),但是每个集合的内部结构都是不同的,Class中有很多不同的表。如下图中cp_info类型,是表类型,但是它是一个固定结构的类型吗?不是,它好比Collection集合下的List集合,只是一类集合的统称,实际上cp_info表是14种具体表类型的统称,constant_pool_count-1指出了有多少个cp_info表,那到底是哪些具体的表,就需要具体看了。

  • 4.整个Class文件本质上就是一张表,下表就是Class文件格式。Class中所有内容都在这些类型中定义了。

    • 注:表中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

      Class文件格式

      class文件结构

  • 5.无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

  • 如上表的描述常量池数据使用了一个constant_pool_count、多个constant_pool,其中constant_pool是表类型并且数量为constant_pool_count值减去1,把一个constant_pool_count和多个constant_pool数据项称为常量池集合

  • 从Class文件格式中可以看出有:常量池集合、接口索引集合、字段表集合、方法表集合、属性表集合。

  • 6.具体的Class文件案例,以下讲解会通过这个TestClass类的TestClass.class文件来分析。

    1
    2
    3
    4
    5
    6
    7
    8
    package com.zlcook.clazz;

    public class TestClass{
    private int m;
    public int inc(){
    return m+1;
    }
    }

1. 魔数与Class文件的版本

  • 由上表得Class文件的前三个数据类型存储了魔数(magic)、次版本号(minor_version)、主版本号(major_version)的值,数据类型分别为u4、u2、u2。共占8个字节。
  • 魔数:0xCAFEBABE (16进制),值固定,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
  • Class文件版本号:次版本号组成u2+主版本号u2。共占4个字节。
  • 高版本的JDK能向下兼容以前的版本的Class文件,但不能运行高版本的Class文件。
  • JDK1.1的版本号为45.0-45.65535(10进制),之后每个大版本发布主版本号加1,如:JDK1.2:46.0~46.65535。
  • 例如:Class文件中紧接着魔数的4个字节的16进制为: ox00000034,那么它代表的十进制版本号为:次版本号为ox0000=0,主版本号为:ox0034 = 52。所以ox00000034的版本号为52.0,对应的JDK版本为JDK1.8

TestClass.class文件的前8个字节

2. 常量池

  • 先了解常量池中需要存放哪些内容,再讨论用什么类来存放这些内容。

2.1 常量池中存放的内容

  • Class文件中包含常量池,那么我就需要知道常量池会包含哪些内容,接下来才是关心class格式文件用什么类型来存放这些内容。

  • 字面量(Literal)

  • 字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。

  • 符号引用(Symbolic References)

  • 符号引用则属于编译原理方面的概念,包括了下面三类常量:
    类和接口的全限定名(Fully Qualified Name)
    字段的名称和描述符(Descriptor)
    方法的名称和描述符

  • 其它:常量池中主要内容是上面2项,说明还有其它内容,这部分内容,在下面我们看到用来描述常量池内容的14种常量项的介绍时就发现标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。

  • 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。

2.2 常量池中为什么要包含这些内容

  • Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在虚拟机类加载过程时再进行详细讲解。

2.3 Class文件中如何描述常量池中内容

  • 知道Class文件的常量池包含的内容后,我们下面就来看看class格式文件使用了哪些类型数据来存放常量池的内容。

  • 由Class文件格式可得紧接着主版本号的是常量池入口。

类型 名称 数量
u2(无符号数) constant_pool_count 1
cp_info(表) constant_pool constant_pool_count-1
  • 占用的字节数:2+(constant_pool_count-1)个具体表所占字节。

  • 由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。

  • 先给看一下TestClass.class文件全局的内容,下面就来分析其中常量池中的内容,其它内容后面的文章在分析。从图片也可以看出常量池内容占据了class文件的很大一部分,当然TestClass类中代码比较少就更显得常量池内容的多了。

TestClass.class文件的16进制内容

2.3.1 constant_pool_count

  • 常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项。

  • 设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。

  • TestClass.class文件中constant_pool_count的十进制值为19,表示常量池中有18项常量,索引范围1-18。

TestClass.class文件中constant_pool_count的十进制值为19

2.3.2 constant_pool

  • constant_pool_count表明了后面有多少个常量项。
14种常量项结构
  • 常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。一个常量池中的每个常量项都逃不脱这14种结构。根据下图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容(主要是字面量、符号引用)的。比如:CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只是整型字面量信息。而标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。

常量池中的14种项目类型

常量池中的14种常量项的结构总表

常量池中的14种常量项的结构总表(续)

  • 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。

  • 这14种常量项结构还有一个特点是,其中13表占用得字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,无法确定大小不固定,编译后,通过utf-8编码,就可以知道其长度。

占用字节
CONSTANT_Class_info 3
CONSTANT_Integer_info 5
CONSTANT_Fieldref_info 5
CONSTANT_Methodref_info 5
CONSTANT_Utf8_info 不固定,取决于length大小

2.4 查找testClass.class文件的第一个常量项内容

  • 由上面constant_pool_count得到值为19,因为从1开始计数,所以说明后面有18个常量项,由于每个常量项的表结构都不同但是第一位相同,所以读到第一位就可以确定表结构了。下面我们就来查看第一个常量项包含得内容,至于其它17个常量项内容类似,最后还会介绍java提供得一个工具命令javap来帮我们分析class文件字节码内容。

第一个表的tag为10

  • 由上图可知常量池中第一项常量标志的16进制值是0x0A=10,查表发现这个常量属于CONSTANT_Methodref_info类型,此类型表示类中方法的符号引用。查看该类型的结构如下:

CONSTANT\_Methodref\_info类型结构

  • CONSTANT_Methodref_info型常量的第二个数据项为index,类型是u2,index存储的是一个索引值,从class文件中查得该值为oX0004=4,即它指向常量池中第4个常量;第三个数据项也是索引其值为0X000F=15,指向常量池种第15个常量。

Paste_Image.png

  • 到此为止,第一个常量项是CONSTANT_Methodref_info型常量项,该类型常量项用来表示类中方法的符号引用,其内容为tag=10,index1=4,index2=15,因为其表示的是类中方法的符号引用,所以index中存放的不是一个具体得内容,而是一个索引位置,所以说其具体内容存放在另一个常量项中。下面我们就来看看其索引指向的常量项(即第4个常量项)的内容到底是什么?
  • 找第4个常量项之前需要知道第4个常量项的开始位置,所以需要知道前3个常量项所占字节数。那好就看第2个常量项,由于第一个常量项共占了5个字节,则紧接着的字节就为第二个常量项的tag,如下图可得其值为0X09=9,说明第2个常量项得项目类型为CONSTANT_Fieldref_info。查表得其该类型得字节长度固定占5个字节。

第二个常量项

  • 依次类推查的第3,4个常量项为CONSTANT_Class_info型。如下图:

前4个常量项

  • 下面就看第四个常量项CONSTANT_Class_info的内容0X070012。 CONSTANT_Class_info存放的是指向类或接口的符号引用。

CONSTANT\_Class\_info型常量项

根据CONSTANT_Class_info项常量项的结构可知其index数据项又是一个索引项,指向全限定名常量项索引,index数据项的值为0X12=18,表示指向第18个常量项,根据constant_pool_count的值为19可得,常量池中一共有18个常量项,巧了正好在最后一个,但是要知道18个常量项必须知道前17个常量项所占字节,这里就不一一找了,最后找到第18个常量项CONSTANT_Utf8_info在class文件中包含的内容如下:

第18个常量项

  • CONSTANT\_Utf8\_info型表的结构
  • 根据tag等于1得第18项是CONSTANT_Utf8_info型,该类型存储UTF-8编码的字符串,在TestClass.class文件种该常量项种个数据项的内容如下:

    • length(u2):表示UTF-8编码的字符串占用的字节数,值为0x0010=16.
    • bytes(u1):表示长度为length的UTF-8编码的字符串.
    • 因为length=16,所以 length后面紧跟的长度为16个字节的连续数据是一个使用UTF-8缩略编码表示的字符串。后面紧跟的第一个字节为0x6A=106,那该编码代表的字符为j,我们发现106其实就是字符j对应的ASCII码。后面16个字节代表的字符就是: java/lang/Object

到此为止,我们得到了第一个常量项CONSTANT_Methodref_info的第二个数据项index指向的内容为CONSTANT_Class_info常量项,CONSTANT_Class_info常量的第二个数据项index指向CONSTANT_Utf8_info常量项,CONSTANT_Utf8_info常量项的内容为 java/lang/Object 。
当然CONSTANT_Methodref_info常量项还有第三个数据项index,其存放的也是一个其他常量的索引。

  • 根据上面的找法我们就可以找出常量池中包含的内容:字面量和符号引用。

2.5 采用javap命令分析class文件

  • 根据上面的找法我们就可以找出常量池中包含的内容:字面量和符号引用。java考虑到这种找法太麻烦了,所以提供了一个命令javap来帮助我们分析class文件的内容。

  • javap分析class文件用法:javap -verbose class文件名

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
$ javap -verbose TestClass.class
Classfile /E:/studytry/com/zlcook/clazz/TestClass.class
Last modified 2017-4-7; size 292 bytes
MD5 checksum 486567c6d4d7432fc359230fed9c92c7
Compiled from "TestClass.java"
public class com.zlcook.clazz.TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/zlcook/clazz/TestClass.m:I
#3 = Class #17 // com/zlcook/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/zlcook/clazz/TestClass
#18 = Utf8 java/lang/Object
{
public com.zlcook.clazz.TestClass();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0

public int inc();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
  • 上面通过javap命令得到的结果,该结果显示的很友好,由过上面的理论我们可以很清楚的看到常量池一共18项:其中第一项如下:

    1
    #1 = Methodref          #4.#15       //  java/lang/Object."<init>":()V
  • 和我们通过手动方式查看第一个常量项CONSTANT_Methodref_info对比一下就知道javap显示的内容是多么友好了。

第一个常量项 第几个 tag index index 最终代表的内容
class中16进制值 0X0A 0X004 0X000F
转换成10进制值 10 4 15 查完4和15才知道
javap分析显示的友好值 #1 Methodref #4 #15 java/lang/Object.”<init>”:()V

2.6 class文件中包含的内容

  • 下面我们来看一下class文件中常量池的内容和java源码中的内容。

  • TestClass.java代码内容

1
2
3
4
5
6
7
8
package com.zlcook.clazz;

public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
  • TestClass.class中常量池内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Constant pool:
    #1 = Methodref #4.#15 // java/lang/Object."<init>":()V
    #2 = Fieldref #3.#16 // com/zlcook/clazz/TestClass.m:I
    #3 = Class #17 // com/zlcook/clazz/TestClass
    #4 = Class #18 // java/lang/Object
    #5 = Utf8 m
    #6 = Utf8 I
    #7 = Utf8 <init>
    #8 = Utf8 ()V
    #9 = Utf8 Code
    #10 = Utf8 LineNumberTable
    #11 = Utf8 inc
    #12 = Utf8 ()I
    #13 = Utf8 SourceFile
    #14 = Utf8 TestClass.java
    #15 = NameAndType #7:#8 // "<init>":()V
    #16 = NameAndType #5:#6 // m:I
    #17 = Utf8 com/zlcook/clazz/TestClass
    #18 = Utf8 java/lang/Object
  • 再复习一下常量池中主要存放字面量:如文本字符串、声明为final的常量值等。和符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

  • 所以出现com/zlcook/clazz/TestClass、java/lang/Object、m、inc都是应该的,那么I、V、、LineNumberTable都是什么?那肯定是字段描述符或者是方法描述符了。这部分是编译时自动生成的,它们会被class文件中其它部分(字段表field_info、方法表method_info、属性表attribute_info)引用到,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。

3. 哪些字面量会进入常量池中

  • 我们知道class文件存放字面量:如文本字符串、声明为final的常量值等。这里的“等”就挺烦人。
  • 下面我们来看看哪些字面量会进入常量池。(jdk1.8.0环境)

8种基本类型:

测试案例:

  • final类型 FinalTest.java代码
1
2
3
4
5
6
7
8
9
10
11
public class FinalTest{

private final int int_num =12;
private final char char_num = 'a';
private final short short_num =30;
private final float float_num = 45.3f;
private final double double_num =39.8;
private final byte byte_num =121;
private final long long_num = 2323L;
private final boolean boolean_flage = true;
}
  • 非final类型 test.java代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class test{

    private int int_num =12;
    private char char_num = 'a';
    private short short_num =30;
    private float float_num = 45.3f;
    private double double_num =39.8;
    private byte byte_num =121;
    private long long_num = 2323L;
    private long long_delay_num ;
    private boolean boolean_flage = true;

    public void init(){
    this.long_delay_num = 5555L;
    }
    }

上面代码测试结果:

  • final类型的8种基本类型的值会进入常量池。
  • 非final类型的8种基本类型的值double、float、long的值会进入常量池,包括long_delay_num的值。

String类型

1
2
3
4
5
6
7
8
9
10
11
12
13
*   StringTest.java代码:

public class StringTest{

private String str1 = "zl"+"cook";
private String str2 = str1+"hello";
private String str3 = new String("zlcook here?");
private String str4 = "everybody "+ new String("here?");

private final String fin1 = "boy";
private final String fin2 = fin1+ "is boy";
private final String fin3 = str1+ "is boy";
}
  • StringTest.class的常量池种包含内容:

常量池中包含的字符串类型字面量

所有测试数据github: 测试数据

结束

  • 这一节主要讲了Class文件魔数、版本号和常量池,比较详细介绍了常量池包含的内容以及用到的14种常量项结构。记住本节讲的常量池是class文件中的常量池,要记住还有运行时常量池,每个class文件中的常量池内容在类加载侯会进入方法区的运行时常量池中存放。当然运行时常量池的内容不仅包含这些还包含运行期加入的常量,常见的就是String类的intern()方法。

修改ubuntu下的dns

适用环境:oracle vm 虚拟机下的ununtu-14.04.5版本。

验证有效的方法:https://blog.csdn.net/u014453443/article/details/80878061

基本步骤:

  1. 查看当前dns配置:

    1
    cat /etc/resolv.conf
  2. 添加dns配置,在interfaces文件下添加需要的dns配置,

    1
    sudo vi /etc/network/interfaces

    参考的dns配置地址:

    1
    2
    dns-nameservers 8.8.8.8 
    dns-nameservers 8.8.4.4

    参考配置如下:
    image.png

  3. 重启,后验证dns

    1
    cat /etc/resolv.conf

image.png

dns修改成功!

可能有效的方法 https://blog.csdn.net/qq_27818541/article/details/75730125

验证无效,但是其他版本可能有效的方法 https://blog.csdn.net/zd147896325/article/details/81078414

Android 插件化原理解析——插件加载机制

本文转载自http://weishu.me/2016/04/05/understand-plugin-framework-classloader/

上文 Activity生命周期管理 中我们地完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的任务;通过Hook AMS和拦截ActivityThread中H类对于组件调度我们成功地绕过了AndroidMAnifest.xml的限制。

但是我们启动的『没有在AndroidManifet.xml中显式声明』的Activity和宿主程序存在于同一个Apk中;通常情况下,插件均以独立的文件存在甚至通过网络获取,这时候插件中的Activity能否成功启动呢?

要启动Activity组件肯定先要创建对应的Activity类的对象,从上文 Activity生命周期管理 知道,创建Activity类对象的过程如下:

1
2
3
4
5
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();  
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

也就是说,系统通过ClassLoader加载了需要的Activity类并通过反射调用构造函数创建出了Activity对象。如果Activity组件存在于独立于宿主程序的文件之中,系统的ClassLoader怎么知道去哪里加载呢?因此,如果不做额外的处理,插件中的Activity对象甚至都没有办法创建出来,谈何启动?

因此,要使存在于独立文件或者网络中的插件被成功启动,首先就需要解决这个插件类加载的问题。
下文将围绕此问题展开,完成『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

阅读本文之前,可以先clone一份 understand-plugin-framework,参考此项目的classloader-hook 模块。另外,插件框架原理解析系列文章见索引

ClassLoader机制

或许有的童鞋还不太了解Java的ClassLoader机制,我这里简要介绍一下。

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化的,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
与那些在编译时进行链连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以同代拓展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。例如,如果编写一个面相接口的应用程序,可以等到运行时在制定实际的实现类;用户可以通过Java与定义的和自定义的类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为代码的一部分,这种组装应用程序的方式目前已经广泛应用于Java程序之中。从最基础的Applet,JSP到复杂的OSGi技术,都使用了Java语言运行期类加载的特性。

Java的类加载是一个相对复杂的过程;它包括加载、验证、准备、解析和初始化五个阶段;对于开发者来说,可控性最强的是加载阶段;加载阶段主要完成三件事:

  1. 根据一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为JVM方法区中的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

『通过一个类的全限定名获取描述此类的二进制字节流』这个过程被抽象出来,就是Java的类加载器模块,也即JDK中ClassLoader API。

Android Framework提供了DexClassLoader这个类,简化了『通过一个类的全限定名获取描述次类的二进制字节流』这个过程;我们只需要告诉DexClassLoader一个dex文件或者apk文件的路径就能完成类的加载。因此本文的内容用一句话就可以概括:

将插件的dex或者apk文件告诉『合适的』DexClassLoader,借助它完成插件类的加载

关于CLassLoader机制更多的内容,请参阅『深入理解Java虚拟机』这本书。

思路分析

Android系统使用了ClassLoader机制来进行Activity等组件的加载;apk被安装之后,APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name/base-1.apk )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类;但是系统并不知道存在于插件中的Activity组件的信息(插件可以是任意位置,甚至是网络,系统无法提前预知),因此正常情况下系统无法加载我们插件中的类;因此也没有办法创建Activity的对象,更不用谈启动组件了。

解决这个问题有两个思路,要么全盘接管这个类加载的过程;要么告知系统我们使用的插件存在于哪里,让系统帮忙加载;这两种方式或多或少都需要干预这个类加载的过程。老规矩,知己知彼,百战不殆。我们首先分析一下,系统是如果完成这个类加载过程的。

我们再次搬出Activity的创建过程的代码:

1
2
3
4
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();  
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

这里可以很明显地看到,系统通过待启动的Activity的类名className,然后使用ClassLoader对象cl把这个类加载进虚拟机,最后使用反射创建了这个Activity类的实例对象。要想干预这个ClassLoader(告知它我们的路径或者替换他),我们首先得看看这玩意到底是个什么来头。(从哪里创建的)

cl这个ClasssLoader对象通过r.packageInfo对象的getClassLoader()方法得到,r.packageInfo是一个LoadedApk类的对象;那么,LoadedApk到底是个什么东西??

我们查阅LoadedApk类的文档,只有一句话,不过说的很明白:

Local state maintained about a currently loaded .apk.

LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。

OK, 我们知道这个LoadedApk是何方神圣了;接下来我们要搞清楚的是:这个 r.packageInfo 到底是从哪里获取的?

我们顺着 performLaunchActivity上溯,辗转handleLaunchActivity回到了 H 类的LAUNCH_ACTIVITY消息,找到了r.packageInfo的来源:

1
2
3
4
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;  
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);

getPackageInfoNoCheck方法很简单,直接调用了getPackageInfo方法:

1
2
3
4
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,  
CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
}

在这个getPackageInfo方法里面我们发现了端倪:

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
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,  
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
// 获取userid信息
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
// 尝试获取缓存信息
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}

LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
// 缓存没有命中,直接new
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG\_HAS\_CODE) != 0, registerPackage);

// 省略。。更新缓存
return packageInfo;
}
}

这个方法很重要,我们必须弄清楚每一步;

首先,它判断了调用方和或许App信息的一方是不是同一个userId;如果是同一个user,那么可以共享缓存数据(要么缓存的代码数据,要么缓存的资源数据)

接下来尝试获取缓存数据;如果没有命中缓存数据,才通过LoadedApk的构造函数创建了LoadedApk对象;创建成功之后,如果是同一个uid还放入了缓存。

提到缓存数据,看过Hook机制之Binder Hook的童鞋可能就知道了,我们之前成功借助ServiceManager的本地代理使用缓存的机制Hook了各种Binder;因此这里完全可以如法炮制——我们拿到这一份缓存数据,修改里面的ClassLoader;自己控制类加载的过程,这样加载插件中的Activity类的问题就解决了。这就引出了我们加载插件类的第一种方案:

激进方案:Hook掉ClassLoader,自己操刀

从上述分析中我们得知,在获取LoadedApk的过程中使用了一份缓存数据;这个缓存数据是一个Map,从包名到LoadedApk的一个映射。正常情况下,我们的插件肯定不会存在于这个对象里面;但是如果我们手动把我们插件的信息添加到里面呢?系统在查找缓存的过程中,会直接命中缓存!进而使用我们添加进去的LoadedApk的ClassLoader来加载这个特定的Activity类!这样我们就能接管我们自己插件类的加载过程了!

这个缓存对象mPackages存在于ActivityThread类中;老方法,我们首先获取这个对象:

1
2
3
4
5
6
7
8
9
10
// 先获取到当前的ActivityThread对象  
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
Map mPackages = (Map) mPackagesField.get(currentActivityThread);

拿到这个Map之后接下来怎么办呢?我们需要填充这个map,把插件的信息塞进这个map里面,以便系统在查找的时候能命中缓存。但是这个填充这个Map我们出了需要包名之外,还需要一个LoadedApk对象;如何创建一个LoadedApk对象呢?

我们当然可以直接反射调用它的构造函数直接创建出需要的对象,但是万一哪里有疏漏,构造参数填错了怎么办?又或者Android的不同版本使用了不同的参数,导致我们创建出来的对象与系统创建出的对象不一致,无法work怎么办?

因此我们需要使用与系统完全相同的方式创建LoadedApk对象;从上文分析得知,系统创建LoadedApk对象是通过getPackageInfo来完成的,因此我们可以调用这个函数来创建LoadedApk对象;但是这个函数是private的,我们无法使用。

有的童鞋可能会有疑问了,private不是也能反射到吗?我们确实能够调用这个函数,但是private表明这个函数是内部实现,或许那一天Google高兴,把这个函数改个名字我们就直接GG了;但是public函数不同,public被导出的函数你无法保证是否有别人调用它,因此大部分情况下不会修改;我们最好调用public函数来保证尽可能少的遇到兼容性问题。(当然,如果实在木有路可以考虑调用私有方法,自己处理兼容性问题,这个我们以后也会遇到)

间接调用getPackageInfo这个私有函数的public函数有同名的getPackageInfo系列和getPackageInfoNoCheck;简单查看源代码发现,getPackageInfo除了获取包的信息,还检查了包的一些组件;为了绕过这些验证,我们选择使用getPackageInfoNoCheck获取LoadedApk信息。

构建插件LoadedApk对象

我们这一步的目的很明确,通过getPackageInfoNoCheck函数创建出我们需要的LoadedApk对象,以供接下来使用。

这个函数的签名如下:

1
2
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,  
CompatibilityInfo compatInfo) {

因此,为了调用这个函数,我们需要构造两个参数。其一是ApplicationInfo,其二是CompatibilityInfo;第二个参数顾名思义,代表这个App的兼容性信息,比如targetSDK版本等等,这里我们只需要提取出app的信息,因此直接使用默认的兼容性即可;在CompatibilityInfo类里面有一个公有字段DEFAULT_COMPATIBILITY_INFO代表默认兼容性信息;因此,我们的首要目标是获取这个ApplicationInfo信息。

构建插件ApplicationInfo信息

我们首先看看ApplicationInfo代表什么,这个类的文档说的很清楚:

Information you can retrieve about a particular application. This corresponds to information collected from the AndroidManifest.xml’s tag.

也就是说,这个类就是AndroidManifest.xml里面的 这个标签下面的信息;这个AndroidManifest.xml无疑是一个标准的xml文件,因此我们完全可以自己使用parse来解析这个信息。

那么,系统是如何获取这个信息的呢?其实Framework就有一个这样的parser,也即PackageParser;理论上,我们也可以借用系统的parser来解析AndroidMAnifest.xml从而得到ApplicationInfo的信息。但遗憾的是,这个类的兼容性很差;Google几乎在每一个Android版本都对这个类动刀子,如果坚持使用系统的解析方式,必须写一系列兼容行代码!!DroidPlugin就选择了这种方式,相关类如下:

DroidPlugin的PackageParser

DroidPlugin的PackageParser

看到这里我就问你怕不怕!!!这也是我们之前提到的私有或者隐藏的API可以使用,但必须处理好兼容性问题;如果Android 7.0发布,这里估计得添加一个新的类PackageParseApi24。

我这里使用API 23作为演示,版本不同的可能无法运行请自行查阅 DroidPlugin 不同版本如何处理。

OK回到正题,我们决定使用PackageParser类来提取ApplicationInfo信息。下图是API 23上,PackageParser的部分类结构图:

看起来有我们需要的方法 generateApplication;确实如此,依靠这个方法我们可以成功地拿到ApplicationInfo。
由于PackageParser是@hide的,因此我们需要通过反射进行调用。我们根据这个generateApplicationInfo方法的签名:

1
2
public static ApplicationInfo generateApplicationInfo(Package p, int flags,  
PackageUserState state)

可以写出调用generateApplicationInfo的反射代码:

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");  
// 首先拿到我们得终极目标: generateApplicationInfo方法
// API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// public static ApplicationInfo generateApplicationInfo(Package p, int flags,
// PackageUserState state) {
// 其他Android版本不保证也是如此.
Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",
packageParser$PackageClass,
int.class,
packageUserStateClass);

要成功调用这个方法,还需要三个参数;因此接下来我们需要一步一步构建调用此函数的参数信息。

构建PackageParser.Package

generateApplicationInfo方法需要的第一个参数是PackageParser.Package;从名字上看这个类代表某个apk包的信息,我们看看文档怎么解释:

Representation of a full package parsed from APK files on disk. A package consists of a single base APK, and zero or more split APKs.

果然,这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示;因此,要获取这个类,肯定需要解析整个apk文件。PackageParser中解析apk的核心方法是parsePackage,这个方法返回的就是一个Package类型的实例,因此我们调用这个方法即可;使用反射代码如下:

1
2
3
4
5
6
7
8
9
// 首先, 我们得创建出一个Package对象出来供这个方法调用  
// 而这个需要得对象可以通过 android.content.pm.PackageParser#parsePackage 这个方法返回得 Package对象得字段获取得到
// 创建出一个PackageParser对象供使用
Object packageParser = packageParserClass.newInstance();
// 调用 PackageParser.parsePackage 解析apk的信息
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

// 实际上是一个 android.content.pm.PackageParser.Package 对象
Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);

这样,我们就得到了generateApplicationInfo的第一个参数;第二个参数是解析包使用的flag,我们直接选择解析全部信息,也就是0;

构建PackageUserState

第三个参数是PackageUserState,代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可;

至此,generateApplicaionInfo的参数我们已经全部构造完成,直接调用此方法即可得到我们需要的applicationInfo对象;在返回之前我们需要做一点小小的修改:使用系统系统的这个方法解析得到的ApplicationInfo对象中并没有apk文件本身的信息,所以我们把解析的apk文件的路径设置一下(ClassLoader依赖dex文件以及apk的路径):

1
2
3
4
5
6
7
8
9
// 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可  
Object defaultPackageUserState = packageUserStateClass.newInstance();

// 万事具备!!!!!!!!!!!!!!
ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
packageObj, 0, defaultPackageUserState);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;

替换ClassLoader

获取LoadedApk信息

方才为了获取ApplicationInfo我们费了好大一番精力;回顾一下我们的初衷:

我们最终的目的是调用getPackageInfoNoCheck得到LoadedApk的信息,并替换其中的mClassLoader然后把把添加到ActivityThread的mPackages缓存中;从而达到我们使用自己的ClassLoader加载插件中的类的目的。

现在我们已经拿到了getPackageInfoNoCheck这个方法中至关重要的第一个参数applicationInfo;上文提到第二个参数CompatibilityInfo代表设备兼容性信息,直接使用默认的值即可;因此,两个参数都已经构造出来,我们可以调用getPackageInfoNoCheck获取LoadedApk:

1
2
3
4
5
6
7
8
9
10
11
// android.content.res.CompatibilityInfo  
Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);

Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT\_COMPATIBILITY\_INFO");
defaultCompatibilityInfoField.setAccessible(true);

Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);

Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

我们成功地构造出了LoadedAPK, 接下来我们需要替换其中的ClassLoader,然后把它添加进ActivityThread的mPackages中:

1
2
3
4
5
6
7
8
9
10
11
String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();  
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApk, classLoader);
// 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC; 那么就前功尽弃了.
sLoadedApk.put(applicationInfo.packageName, loadedApk);

WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName, weakReference);

我们的这个CustomClassLoader非常简单,直接继承了DexClassLoader,什么都没有做;当然这里可以直接使用DexClassLoader,这里重新创建一个类是为了更有区分度;以后也可以通过修改这个类实现对于类加载的控制:

1
2
3
4
5
6
public class CustomClassLoader extends DexClassLoader {  

public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
}

到这里,我们已经成功地把把插件的信息放入ActivityThread中,这样我们插件中的类能够成功地被加载;因此插件中的Activity实例能被成功第创建;由于整个流程较为复杂,我们简单梳理一下:

  1. 在ActivityThread接收到IApplication的 scheduleLaunchActivity远程调用之后,将消息转发给H
  2. H类在handleMessage的时候,调用了getPackageInfoNoCheck方法来获取待启动的组件信息。在这个方法中会优先查找mPackages中的缓存信息,而我们已经手动把插件信息添加进去;因此能够成功命中缓存,获取到独立存在的插件信息。
  3. H类然后调用handleLaunchActivity最终转发到performLaunchActivity方法;这个方法使用从getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader来加载Activity类,进而使用反射创建Activity实例;接着创建Application,Context等完成Activity组件的启动。

看起来好像已经天衣无缝万事大吉了;但是运行一下会出现一个异常,如下:

1
2
3
04-05 02:49:53.742  11759-11759/com.weishu.upf.hook_classloader E/AndroidRuntime﹕ FATAL EXCEPTION: main  
Process: com.weishu.upf.hook_classloader, PID: 11759
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.weishu.upf.ams\_pms\_hook.app/com.weishu.upf.ams\_pms\_hook.app.MainActivity}: java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException: Unable to get package info for com.weishu.upf.ams\_pms\_hook.app; is package not installed?

错误提示说是无法实例化 Application,而Application的创建也是在performLaunchActivity中进行的,这里有些蹊跷,我们仔细查看一下。

绕过系统检查

通过ActivityThread的performLaunchActivity方法可以得知,Application通过LoadedApk的makeApplication方法创建,我们查看这个方法,在源码中发现了上文异常抛出的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {  
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " \+ appClass
\+ ": " \+ e.toString(), e);
}
}

木有办法,我们只有一行一行地查看到底是哪里抛出这个异常的了;所幸代码不多。(所以说,缩小异常范围是一件多么重要的事情!!!)

第一句 getClassLoader() 没什么可疑的,虽然方法很长,但是它木有抛出任何异常(当然,它调用的代码可能抛出异常,万一找不到只能进一步深搜了;所以我觉得这里应该使用受检异常)。

然后我们看第二句,如果包名不是android开头,那么调用了一个叫做initializeJavaContextClassLoader的方法;我们查阅这个方法:

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
private void initializeJavaContextClassLoader() {  
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, 0, UserHandle.myUserId());
} catch (RemoteException e) {
throw new IllegalStateException("Unable to get package info for "
\+ mPackageName + "; is system dying?", e);
}
if (pi == null) {
throw new IllegalStateException("Unable to get package info for "
\+ mPackageName + "; is package not installed?");
}

boolean sharedUserIdSet = (pi.sharedUserId != null);
boolean processNameNotDefault =
(pi.applicationInfo != null &&
!mPackageName.equals(pi.applicationInfo.processName));
boolean sharable = (sharedUserIdSet || processNameNotDefault);
ClassLoader contextClassLoader =
(sharable)
? new WarningContextClassLoader()
: mClassLoader;
Thread.currentThread().setContextClassLoader(contextClassLoader);
}

这里,我们找出了这个异常的来源:原来这里调用了getPackageInfo方法获取包的信息;而我们的插件并没有安装在系统上,因此系统肯定认为插件没有安装,这个方法肯定返回null。所以,我们还要欺骗一下PMS,让系统觉得插件已经安装在系统上了;至于如何欺骗 PMS,Hook机制之AMS&PMS 有详细解释,这里直接给出代码,不赘述了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void hookPackageManager() throws Exception {  

// 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
// 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.

Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 获取ActivityThread里面原始的 sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);

// 准备好代理对象, 用来替换原始的对象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
new Class<?>\[\] { iPackageManagerInterface },
new IPackageManagerHookHandler(sPackageManager));

// 1\. 替换掉ActivityThread里面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);
}

OK到这里,我们已经能够成功地加载简单的独立的存在于外部文件系统中的apk了。至此 关于 DroidPlugin 对于Activity生命周期的管理已经完全讲解完毕了;这是一种极其复杂的Activity管理方案,我们仅仅写一个用来理解的demo就Hook了相当多的东西,在Framework层来回牵扯;这其中的来龙去脉要完全把握清楚还请读者亲自翻阅源码。另外,我在此 对DroidPlugin 作者献上我的膝盖~这其中的玄妙让人叹为观止!

上文给出的方案中,我们全盘接管了插件中类的加载过程,这是一种相对暴力的解决方案;能不能更温柔一点呢?通俗来说,我们可以选择改革,而不是革命——告诉系统ClassLoader一些必要信息,让它帮忙完成插件类的加载。

保守方案:委托系统,让系统帮忙加载

我们再次搬出ActivityThread中加载Activity类的代码:

1
2
3
4
5
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();  
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

我们知道 这个r.packageInfo中的r是通过getPackageInfoNoCheck获取到的;在『激进方案』中我们把插件apk手动添加进缓存,采用自己加载办法解决;如果我们不干预这个过程,导致无法命中mPackages中的缓存,会发生什么?

查阅 getPackageInfo方法如下:

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
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,  
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}

LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG\_HAS\_CODE) != 0, registerPackage);

// 略
}
}

可以看到,没有命中缓存的情况下,系统直接new了一个LoadedApk;注意这个构造函数的第二个参数aInfo,这是一个ApplicationInfo类型的对象。在『激进方案』中我们为了获取独立插件的ApplicationInfo花了不少心思;那么如果不做任何处理这里传入的这个aInfo参数是什么?

追本溯源不难发现,这个aInfo是从我们的替身StubActivity中获取的!而StubActivity存在于宿主程序中,所以,这个aInfo对象代表的实际上就是宿主程序的Application信息!

我们知道,接下来会使用new出来的这个LoadedApk的getClassLoader()方法获取到ClassLoader来对插件的类进行加载;而获取到的这个ClassLoader是宿主程序使用的ClassLoader,因此现在还无法加载插件的类;那么,我们能不能让宿主的ClasLoader获得加载插件类的能力呢?;如果我们告诉宿主使用的ClassLoader插件使用的类在哪里,就能帮助他完成加载!

宿主的ClassLoader在哪里,是唯一的吗?

上面说到,我们可以通过告诉宿主程序的ClassLoader插件使用的类,让宿主的ClasLoader完成对于插件类的加载;那么问题来了,我们如何获取到宿主的ClassLoader?宿主程序使用的ClasLoader默认情况下是全局唯一的吗?

答案是肯定的。

因为在FrameWork中宿主程序也是使用LoadedApk表示的,如同Activity启动是加载Activity类一样,宿主中的类也都是通过LoadedApk的getClassLoader()方法得到的ClassLoader加载的;由类加载机制的『双亲委派』特性,只要有一个应用程序类由某一个ClassLoader加载,那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。

表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader

LoadedApk的ClassLoader到底是什么?

现在我们确保了『使用宿主ClassLoader帮助加载插件类』可行性;那么我们应该如何完成这个过程呢?

知己知彼,百战不殆。

不论是宿主程序还是插件程序都是通过LoadedApk的getClassLoader()方法返回的ClassLoader进行类加载的,返回的这个ClassLoader到底是个什么东西??这个方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public ClassLoader getClassLoader() {  
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}

if (mIncludeCode && !mPackageName.equals("android")) {
// 略...
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);

StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}

可以看到,非android开头的包和android开头的包分别使用了两种不同的ClassLoader,我们只关心第一种;因此继续跟踪ApplicationLoaders类:

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
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)  
{

ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}

if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}

Trace.traceBegin(Trace.TRACE\_TAG\_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent);
Trace.traceEnd(Trace.TRACE\_TAG\_ACTIVITY_MANAGER);

mLoaders.put(zip, pathClassloader);
return pathClassloader;
}

Trace.traceBegin(Trace.TRACE\_TAG\_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
Trace.traceEnd(Trace.TRACE\_TAG\_ACTIVITY_MANAGER);
return pathClassloader;
}
}

可以看到,应用程序使用的ClassLoader都是PathClassLoader类的实例。那么,这个PathClassLoader是什么呢?从Android SDK给出的源码只能看出这么多:

1
2
3
4
5
6
7
8
9
10
11
public class PathClassLoader extends BaseDexClassLoader {  
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}

public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}

SDK没有导出这个类的源码,我们去androidxref上面看;发现其实这个类真的就这么多内容;我们继续查看它的父类BaseDexClassLoader;ClassLoader嘛,我们查看findClass或者defineClass方法,BaseDexClassLoader的findClass方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
protected Class<?> findClass(String name) throws ClassNotFoundException {  
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \\"" \+ name + "\\" on path: " \+ pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

可以看到,查找Class的任务通过pathList完成;这个pathList是一个DexPathList类的对象,它的findClass方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Class findClass(String name, List<Throwable> suppressed) {  
for (Element element : dexElements) {
DexFile dex = element.dexFile;

if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

这个DexPathList内部有一个叫做dexElements的数组,然后findClass的时候会遍历这个数组来查找Class;如果我们把插件的信息塞进这个数组里面,那么不就能够完成类的加载过程吗?!!

给默认ClassLoader打补丁

通过上述分析,我们知道,可以把插件的相关信息放入BaseDexClassLoader的表示dex文件的数组里面,这样宿主程序的ClassLoader在进行类加载,遍历这个数组的时候,会自动遍历到我们添加进去的插件信息,从而完成插件类的加载!

接下来,我们实现这个过程;我们会用到一些较为复杂的反射技术哦~不过代码非常短:

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
public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)  
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// 获取 BaseDexClassLoader : pathList
Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObj = pathListField.get(cl);

// 获取 PathList: Element\[\] dexElements
Field dexElementArray = pathListObj.getClass().getDeclaredField("dexElements");
dexElementArray.setAccessible(true);
Object\[\] dexElements = (Object\[\]) dexElementArray.get(pathListObj);

// Element 类型
Class<?> elementClass = dexElements.getClass().getComponentType();

// 创建一个数组, 用来替换原始的数组
Object\[\] newElements = (Object\[\]) Array.newInstance(elementClass, dexElements.length + 1);

// 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
Constructor<?> constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);
Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));

Object\[\] toAddElementArray = new Object\[\] { o };
// 把原始的elements复制进去
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
// 插件的那个element复制进去
System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

// 替换
dexElementArray.set(pathListObj, newElements);

}

短短的二十几行代码,我们就完成了『委托宿主ClassLoader加载插件类』的任务;因此第二种方案也宣告完成!我们简要总结一下这种方式的原理:

  1. 默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。
  2. 宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。

小结

本文中我们采用两种方案成功完成了『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

『激进方案』中我们自定义了插件的ClassLoader,并且绕开了Framework的检测;利用ActivityThread对于LoadedApk的缓存机制,我们把携带这个自定义的ClassLoader的插件信息添加进mPackages中,进而完成了类的加载过程。

『保守方案』中我们深入探究了系统使用ClassLoader findClass的过程,发现应用程序使用的非系统类都是通过同一个PathClassLoader加载的;而这个类的最终父类BaseDexClassLoader通过DexPathList完成类的查找过程;我们hack了这个查找过程,从而完成了插件类的加载。

这两种方案孰优孰劣呢?

很显然,『激进方案』比较麻烦,从代码量和分析过程就可以看出来,这种机制异常复杂;而且在解析apk的时候我们使用的PackageParser的兼容性非常差,我们不得不手动处理每一个版本的apk解析api;另外,它Hook的地方也有点多:不仅需要Hook AMS和H,还需要Hook ActivityThread的mPackages和PackageManager!

『保守方案』则简单得多(虽然原理也不简单),不仅代码很少,而且Hook的地方也不多;有一点正本清源的意思,从最最上层Hook住了整个类的加载过程。

但是,我们不能简单地说『保守方案』比『激进方案』好。从根本上说,这两种方案的差异在哪呢?

『激进方案』是多ClassLoader构架,每一个插件都有一个自己的ClassLoader,因此类的隔离性非常好——如果不同的插件使用了同一个库的不同版本,它们相安无事!『保守方案』是单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,虽然代码简单,但是鲁棒性很差;一旦插件之间甚至插件与宿主之间使用的类库有冲突,那么直接GG。

多ClassLoader还有一个优点:可以真正完成代码的热加载!如果插件需要升级,直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可(Java中,不同ClassLoader加载的同一个类被认为是不同的类);单ClassLoader的话实现非常麻烦,有可能需要重启进程。

在J2EE领域中广泛使用ClasLoader的地方均采用多ClassLoader架构,比如Tomcat服务器,Java模块化事实标准的OSGi技术;所以,我们有足够的理由认为选择多ClassLoader架构在大多数情况下是明智之举

目前开源的插件方案中,DroidPlugin采用的『激进方案』,Small采用的『保守方案』那么,有没有两种优点兼顾的方案呢??

答案自然是有的。

DroidPlugin和Small的共同点是两者都是非侵入式的插件框架;什么是『非侵入式』呢?打个比方,你启动一个插件Activity,直接使用startActivity即可,就跟开发普通的apk一样,开发插件和普通的程序对于开发者来说没有什么区别。

如果我们一定程度上放弃这种『侵入性』,那么我们就能实现一个两者优点兼而有之的插件框架!这里我先卖个关子~

OK,本文的内容就到这里了;关于『插件机制对于Activity的处理方式』也就此完结。要说明的是,在本文的『保守方案』其实只处理了代码的加载过程,它并不能加载有资源的apk!所以目前我这个实现基本没什么暖用;当然我这里只是就『代码加载』进行举例;至于资源,那牵扯到另外一个问题——插件系统的资源管理机制这个在后续文章的合适机会我会单独讲解。

接下来的文章,会讲述Android四大组件的另外三个ServiceBroadCastReceiver, ContentProvider的处理方式。喜欢就点个赞吧~持续更新,请关注github项目 understand-plugin-framework和我的 博客! 这文章我前前后后准备了快两个星期,如果你看到了这里,还请支持一下 :)

#android #binder #classloader #droidplugin #plugin framework

Android插件化原理解析——广播的管理

Android 插件化原理解析——Activity生命周期管理

Android 插件化原理解析——插件加载机制

本文转载自http://weishu.me/2016/04/05/understand-plugin-framework-classloader/

上文 Activity生命周期管理 中我们地完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的任务;通过Hook AMS和拦截ActivityThread中H类对于组件调度我们成功地绕过了AndroidMAnifest.xml的限制。

但是我们启动的『没有在AndroidManifet.xml中显式声明』的Activity和宿主程序存在于同一个Apk中;通常情况下,插件均以独立的文件存在甚至通过网络获取,这时候插件中的Activity能否成功启动呢?

要启动Activity组件肯定先要创建对应的Activity类的对象,从上文 Activity生命周期管理 知道,创建Activity类对象的过程如下:

1
2
3
4
5
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();  
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

也就是说,系统通过ClassLoader加载了需要的Activity类并通过反射调用构造函数创建出了Activity对象。如果Activity组件存在于独立于宿主程序的文件之中,系统的ClassLoader怎么知道去哪里加载呢?因此,如果不做额外的处理,插件中的Activity对象甚至都没有办法创建出来,谈何启动?

因此,要使存在于独立文件或者网络中的插件被成功启动,首先就需要解决这个插件类加载的问题。
下文将围绕此问题展开,完成『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

阅读本文之前,可以先clone一份 understand-plugin-framework,参考此项目的classloader-hook 模块。另外,插件框架原理解析系列文章见索引

ClassLoader机制

或许有的童鞋还不太了解Java的ClassLoader机制,我这里简要介绍一下。

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化的,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
与那些在编译时进行链连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以同代拓展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。例如,如果编写一个面相接口的应用程序,可以等到运行时在制定实际的实现类;用户可以通过Java与定义的和自定义的类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为代码的一部分,这种组装应用程序的方式目前已经广泛应用于Java程序之中。从最基础的Applet,JSP到复杂的OSGi技术,都使用了Java语言运行期类加载的特性。

Java的类加载是一个相对复杂的过程;它包括加载、验证、准备、解析和初始化五个阶段;对于开发者来说,可控性最强的是加载阶段;加载阶段主要完成三件事:

  1. 根据一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为JVM方法区中的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

『通过一个类的全限定名获取描述此类的二进制字节流』这个过程被抽象出来,就是Java的类加载器模块,也即JDK中ClassLoader API。

Android Framework提供了DexClassLoader这个类,简化了『通过一个类的全限定名获取描述次类的二进制字节流』这个过程;我们只需要告诉DexClassLoader一个dex文件或者apk文件的路径就能完成类的加载。因此本文的内容用一句话就可以概括:

将插件的dex或者apk文件告诉『合适的』DexClassLoader,借助它完成插件类的加载

关于CLassLoader机制更多的内容,请参阅『深入理解Java虚拟机』这本书。

思路分析

Android系统使用了ClassLoader机制来进行Activity等组件的加载;apk被安装之后,APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name/base-1.apk )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类;但是系统并不知道存在于插件中的Activity组件的信息(插件可以是任意位置,甚至是网络,系统无法提前预知),因此正常情况下系统无法加载我们插件中的类;因此也没有办法创建Activity的对象,更不用谈启动组件了。

解决这个问题有两个思路,要么全盘接管这个类加载的过程;要么告知系统我们使用的插件存在于哪里,让系统帮忙加载;这两种方式或多或少都需要干预这个类加载的过程。老规矩,知己知彼,百战不殆。我们首先分析一下,系统是如果完成这个类加载过程的。

我们再次搬出Activity的创建过程的代码:

1
2
3
4
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();  
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

这里可以很明显地看到,系统通过待启动的Activity的类名className,然后使用ClassLoader对象cl把这个类加载进虚拟机,最后使用反射创建了这个Activity类的实例对象。要想干预这个ClassLoader(告知它我们的路径或者替换他),我们首先得看看这玩意到底是个什么来头。(从哪里创建的)

cl这个ClasssLoader对象通过r.packageInfo对象的getClassLoader()方法得到,r.packageInfo是一个LoadedApk类的对象;那么,LoadedApk到底是个什么东西??

我们查阅LoadedApk类的文档,只有一句话,不过说的很明白:

Local state maintained about a currently loaded .apk.

LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。

OK, 我们知道这个LoadedApk是何方神圣了;接下来我们要搞清楚的是:这个 r.packageInfo 到底是从哪里获取的?

我们顺着 performLaunchActivity上溯,辗转handleLaunchActivity回到了 H 类的LAUNCH_ACTIVITY消息,找到了r.packageInfo的来源:

1
2
3
4
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;  
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);

getPackageInfoNoCheck方法很简单,直接调用了getPackageInfo方法:

1
2
3
4
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,  
CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
}

在这个getPackageInfo方法里面我们发现了端倪:

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
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,  
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
// 获取userid信息
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
// 尝试获取缓存信息
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}

LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
// 缓存没有命中,直接new
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG\_HAS\_CODE) != 0, registerPackage);

// 省略。。更新缓存
return packageInfo;
}
}

这个方法很重要,我们必须弄清楚每一步;

首先,它判断了调用方和或许App信息的一方是不是同一个userId;如果是同一个user,那么可以共享缓存数据(要么缓存的代码数据,要么缓存的资源数据)

接下来尝试获取缓存数据;如果没有命中缓存数据,才通过LoadedApk的构造函数创建了LoadedApk对象;创建成功之后,如果是同一个uid还放入了缓存。

提到缓存数据,看过Hook机制之Binder Hook的童鞋可能就知道了,我们之前成功借助ServiceManager的本地代理使用缓存的机制Hook了各种Binder;因此这里完全可以如法炮制——我们拿到这一份缓存数据,修改里面的ClassLoader;自己控制类加载的过程,这样加载插件中的Activity类的问题就解决了。这就引出了我们加载插件类的第一种方案:

激进方案:Hook掉ClassLoader,自己操刀

从上述分析中我们得知,在获取LoadedApk的过程中使用了一份缓存数据;这个缓存数据是一个Map,从包名到LoadedApk的一个映射。正常情况下,我们的插件肯定不会存在于这个对象里面;但是如果我们手动把我们插件的信息添加到里面呢?系统在查找缓存的过程中,会直接命中缓存!进而使用我们添加进去的LoadedApk的ClassLoader来加载这个特定的Activity类!这样我们就能接管我们自己插件类的加载过程了!

这个缓存对象mPackages存在于ActivityThread类中;老方法,我们首先获取这个对象:

1
2
3
4
5
6
7
8
9
10
// 先获取到当前的ActivityThread对象  
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
Map mPackages = (Map) mPackagesField.get(currentActivityThread);

拿到这个Map之后接下来怎么办呢?我们需要填充这个map,把插件的信息塞进这个map里面,以便系统在查找的时候能命中缓存。但是这个填充这个Map我们出了需要包名之外,还需要一个LoadedApk对象;如何创建一个LoadedApk对象呢?

我们当然可以直接反射调用它的构造函数直接创建出需要的对象,但是万一哪里有疏漏,构造参数填错了怎么办?又或者Android的不同版本使用了不同的参数,导致我们创建出来的对象与系统创建出的对象不一致,无法work怎么办?

因此我们需要使用与系统完全相同的方式创建LoadedApk对象;从上文分析得知,系统创建LoadedApk对象是通过getPackageInfo来完成的,因此我们可以调用这个函数来创建LoadedApk对象;但是这个函数是private的,我们无法使用。

有的童鞋可能会有疑问了,private不是也能反射到吗?我们确实能够调用这个函数,但是private表明这个函数是内部实现,或许那一天Google高兴,把这个函数改个名字我们就直接GG了;但是public函数不同,public被导出的函数你无法保证是否有别人调用它,因此大部分情况下不会修改;我们最好调用public函数来保证尽可能少的遇到兼容性问题。(当然,如果实在木有路可以考虑调用私有方法,自己处理兼容性问题,这个我们以后也会遇到)

间接调用getPackageInfo这个私有函数的public函数有同名的getPackageInfo系列和getPackageInfoNoCheck;简单查看源代码发现,getPackageInfo除了获取包的信息,还检查了包的一些组件;为了绕过这些验证,我们选择使用getPackageInfoNoCheck获取LoadedApk信息。

构建插件LoadedApk对象

我们这一步的目的很明确,通过getPackageInfoNoCheck函数创建出我们需要的LoadedApk对象,以供接下来使用。

这个函数的签名如下:

1
2
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,  
CompatibilityInfo compatInfo) {

因此,为了调用这个函数,我们需要构造两个参数。其一是ApplicationInfo,其二是CompatibilityInfo;第二个参数顾名思义,代表这个App的兼容性信息,比如targetSDK版本等等,这里我们只需要提取出app的信息,因此直接使用默认的兼容性即可;在CompatibilityInfo类里面有一个公有字段DEFAULT_COMPATIBILITY_INFO代表默认兼容性信息;因此,我们的首要目标是获取这个ApplicationInfo信息。

构建插件ApplicationInfo信息

我们首先看看ApplicationInfo代表什么,这个类的文档说的很清楚:

Information you can retrieve about a particular application. This corresponds to information collected from the AndroidManifest.xml’s tag.

也就是说,这个类就是AndroidManifest.xml里面的 这个标签下面的信息;这个AndroidManifest.xml无疑是一个标准的xml文件,因此我们完全可以自己使用parse来解析这个信息。

那么,系统是如何获取这个信息的呢?其实Framework就有一个这样的parser,也即PackageParser;理论上,我们也可以借用系统的parser来解析AndroidMAnifest.xml从而得到ApplicationInfo的信息。但遗憾的是,这个类的兼容性很差;Google几乎在每一个Android版本都对这个类动刀子,如果坚持使用系统的解析方式,必须写一系列兼容行代码!!DroidPlugin就选择了这种方式,相关类如下:

DroidPlugin的PackageParser

DroidPlugin的PackageParser

看到这里我就问你怕不怕!!!这也是我们之前提到的私有或者隐藏的API可以使用,但必须处理好兼容性问题;如果Android 7.0发布,这里估计得添加一个新的类PackageParseApi24。

我这里使用API 23作为演示,版本不同的可能无法运行请自行查阅 DroidPlugin 不同版本如何处理。

OK回到正题,我们决定使用PackageParser类来提取ApplicationInfo信息。下图是API 23上,PackageParser的部分类结构图:

看起来有我们需要的方法 generateApplication;确实如此,依靠这个方法我们可以成功地拿到ApplicationInfo。
由于PackageParser是@hide的,因此我们需要通过反射进行调用。我们根据这个generateApplicationInfo方法的签名:

1
2
public static ApplicationInfo generateApplicationInfo(Package p, int flags,  
PackageUserState state)

可以写出调用generateApplicationInfo的反射代码:

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");  
// 首先拿到我们得终极目标: generateApplicationInfo方法
// API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// public static ApplicationInfo generateApplicationInfo(Package p, int flags,
// PackageUserState state) {
// 其他Android版本不保证也是如此.
Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",
packageParser$PackageClass,
int.class,
packageUserStateClass);

要成功调用这个方法,还需要三个参数;因此接下来我们需要一步一步构建调用此函数的参数信息。

构建PackageParser.Package

generateApplicationInfo方法需要的第一个参数是PackageParser.Package;从名字上看这个类代表某个apk包的信息,我们看看文档怎么解释:

Representation of a full package parsed from APK files on disk. A package consists of a single base APK, and zero or more split APKs.

果然,这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示;因此,要获取这个类,肯定需要解析整个apk文件。PackageParser中解析apk的核心方法是parsePackage,这个方法返回的就是一个Package类型的实例,因此我们调用这个方法即可;使用反射代码如下:

1
2
3
4
5
6
7
8
9
// 首先, 我们得创建出一个Package对象出来供这个方法调用  
// 而这个需要得对象可以通过 android.content.pm.PackageParser#parsePackage 这个方法返回得 Package对象得字段获取得到
// 创建出一个PackageParser对象供使用
Object packageParser = packageParserClass.newInstance();
// 调用 PackageParser.parsePackage 解析apk的信息
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

// 实际上是一个 android.content.pm.PackageParser.Package 对象
Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);

这样,我们就得到了generateApplicationInfo的第一个参数;第二个参数是解析包使用的flag,我们直接选择解析全部信息,也就是0;

构建PackageUserState

第三个参数是PackageUserState,代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可;

至此,generateApplicaionInfo的参数我们已经全部构造完成,直接调用此方法即可得到我们需要的applicationInfo对象;在返回之前我们需要做一点小小的修改:使用系统系统的这个方法解析得到的ApplicationInfo对象中并没有apk文件本身的信息,所以我们把解析的apk文件的路径设置一下(ClassLoader依赖dex文件以及apk的路径):

1
2
3
4
5
6
7
8
9
// 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可  
Object defaultPackageUserState = packageUserStateClass.newInstance();

// 万事具备!!!!!!!!!!!!!!
ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
packageObj, 0, defaultPackageUserState);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;

替换ClassLoader

获取LoadedApk信息

方才为了获取ApplicationInfo我们费了好大一番精力;回顾一下我们的初衷:

我们最终的目的是调用getPackageInfoNoCheck得到LoadedApk的信息,并替换其中的mClassLoader然后把把添加到ActivityThread的mPackages缓存中;从而达到我们使用自己的ClassLoader加载插件中的类的目的。

现在我们已经拿到了getPackageInfoNoCheck这个方法中至关重要的第一个参数applicationInfo;上文提到第二个参数CompatibilityInfo代表设备兼容性信息,直接使用默认的值即可;因此,两个参数都已经构造出来,我们可以调用getPackageInfoNoCheck获取LoadedApk:

1
2
3
4
5
6
7
8
9
10
11
// android.content.res.CompatibilityInfo  
Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);

Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT\_COMPATIBILITY\_INFO");
defaultCompatibilityInfoField.setAccessible(true);

Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);

Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

我们成功地构造出了LoadedAPK, 接下来我们需要替换其中的ClassLoader,然后把它添加进ActivityThread的mPackages中:

1
2
3
4
5
6
7
8
9
10
11
String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();  
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApk, classLoader);
// 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC; 那么就前功尽弃了.
sLoadedApk.put(applicationInfo.packageName, loadedApk);

WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName, weakReference);

我们的这个CustomClassLoader非常简单,直接继承了DexClassLoader,什么都没有做;当然这里可以直接使用DexClassLoader,这里重新创建一个类是为了更有区分度;以后也可以通过修改这个类实现对于类加载的控制:

1
2
3
4
5
6
public class CustomClassLoader extends DexClassLoader {  

public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
}

到这里,我们已经成功地把把插件的信息放入ActivityThread中,这样我们插件中的类能够成功地被加载;因此插件中的Activity实例能被成功第创建;由于整个流程较为复杂,我们简单梳理一下:

  1. 在ActivityThread接收到IApplication的 scheduleLaunchActivity远程调用之后,将消息转发给H
  2. H类在handleMessage的时候,调用了getPackageInfoNoCheck方法来获取待启动的组件信息。在这个方法中会优先查找mPackages中的缓存信息,而我们已经手动把插件信息添加进去;因此能够成功命中缓存,获取到独立存在的插件信息。
  3. H类然后调用handleLaunchActivity最终转发到performLaunchActivity方法;这个方法使用从getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader来加载Activity类,进而使用反射创建Activity实例;接着创建Application,Context等完成Activity组件的启动。

看起来好像已经天衣无缝万事大吉了;但是运行一下会出现一个异常,如下:

1
2
3
04-05 02:49:53.742  11759-11759/com.weishu.upf.hook_classloader E/AndroidRuntime﹕ FATAL EXCEPTION: main  
Process: com.weishu.upf.hook_classloader, PID: 11759
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.weishu.upf.ams\_pms\_hook.app/com.weishu.upf.ams\_pms\_hook.app.MainActivity}: java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException: Unable to get package info for com.weishu.upf.ams\_pms\_hook.app; is package not installed?

错误提示说是无法实例化 Application,而Application的创建也是在performLaunchActivity中进行的,这里有些蹊跷,我们仔细查看一下。

绕过系统检查

通过ActivityThread的performLaunchActivity方法可以得知,Application通过LoadedApk的makeApplication方法创建,我们查看这个方法,在源码中发现了上文异常抛出的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {  
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " \+ appClass
\+ ": " \+ e.toString(), e);
}
}

木有办法,我们只有一行一行地查看到底是哪里抛出这个异常的了;所幸代码不多。(所以说,缩小异常范围是一件多么重要的事情!!!)

第一句 getClassLoader() 没什么可疑的,虽然方法很长,但是它木有抛出任何异常(当然,它调用的代码可能抛出异常,万一找不到只能进一步深搜了;所以我觉得这里应该使用受检异常)。

然后我们看第二句,如果包名不是android开头,那么调用了一个叫做initializeJavaContextClassLoader的方法;我们查阅这个方法:

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
private void initializeJavaContextClassLoader() {  
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, 0, UserHandle.myUserId());
} catch (RemoteException e) {
throw new IllegalStateException("Unable to get package info for "
\+ mPackageName + "; is system dying?", e);
}
if (pi == null) {
throw new IllegalStateException("Unable to get package info for "
\+ mPackageName + "; is package not installed?");
}

boolean sharedUserIdSet = (pi.sharedUserId != null);
boolean processNameNotDefault =
(pi.applicationInfo != null &&
!mPackageName.equals(pi.applicationInfo.processName));
boolean sharable = (sharedUserIdSet || processNameNotDefault);
ClassLoader contextClassLoader =
(sharable)
? new WarningContextClassLoader()
: mClassLoader;
Thread.currentThread().setContextClassLoader(contextClassLoader);
}

这里,我们找出了这个异常的来源:原来这里调用了getPackageInfo方法获取包的信息;而我们的插件并没有安装在系统上,因此系统肯定认为插件没有安装,这个方法肯定返回null。所以,我们还要欺骗一下PMS,让系统觉得插件已经安装在系统上了;至于如何欺骗 PMS,Hook机制之AMS&PMS 有详细解释,这里直接给出代码,不赘述了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void hookPackageManager() throws Exception {  

// 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
// 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.

Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 获取ActivityThread里面原始的 sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);

// 准备好代理对象, 用来替换原始的对象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
new Class<?>\[\] { iPackageManagerInterface },
new IPackageManagerHookHandler(sPackageManager));

// 1\. 替换掉ActivityThread里面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);
}

OK到这里,我们已经能够成功地加载简单的独立的存在于外部文件系统中的apk了。至此 关于 DroidPlugin 对于Activity生命周期的管理已经完全讲解完毕了;这是一种极其复杂的Activity管理方案,我们仅仅写一个用来理解的demo就Hook了相当多的东西,在Framework层来回牵扯;这其中的来龙去脉要完全把握清楚还请读者亲自翻阅源码。另外,我在此 对DroidPlugin 作者献上我的膝盖~这其中的玄妙让人叹为观止!

上文给出的方案中,我们全盘接管了插件中类的加载过程,这是一种相对暴力的解决方案;能不能更温柔一点呢?通俗来说,我们可以选择改革,而不是革命——告诉系统ClassLoader一些必要信息,让它帮忙完成插件类的加载。

保守方案:委托系统,让系统帮忙加载

我们再次搬出ActivityThread中加载Activity类的代码:

1
2
3
4
5
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();  
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

我们知道 这个r.packageInfo中的r是通过getPackageInfoNoCheck获取到的;在『激进方案』中我们把插件apk手动添加进缓存,采用自己加载办法解决;如果我们不干预这个过程,导致无法命中mPackages中的缓存,会发生什么?

查阅 getPackageInfo方法如下:

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
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,  
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}

LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG\_HAS\_CODE) != 0, registerPackage);

// 略
}
}

可以看到,没有命中缓存的情况下,系统直接new了一个LoadedApk;注意这个构造函数的第二个参数aInfo,这是一个ApplicationInfo类型的对象。在『激进方案』中我们为了获取独立插件的ApplicationInfo花了不少心思;那么如果不做任何处理这里传入的这个aInfo参数是什么?

追本溯源不难发现,这个aInfo是从我们的替身StubActivity中获取的!而StubActivity存在于宿主程序中,所以,这个aInfo对象代表的实际上就是宿主程序的Application信息!

我们知道,接下来会使用new出来的这个LoadedApk的getClassLoader()方法获取到ClassLoader来对插件的类进行加载;而获取到的这个ClassLoader是宿主程序使用的ClassLoader,因此现在还无法加载插件的类;那么,我们能不能让宿主的ClasLoader获得加载插件类的能力呢?;如果我们告诉宿主使用的ClassLoader插件使用的类在哪里,就能帮助他完成加载!

宿主的ClassLoader在哪里,是唯一的吗?

上面说到,我们可以通过告诉宿主程序的ClassLoader插件使用的类,让宿主的ClasLoader完成对于插件类的加载;那么问题来了,我们如何获取到宿主的ClassLoader?宿主程序使用的ClasLoader默认情况下是全局唯一的吗?

答案是肯定的。

因为在FrameWork中宿主程序也是使用LoadedApk表示的,如同Activity启动是加载Activity类一样,宿主中的类也都是通过LoadedApk的getClassLoader()方法得到的ClassLoader加载的;由类加载机制的『双亲委派』特性,只要有一个应用程序类由某一个ClassLoader加载,那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。

表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader

LoadedApk的ClassLoader到底是什么?

现在我们确保了『使用宿主ClassLoader帮助加载插件类』可行性;那么我们应该如何完成这个过程呢?

知己知彼,百战不殆。

不论是宿主程序还是插件程序都是通过LoadedApk的getClassLoader()方法返回的ClassLoader进行类加载的,返回的这个ClassLoader到底是个什么东西??这个方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public ClassLoader getClassLoader() {  
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}

if (mIncludeCode && !mPackageName.equals("android")) {
// 略...
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);

StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}

可以看到,非android开头的包和android开头的包分别使用了两种不同的ClassLoader,我们只关心第一种;因此继续跟踪ApplicationLoaders类:

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
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)  
{

ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}

if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}

Trace.traceBegin(Trace.TRACE\_TAG\_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent);
Trace.traceEnd(Trace.TRACE\_TAG\_ACTIVITY_MANAGER);

mLoaders.put(zip, pathClassloader);
return pathClassloader;
}

Trace.traceBegin(Trace.TRACE\_TAG\_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
Trace.traceEnd(Trace.TRACE\_TAG\_ACTIVITY_MANAGER);
return pathClassloader;
}
}

可以看到,应用程序使用的ClassLoader都是PathClassLoader类的实例。那么,这个PathClassLoader是什么呢?从Android SDK给出的源码只能看出这么多:

1
2
3
4
5
6
7
8
9
10
11
public class PathClassLoader extends BaseDexClassLoader {  
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}

public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}

SDK没有导出这个类的源码,我们去androidxref上面看;发现其实这个类真的就这么多内容;我们继续查看它的父类BaseDexClassLoader;ClassLoader嘛,我们查看findClass或者defineClass方法,BaseDexClassLoader的findClass方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
protected Class<?> findClass(String name) throws ClassNotFoundException {  
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \\"" \+ name + "\\" on path: " \+ pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

可以看到,查找Class的任务通过pathList完成;这个pathList是一个DexPathList类的对象,它的findClass方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Class findClass(String name, List<Throwable> suppressed) {  
for (Element element : dexElements) {
DexFile dex = element.dexFile;

if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

这个DexPathList内部有一个叫做dexElements的数组,然后findClass的时候会遍历这个数组来查找Class;如果我们把插件的信息塞进这个数组里面,那么不就能够完成类的加载过程吗?!!

给默认ClassLoader打补丁

通过上述分析,我们知道,可以把插件的相关信息放入BaseDexClassLoader的表示dex文件的数组里面,这样宿主程序的ClassLoader在进行类加载,遍历这个数组的时候,会自动遍历到我们添加进去的插件信息,从而完成插件类的加载!

接下来,我们实现这个过程;我们会用到一些较为复杂的反射技术哦~不过代码非常短:

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
public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)  
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// 获取 BaseDexClassLoader : pathList
Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObj = pathListField.get(cl);

// 获取 PathList: Element\[\] dexElements
Field dexElementArray = pathListObj.getClass().getDeclaredField("dexElements");
dexElementArray.setAccessible(true);
Object\[\] dexElements = (Object\[\]) dexElementArray.get(pathListObj);

// Element 类型
Class<?> elementClass = dexElements.getClass().getComponentType();

// 创建一个数组, 用来替换原始的数组
Object\[\] newElements = (Object\[\]) Array.newInstance(elementClass, dexElements.length + 1);

// 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
Constructor<?> constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);
Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));

Object\[\] toAddElementArray = new Object\[\] { o };
// 把原始的elements复制进去
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
// 插件的那个element复制进去
System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

// 替换
dexElementArray.set(pathListObj, newElements);

}

短短的二十几行代码,我们就完成了『委托宿主ClassLoader加载插件类』的任务;因此第二种方案也宣告完成!我们简要总结一下这种方式的原理:

  1. 默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。
  2. 宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。

小结

本文中我们采用两种方案成功完成了『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

『激进方案』中我们自定义了插件的ClassLoader,并且绕开了Framework的检测;利用ActivityThread对于LoadedApk的缓存机制,我们把携带这个自定义的ClassLoader的插件信息添加进mPackages中,进而完成了类的加载过程。

『保守方案』中我们深入探究了系统使用ClassLoader findClass的过程,发现应用程序使用的非系统类都是通过同一个PathClassLoader加载的;而这个类的最终父类BaseDexClassLoader通过DexPathList完成类的查找过程;我们hack了这个查找过程,从而完成了插件类的加载。

这两种方案孰优孰劣呢?

很显然,『激进方案』比较麻烦,从代码量和分析过程就可以看出来,这种机制异常复杂;而且在解析apk的时候我们使用的PackageParser的兼容性非常差,我们不得不手动处理每一个版本的apk解析api;另外,它Hook的地方也有点多:不仅需要Hook AMS和H,还需要Hook ActivityThread的mPackages和PackageManager!

『保守方案』则简单得多(虽然原理也不简单),不仅代码很少,而且Hook的地方也不多;有一点正本清源的意思,从最最上层Hook住了整个类的加载过程。

但是,我们不能简单地说『保守方案』比『激进方案』好。从根本上说,这两种方案的差异在哪呢?

『激进方案』是多ClassLoader构架,每一个插件都有一个自己的ClassLoader,因此类的隔离性非常好——如果不同的插件使用了同一个库的不同版本,它们相安无事!『保守方案』是单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,虽然代码简单,但是鲁棒性很差;一旦插件之间甚至插件与宿主之间使用的类库有冲突,那么直接GG。

多ClassLoader还有一个优点:可以真正完成代码的热加载!如果插件需要升级,直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可(Java中,不同ClassLoader加载的同一个类被认为是不同的类);单ClassLoader的话实现非常麻烦,有可能需要重启进程。

在J2EE领域中广泛使用ClasLoader的地方均采用多ClassLoader架构,比如Tomcat服务器,Java模块化事实标准的OSGi技术;所以,我们有足够的理由认为选择多ClassLoader架构在大多数情况下是明智之举

目前开源的插件方案中,DroidPlugin采用的『激进方案』,Small采用的『保守方案』那么,有没有两种优点兼顾的方案呢??

答案自然是有的。

DroidPlugin和Small的共同点是两者都是非侵入式的插件框架;什么是『非侵入式』呢?打个比方,你启动一个插件Activity,直接使用startActivity即可,就跟开发普通的apk一样,开发插件和普通的程序对于开发者来说没有什么区别。

如果我们一定程度上放弃这种『侵入性』,那么我们就能实现一个两者优点兼而有之的插件框架!这里我先卖个关子~

OK,本文的内容就到这里了;关于『插件机制对于Activity的处理方式』也就此完结。要说明的是,在本文的『保守方案』其实只处理了代码的加载过程,它并不能加载有资源的apk!所以目前我这个实现基本没什么暖用;当然我这里只是就『代码加载』进行举例;至于资源,那牵扯到另外一个问题——插件系统的资源管理机制这个在后续文章的合适机会我会单独讲解。

接下来的文章,会讲述Android四大组件的另外三个ServiceBroadCastReceiver, ContentProvider的处理方式。喜欢就点个赞吧~持续更新,请关注github项目 understand-plugin-framework和我的 博客! 这文章我前前后后准备了快两个星期,如果你看到了这里,还请支持一下 :)

#android #binder #classloader #droidplugin #plugin framework

Android插件化原理解析——广播的管理

Android 插件化原理解析——Activity生命周期管理