本文將通過一個極簡的例子來說明RePlugin的one hook原理。文章相關程式碼:github.com/jeepc/Simpl… 。
所謂one hook,即hook classloader。為什麼hook classloader就能實現外掛化,這要從Android四大元件啟動原理說起(這裡只說Activity,其他讀者自行探究)。
熟悉Android framework層的讀者應該知道,一次startActivity的呼叫,會通過Binder IPC呼叫到AMS的startActivity方法,AMS會進行一系列檢驗工作,其中包括該Activity是否在AndroidManifest.xml中註冊過。只有通過檢驗才能返回到App自己的程式來建立Activity。RePlugin的做法非常巧妙,就是一開始傳遞的Activity為在AndroidManifest.xml預註冊的StubActivity(大部分的外掛化框架都會用這個方案,巧妙的在後頭),以此來騙過AMS的檢驗,返回App建立Activity時,由於我們hook了classloader,所以我們就可以為所欲為,替換成自己想啟動的Activity(巧妙之處在於只hook一個地方,相容性更好,不易出錯)。
hook相關程式碼(核心程式碼都來自RePlugin):
public class App extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
hookClassloader();
}
private void hookClassloader() {
try {
Context oBase = this.getBaseContext();
Object oPackageInfo = null;
oPackageInfo = ReflectUtils.readField(oBase, "mPackageInfo");
// 獲取mPackageInfo.mClassLoader
ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
if (oClassLoader == null) {
return;
}
ClassLoader cl = new SRPClassloader(oClassLoader.getParent(), oClassLoader);
// 將新的ClassLoader寫入mPackageInfo.mClassLoader
ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);
// 設定執行緒上下文中的ClassLoader為RePluginClassLoader
// 防止在個別Java庫用到了Thread.currentThread().getContextClassLoader()時,“用了原來的PathClassLoader”,或為空指標
Thread.currentThread().setContextClassLoader(cl);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
複製程式碼
程式碼很簡單就是利用反射,將我們自定義的Classloader替換掉原來mPackageInfo中的Classloader,因為後面類的載入都會用到mPackageInfo的Classloader,更深層的原因可以參考這篇文章《唯一外掛化Replugin原始碼及原理深度剖析--唯一Hook點原理》。自定義的SRPClassloader裡面具體實現,我們接著說。
public SRPClassloader(ClassLoader parent, ClassLoader orig) {
// 由於PathClassLoader在初始化時會做一些Dir的處理,所以這裡必須要傳一些內容進來
// 但我們最終不用它,而是拷貝所有的Fields
super("", "", parent);
mOrig = orig;
// 將原來宿主裡的關鍵欄位,拷貝到這個物件上,這樣騙系統以為用的還是以前的東西(尤其是DexPathList)
// 注意,這裡用的是“淺拷貝”(淺拷貝就是複製引用,確保物件一模一樣)
// Added by Jiongxuan Zhang
copyFromOriginal(orig);
initMethods(orig);
}
private void copyFromOriginal(ClassLoader orig) {
// Android 4.0以上只需要複製pathList即可
// 以下方法在較慢的手機上用時:1ms
copyFieldValue("pathList", orig);
}
private void copyFieldValue(String field, ClassLoader orig) {
try {
Field f = ReflectUtils.getField(orig.getClass(), field);
if (f == null) {
return;
}
// 刪除final修飾符
ReflectUtils.removeFieldFinalModifier(f);
// 複製Field中的值到this裡
Object o = ReflectUtils.readField(f, orig);
ReflectUtils.writeField(f, this, o);
} catch (IllegalAccessException e) {
}
}
複製程式碼
以上程式碼註釋已經解釋很清楚了,就是利用反射將原來宿主裡的關鍵欄位,拷貝到自定的classloader物件中。
以上過程都是初始化,後面就是實際呼叫的過程了。
我們在MainActivity啟動StubActivity,StubAcitivity一行程式碼都沒有,只是在AndroidManifest.xml註冊過,以此欺騙AMS,後面就會呼叫到自定義的SRPClassloader的loadClass方法。
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//
Class<?> c = null;
//這裡直接用最簡單的方法hook(實際上啟動外掛的Activity,有很多工作要做,有很多問題要解決)
if(className.contains("StubActivity")){
c = mOrig.loadClass("com.jeepc.simplifiedrp.RealActivity");
}
if (c != null) {
return c;
}
//
try {
c = mOrig.loadClass(className);
return c;
} catch (Throwable e) {
//
}
//
return super.loadClass(className, resolve);
}
複製程式碼
這裡是我自己寫的程式碼,簡直讓人不忍直視,哈哈哈。就是判斷如果類名包含"StubActivity",就替換為RealActivity,所以這只是一個demo用來說明道理的,沒有實用價值。
最後再補充一下,為什麼這樣替換,RealActivity具有正常的生命週期呢?原因是因為建立RealActivity的時候會傳入一個token物件,這個token物件是一個IBinder用來和AMS通訊的。AMS後面就只認這個IBinder,不認Activity的名字了。所以RealActivity生命週期相關的方法能正常的執行。詳細可以參考這篇文章《Android 外掛化原理解析——Activity生命週期管理》
總結:這篇文章涉及到的內容很多,其中有關於java動態代理,以及binder、AMS等Android framework等知識。因為篇幅有限(其實是作者水平有限),未能深入剖析,讀者可以自行探究。而且關於RePlugin的原理,遠不止這些,該文章只是提及一小部分。不過通過幾天的學習,我發現RePlugin的程式碼寫得真心漂亮,如果有Android framework的基礎,深入學習也不是問題。以下參考連結都是非常好的學習資料,感謝!