[TOC]
一 .概述
插件化技术听起来高深莫测,实际上要解决的就是两个问题:
- 代码加载
- 资源加载
代码加载
类的加载可以使用Java的ClassLoader
机制,但是对于Android来说,并不是说类加载进来就可以用了,很多组件都是有“生命”的;因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理;
另外,如何管理加载进来的类也是一个问题。假设多个插件依赖了相同的类,是抽取公共依赖进行管理还是插件单独依赖?这就是ClassLoader的管理问题;
资源加载
资源加载方案大家使用的原理都差不多,都是用AssetManager
的隐藏方法addAssetPath
;但是,不同插件的资源如何管理?是公用一套资源还是插件独立资源?共用资源如何避免资源冲突?对于资源加载,有的方案共用一套资源并采用资源分段机制解决冲突(要么修改aapt
要么添加编译插件);有的方案选择独立资源,不同插件管理自己的资源。
目前国内开源的较成熟的插件方案有DL和DroidPlugin;但是DL方案仅仅对Frameworl的表层做了处理,严重依赖that
语法,编写插件代码和主程序代码需单独区分;而DroidPlugin通过Hook增强了Framework层的很多系统服务,开发插件就跟开发独立app差不多;就拿Activity生命周期的管理来说,DL的代理方式就像是牵线木偶,插件只不过是操纵傀儡而已;而DroidPlugin则是借尸还魂,插件是有血有肉的系统管理的真正组件;DroidPlugin Hook了系统几乎所有的Sevice,欺骗了大部分的系统API;掌握这个Hook过程需要掌握很多系统原理,因此学习DroidPlugin对于整个Android FrameWork层大有裨益。
二. Android 类加载器
二.反射基础知识
反射机制的概念就不描述了,看了只会更晕,直接看反射机制能干嘛:
反射机制的作用:
1,反编译:.class-->.java
2,通过反射机制访问java对象的属性,方法,构造方法等;
反射机制中的类:
java.lang.reflect.Constructor;
java.lang.reflect.Field;
java.lang.reflect.Method;
java.lang.reflect.Modifier;
很多反射中的方法,属性等操作我们可以从这四个类中查询。
具体实现:
1,反射机制获取类有三种方法,我们来获取Student类型
//第一种方式:
Classc1 = Class.forName("Student");
//第二种方式:
//java中每个类型都有class 属性.
Classc2 = Student.class;
//第三种方式:
//java语言中任何一个java对象都有getClass 方法
Employeee = new Student();
Classc3 = e.getClass(); //c3是运行时类 (e的运行时类是Student)
2,创建对象:获取类以后我们来创建它的对象,利用newInstance:
Class c =Class.forName("Student");
//创建此Class 对象所表示的类的一个新实例
Objecto = c.newInstance(); //调用了Student的无参数构造方法.
3,获取属性,这里我们先只看获取属性,方法是同样的道理。获取属性也分为分为所有的属性和指定的属性:
- 先看获取所有的属性的写法
//获取整个类
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);
这里可以通过getDeclaredFields
拿到所有的属性数组,然后对于每一个属性,可以通过
getModifiers 获得属性的修饰符,例如public,static等等
getSimpleName 获得属性的类型名称
getName 获得属性的名称
field.get(Object object) 获得相应Field的值
- 指定的属性
public static void main(String[] args) throws Exception{
//获取类
Class c = Class.forName("User");
//获取id属性
Field idF = c.getDeclaredField("id");
//实例化这个类赋给o
Object o = c.newInstance();
//打破封装
idF.setAccessible(true); //使用反射机制可以打破封装性,导致了java对象的属性不安全。设置为true就可以访问private修饰的东西,否则无法访问
//给o对象的id属性赋值"110"
idF.set(o, "110"); //set
//get
System.out.println(idF.get(o));
}
通过getDeclaredField 来拿到指定的属性
4.获取方法,和构造方法 同理
获取类的无参构造函数,并实例化类
Class clazz = Class.forName("com.yano.reflect.Person");
Constructor c = clazz.getConstructor(null);
Person p = (Person) c.newInstance(null);
获取类的含参私有构造函数,并实例化类
Class clazz = Class.forName("com.yano.reflect.Person");
Constructor c = clazz
.getDeclaredConstructor(new Class[] { String.class });
// 由于构造函数是 private 的,所以需要屏蔽Java语言的访问检查
c.setAccessible(true);
Person p = (Person) c
.newInstance(new Object[] { "I'm a reflect name!" });
获取并调用类的无参方法
Class clazz = Class.forName("com.yano.reflect.Person");
Constructor c = clazz.getConstructor(null);
Person p = (Person) c.newInstance(null);
Method method = clazz.getMethod("fun", null);
method.invoke(p, null);
获取并调用类的含参方法
Class clazz = Class.forName("com.yano.reflect.Person");
Constructor c = clazz.getConstructor(null);
Person p = (Person) c.newInstance(null);
Method method = clazz.getMethod("fun", new Class[] { String.class });
method.invoke(p, new Object[] { "I'm a reflect method!" });
二. Android插件化原理解析——Hook机制之动态代理
代理是什么
为什么需要代理呢?其实这个代理与日常生活中的“代理”,“中介”差不多;比如你想海淘买东西,总不可能亲自飞到国外去购物吧,这时候我们使用第三方海淘服务比如惠惠购物助手等;同样拿购物为例,有时候第三方购物会有折扣比如当初的米折网,这时候我们可以少花点钱;当然有时候这个“代理”比较坑,坑我们的钱,坑我们的货。
从这个例子可以看出来,代理可以实现方法增强,比如常用的日志,缓存等;也可以实现方法拦截,通过代理方法修改原方法的参数和返回值,从而实现某种不可告人的目的~接下来我们用代码解释一下。
静态代理
静态代理,是最原始的代理方式;假设我们有一个购物的接口,如下:
public interface Shopping {
Object[] doShopping(long money);
}
它有一个原始的实现,我们可以理解为亲自,直接去商店购物:
public class ShoppingImpl implements Shopping {
@Override
public Object[] doShopping(long money) {
System.out.println("逛淘宝 ,逛商场,买买买!!");
System.out.println(String.format("花了%s块钱", money));
return new Object[] { "鞋子", "衣服", "零食" };
}
}
好了,现在我们自己没时间但是需要买东西,于是我们就找了个代理帮我们买:
public class ProxyShopping implements Shopping {
Shopping base;
ProxyShopping(Shopping base) {
this.base = base;
}
@Override
public Object[] doShopping(long money) {
// 先黑点钱(修改输入参数)
long readCost = (long) (money * 0.5);
System.out.println(String.format("花了%s块钱", readCost));
// 帮忙买东西
Object[] things = base.doShopping(readCost);
// 偷梁换柱(修改返回值)
if (things != null && things.length > 1) {
things[0] = "被掉包的东西!!";
}
return things;
}
public class TestStatic {
public static void main(String[] args) {
// 原始的厂家
Shopping women = new ShoppingImpl();
System.out.println(Arrays.toString(women.doShopping(100)));
// 换成代购
women = new ProxyShopping(women);
System.out.println(Arrays.toString(women.doShopping(100)));
}
}
很不幸,我们找的这个代理有点坑,坑了我们的钱还坑了我们的货;先忍忍。
从以上代码中我们可以了解到,通过静态代理实现我们的需求需要我们在每个方法中都添加相应的逻辑,这里只存在两个方法所以工作量还不算大,假如Sell接口中包含上百个方法呢?这时候使用静态代理就会编写许多冗余代码。通过使用动态代理,我们可以做一个“统一指示”,从而对所有代理类的方法进行统一处理,而不用逐一修改每个方法。下面我们来具体介绍下如何使用动态代理方式实现我们的需求。
动态代理
传统的静态代理模式需要为每一个需要代理的类写一个代理类,如果需要代理的类有几百个那不是要累死?为了更优雅地实现代理模式,JDK提供了动态代理方式,可以简单理解为JVM可以在运行时帮我们动态生成一系列的代理类,这样我们就不需要手写每一个静态的代理类了。依然以购物为例,用动态代理实现如下:
public class ShoppingHandler implements InvocationHandler {
/**
* 被代理的原始对象
*/
Object base;
public ShoppingHandler(Object base) {
this.base = base;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("doShopping".equals(method.getName())) {
// 这里是代理Shopping接口的对象
// 先黑点钱(修改输入参数)
Long money = (Long) args[0];
long readCost = (long) (money * 0.5);
System.out.println(String.format("花了%s块钱", readCost));
// 帮忙买东西
Object[] things = (Object[]) method.invoke(base, readCost);
// 偷梁换柱(修改返回值)
if (things != null && things.length > 1) {
things[0] = "被掉包的东西!!";
}
return things;
}
if ("doSomething".equals(method.getName())) {
// 可以代理别的,做些别的事情
return null;
}
if ("doSomethingElse".equals(method.getName())) {
// 做些别的事情
return null;
}
return null;
}
}
public static void main(String[] args) {
Shopping women = new ShoppingImpl();
// 正常购物
System.out.println(Arrays.toString(women.doShopping(100)));
// 招代理
women = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(),
women.getClass().getInterfaces(), new ShoppingHandler(women));
System.out.println(Arrays.toString(women.doShopping(100)));
}
动态代理主要处理InvocationHandler
和Proxy
类;完整代码可以见github
上面只是个demo,如果没接触过动态代理,肯定会一头雾水,大神们这里也是一笔带过,我们就来分析分析什么是动态代理
动态代理的类和接口
1,Java.lang.reflect.Proxy:动态代理机制的主类,提供一组静态方法为一组接口动态的生成对象和代理类。
// 方法 1: 该方法用于获取指定代理对象所关联的调用处理器
public static InvocationHandler getInvocationHandler(Object proxy)
// 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象
public static Class> getProxyClass(ClassLoader loader,
Class>... interfaces)
// 方法 3:该方法用于判断指定类对象是否是一个动态代理类
public static boolean isProxyClass(Class> cl)
// 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces,InvocationHandler h)
2,java.lang.reflect.InvocationHandler:调用处理器接口,自定义invokle方法,用于实现对于真正委托类的代理访问。在使用动态代理时,我们需要定义一个位于代理类与委托类之间的中介类,这个中介类被要求实现InvocationHandler接口,这个接口的定义如下:
/**
该方法负责集中处理动态代理类上的所有方法调用。
第一个参数既是代理类实例,
第二个参数是被调用的方法对象
第三个方法是调用参数。
调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
可以理解为我们真正实现坑钱偷货的地方,这里虽然我们偷鸡摸狗了,但是雇主委托我们该做的事情还是要做的,做生意要诚信,这里用到了反射的方法去调用它原来的真正实现。即需要实现以下接口:
3,java.lang.ClassLoader:类装载器类,将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy类与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中**。
动态代理机制
java动态代理创建对象的过程为如下步骤:
1.通过实现 InvocationHandler 接口创建自己的调用处理器;
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
// 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
InvocationHandler handler = new InvocationHandlerImpl(..);
2,通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
// 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象
Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... });
3,通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
// 通过反射从生成的类对象获得构造函数对象
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class });
4,通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
// 通过构造函数对象创建动态代理类实例
Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });
好复杂啊,都是什么鬼,简单真诚点,为了简化对象创建过程,Proxy类中的newProxyInstance方法封装了2~4,只需两步即可完成代理对象的创建。
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发,也就是真正的委托实现
InvocationHandler handler = new InvocationHandlerImpl(..);
// 通过 Proxy 直接创建动态代理类实例
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,
new Class[] { Interface.class },
handler );
看下这个方法的三个参数的意义:
复制代码
public static Object newProxyInstance(ClassLoader loader, Class>[] interfaces, InvocationHandler h) throws IllegalArgumentException
loader: 一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
interfaces: 一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
h: 一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上
终于在这里我们最后拿到的proxy就是我们想要的代理。完成了委托->中介->代理
ok,到这里我们梳理一下如何使用动态代理
1.声明一个接口,用作具体的委托事务声明
2.声明一个委托类,集成委托接口,实现委托的具体实现,如:上面的购物,我们实现我们原本要购买的东西,这个时候因为委托者自己买的,当然不会坑蒙拐骗自己。
3.实现一个中介类,这个中介类实现InvocationHandler,在invoke方法中中介在实现客户需求基础上,进行了坑蒙拐骗,挖了一点油水。
4.调用Proxy.newProxyInstance来生成一个委托实例,这个时候我们就可以拿着这个实例去做想做的事情了。
再总结下:
1.在中介类中,我们持有了一个委托类,然后再invoke方法中调用了他的相应方法,这跟我们的静态代理差不多,因此可以理解为:中介类与委托类构成了静态代理关系。在这个关系中,中介类是代理类,委托类就是委托类;
- 代理类与中介类也构成一个静态代理关系,在这个关系中,中介类是委托类,代理类是代理类。也就是说,动态代理关系由两组静态代理关系组成,这就是动态代理的原理。
代理Hook
我们知道代理有比原始对象更强大的能力,比如飞到国外买东西,比如坑钱坑货;那么很自然,如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了;修改参数,替换返回值,我们称之为Hook。
下面我们Hook掉startActivity
这个方法,使得每次调用这个方法之前输出一条日志;(当然,这个输入日志有点点弱,只是为了展示原理;只要你想,你想可以替换参数,拦截这个startActivity
过程,使得调用它导致启动某个别的Activity,指鹿为马!)
这里的hook我们用到了静态代理,松口气,动态代理好复杂。。。
首先我们得找到被Hook的对象,我称之为Hook点;什么样的对象比较好Hook呢?自然是容易找到的对象。什么样的对象容易找到?静态变量和单例;在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。我们根据这个原则找到所谓的Hook点。
然后我们分析一下startActivity
的调用链,找出合适的Hook点。我们知道对于Context.startActivity
(Activity.startActivity的调用链与之不同),由于Context
的实现实际上是ContextImpl
;我们看ConetxtImpl
类的startActivity
方法:
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity)null, intent, -1, options);
}
这里,实际上使用了ActivityThread
类的mInstrumentation
成员的execStartActivity
方法;注意到,ActivityThread
实际上是主线程,而主线程一个进程只有一个,因此这里是一个良好的Hook点。
接下来就是想要Hook掉我们的主线程对象,也就是把这个主线程对象里面的mInstrumentation
给替换成我们修改过的代理对象;要替换主线程对象里面的字段,首先我们得拿到主线程对象的引用,如何获取呢?ActivityThread
类里面有一个静态方法currentActivityThread
可以帮助我们拿到这个对象类;但是ActivityThread
是一个隐藏类,我们需要用反射去获取,代码如下:
// 先获取到当前的ActivityThread对象
Class> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
拿到这个currentActivityThread
之后,我们需要修改它的mInstrumentation
这个字段为我们的代理对象,我们先实现这个代理对象,由于JDK动态代理只支持接口,而这个Instrumentation
是一个类,没办法,我们只有手动写静态代理类,覆盖掉原始的方法即可。(cglib
可以做到基于类的动态代理,这里先不介绍)
public class EvilInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;
public EvilInstrumentation(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// Hook之前, XXX到此一游!
Log.d(TAG, "\n执行了startActivity, 参数如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
// 某该死的rom修改了 需要手动适配
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
Ok,有了代理对象,我们要做的就是偷梁换柱!代码比较简单,采用反射直接修改:
public static void attachContext() throws Exception{
// 先获取到当前的ActivityThread对象
Class> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 创建代理对象
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
// 偷梁换柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
好了,我们启动一个Activity测试一下,结果如下:
可见,Hook确实成功了!这就是使用代理进行Hook的原理——偷梁换柱。整个Hook过程简要总结如下:
- 寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法,非public不保证每个版本都一样,需要适配。
- 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib。
- 偷梁换柱——用代理对象替换原始对象
完整代码参照:understand-plugin-framework;里面留有一个作业:我们目前仅Hook了Context
类的startActivity
方法,但是Activity
类却使用了自己的mInstrumentation
;你可以尝试Hook掉Activity类的startActivity
方法。