[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
方法。