手把手講解 Android Hook無清單啟動Activity的應用
前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細,但是這是為了幫助到儘量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.
這個系列的文章:
1、用通俗易懂的講解方式,講解一門技術的實用價值
2、詳細書寫原始碼的追蹤,原始碼截圖,繪製類的結構圖,儘量詳細地解釋原理的探索過程
3、提供Github 的 可執行的Demo工程,但是我所提供程式碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的執行過程中的注意事項
5、用gif圖,最直觀地展示demo執行效果
如果覺得細節太細,直接跳過看結論即可。
本人能力有限,如若發現描述不當之處,歡迎留言批評指正。
學到老活到老,路漫漫其修遠兮。與眾君共勉 !
引子
前面3篇文章,由易到難(入門, 深入 ,高階),用通俗易懂的語言講述了Hook的用法,最後一篇,實現了 啟動沒有在 menifest
中註冊的 Activity
的效果, 然而,這樣做到底在生產開發中有什麼樣的應用呢?
答案:外掛化.
外掛化是一個寬泛的概念,只要是實現了 宿主app上外掛功能的靈活拔插,實現了宿主app業務和外掛功能的完全解耦,就可以稱之為外掛化.
之前寫過一篇 外掛化的文章: 手把手講解 Android外掛化啟動Activity , 那時候用的外掛化,
原理是用 宿主中 真實Activity
作為 代理
,來啟動外掛中的 Activity
,管理外掛中 Activity
的 生命週期
,並且處理好 外掛原始碼
和 資原始檔
。
現在,外掛化有另一種方式,就是利用 無清單啟動Activity的原理
,實現 外掛apk中Activity的啟動.
Demo地址: https://github.com/18598925736/HookPluginDevDemo
鳴謝
感謝群裡大佬 夜雨提供的demo
感謝享學課堂 VIP群裡Alvin老師的提點
正文大綱
1.整體思路
2.實際效果展示
3.Demo原始碼講解
4.坑坑更健康
正文
1.整體思路
下方有兩張圖:表示了外掛化架構中,外掛單獨執行,和 外掛作為宿主的一部分隨宿主啟動的技術關鍵點。

hook外掛化.png

hook外掛化2.png
如上圖,如果跟隨宿主一起啟動,外掛 apk
的資原始檔要能夠被宿主讀到,外掛的 apk
的 class
檔案也必須能夠被宿主讀取,實現的方式就是,讓在宿主的程式碼中進行 hook
程式設計,生成一個能夠讀取宿主以及所有外掛內 class
的 ClassLoader
,以及 一個能夠讀取 宿主以及外掛內所有資源的 Resource
.而,實現的具體過程,就是一個 融合
過程.
2.實際效果展示
mumu模擬器上的效果

plugin.gif
宿主manifest檔案

image.png
3.Demo原始碼講解
宿主

外掛

image.png
如果您down了我的 Demo ,那麼觀察一下,就會發現,無論是宿主的程式碼, 還是外掛的程式碼,都非常簡單,唯一閱讀價值的,就是 宿主的 Hook核心程式碼
。
在講解 Hook核心程式碼
之前,先回顧一下我的上篇文章所實現的效果:
能夠繞過系統的manifest檢測機制,讓沒有在manifest中註冊的Activity也能夠正常啟動
一定有讀者在看完上篇文章之後,會想, 能夠不去註冊就可以啟動Activity,是很神奇,但是又有什麼利用價值呢? 僅僅是為了不去註冊就去幹涉系統邏輯,太華而不實了.
這個問題的答案:
用 hook
實現外掛化啟動 Activity
,外掛中的 manifest
並不會和宿主的 manifest
發生融合,也就是說,即使我們完成了 對 ClassLoader
和 Resource
的融合,實現了宿主對外掛 class
和資源的訪問,如果不能繞過系統的 manifest
檢測,依然不能啟動外掛的 Activity
.
所以,用hook技術實現外掛化啟動Activity,完整思路是:

hook外掛化完整思路.png
以下是關鍵程式碼 :
宿主的 MyApplication.java 主要用於呼叫Hook核心程式碼 :
public class MyApplication extends Application { private Resources newResource; public static String pluginPath = null; @Override public void onCreate() { super.onCreate(); pluginPath = AssetUtil.copyAssetToCache(this, Const.PLUGIN_FILE_NAME); //Hook第一次,繞過manifest檢測 GlobalActivityHookHelper.hook(this); //Hook第二次把外掛的原始檔class匯入到系統的ClassLoader中 HookInjectHelper.injectPluginClass(this); //Hook第三次,載入外掛資源包,讓系統的Resources能夠讀取外掛的資源 newResource = HookInjectHelper.injectPluginResources(this); } //重寫資源管理器,資源管理器是每個Activity自帶的, // 而Application的getResources則是所有Activity共有的 //重寫了它,就不必一個一個Activity去重寫了 @Override public Resources getResources() { return newResource == null ? super.getResources() : newResource; } }
繞過manifest檢測的hook核心程式碼 GlobalActivityHookHelper.java
public class GlobalActivityHookHelper { public static void hook(Context context) { hookAMS(context);//使用假的Activity,騙過AMS的檢測 if (ifSdkOverIncluding28()) hookActivityThread_mH_AfterIncluding28();//將真實的Intent還原回去,讓系統可以跳到原本該跳的地方. else { hookActivityThread_mH_before28(context); } hookPM(context);//由於AppCompatActivity存在PMS檢測,如果這裡不hook的話,就會包PackageNameNotFoundException } //裝置系統版本是不是大於等於26 private static boolean ifSdkOverIncluding26() { int SDK_INT = Build.VERSION.SDK_INT; if (SDK_INT > 26 || SDK_INT == 26) { return true; } else { return false; } } //裝置系統版本是不是大於等於26 private static boolean ifSdkOverIncluding28() { int SDK_INT = Build.VERSION.SDK_INT; if (SDK_INT > 28 || SDK_INT == 28) { return true; } else { return false; } } ...太長了就不都貼出來了,可以到demo裡面去看 }
將宿主和外掛的ClassLoader/Resource融合的 HookInjectHelper.java
public class HookInjectHelper { /** * * 此方法的作用是:外掛內的class融合到宿主的classLoader中,讓宿主可以直接讀取外掛內的class * * @param context */ public static void injectPluginClass(Context context) { String cachePath = context.getCacheDir().getAbsolutePath(); String apkPath = MyApplication.pluginPath; //還記不記得dexClassLoader?它是專門用於載入外部apk的classes.dex檔案的 //(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) // 4個引數分別是,外部dex的path,優化之後的目錄,lib庫檔案查詢目錄,我們這沒有用到lib裡面的so,所以可以設定為null,最後一個是父ClassLoader DexClassLoader dexClassLoader = new DexClassLoader(apkPath, cachePath, null, context.getClassLoader()); //先構造一個能夠讀取外部apk的classLoader物件 //第一步找到外掛的Elements陣列dexPathlist----?dexElement try { Class myDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader"); Field myPathListFiled = myDexClazzLoader.getDeclaredField("pathList"); myPathListFiled.setAccessible(true); Object myPathListObject = myPathListFiled.get(dexClassLoader); Class myPathClazz = myPathListObject.getClass(); Field myElementsField = myPathClazz.getDeclaredField("dexElements"); myElementsField.setAccessible(true); //自己外掛的dexElements[] Object myElements = myElementsField.get(myPathListObject); //第二步找到系統的Elements陣列dexElements PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); Class baseDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader"); Field pathListFiled = baseDexClazzLoader.getDeclaredField("pathList"); pathListFiled.setAccessible(true); Object pathListObject = pathListFiled.get(pathClassLoader); Class systemPathClazz = pathListObject.getClass(); Field systemElementsField = systemPathClazz.getDeclaredField("dexElements"); systemElementsField.setAccessible(true); //系統的dexElements[] Object systemElements = systemElementsField.get(pathListObject); //第三步上面的dexElements陣列合併成新的dexElements然後通過反射重新注入系統的Field (dexElements )變數中 //新的Element[] 物件 //dalvik.system.Element int systemLength = Array.getLength(systemElements); int myLength = Array.getLength(myElements); //找到 Element的Class型別陣列每一個成員的型別 Class<?> sigleElementClazz = systemElements.getClass().getComponentType(); int newSysteLength = myLength + systemLength; Object newElementsArray = Array.newInstance(sigleElementClazz, newSysteLength); //融合 for (int i = 0; i < newSysteLength; i++) { //先融合 外掛的Elements if (i < myLength) { Array.set(newElementsArray, i, Array.get(myElements, i)); } else { Array.set(newElementsArray, i, Array.get(systemElements, i - myLength)); } } Field elementsField = pathListObject.getClass().getDeclaredField("dexElements"); ; elementsField.setAccessible(true); //將新生成的EleMents陣列物件重新放到系統中去 elementsField.set(pathListObject, newElementsArray); } catch (Exception e) { e.printStackTrace(); } } public static Resources injectPluginResources(Context context) { AssetManager assetManager; Resources newResource = null; String apkPath = MyApplication.pluginPath; try { assetManager = AssetManager.class.newInstance(); Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); addAssetPathMethod.setAccessible(true); addAssetPathMethod.invoke(assetManager, apkPath); Resources supResource = context.getResources(); newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration()); } catch (Exception e) { e.printStackTrace(); } return newResource; } }
關於 Resource
的融合,我的文章: 手把手講解 Android hook技術實現一鍵換膚 裡面有提及。
繞過manifest檢測,在另一篇文章 手把手講解 Android Hook-實現無清單啟動Activity 有詳解,我就不再贅述了。
詳細講講 ClassLoader
如何融合.
推薦一下 安卓原始碼的檢視網址: https://www.androidos.net.cn/sourcecode ,可以很方便幫助我們閱讀系統原始碼,而不必去花大時間去下載整個安卓原始碼。
老規矩,先上圖,下圖是 相關類
的關係圖:

ClassLoader融合.png
我們用 context.getClassLoader
拿到的是 PathClassLoader
,而我們構建能夠訪問外掛中 class
的 classLoader
是 DexClassLoader
,他們有共同的父類 BaseDexClassLoader
,而且,這個 BaseDexClassLoader
類的本身就擁有能夠裝載多個 dex
路徑的能力。
外掛 DexClassLoader
讀取的是外掛 apk
中的 classes.dex
,宿主 PathClassLoader
讀取的是 data/app/包名/base.apk
的 classes.dex
. 他們分別將讀取到的路徑,存到了上圖中的 Element[] dexElements
陣列中.
那麼如果我們可以將外掛 DexClassLoader
中的 dexElements
融合到 宿主 PathClassLoader
的 dexElements
中去,就可以實現宿主讀取外掛 apk
的 class.dex
.
demo程式碼中 HookInjectHelper類中的 injectPluginClass 方法,就是以上面的思路為依據進行的hook。
具體步驟為:
1.構建外掛 DexClassLoader
物件
2.獲得系統的 PathClassLoader
物件
3.分別獲得外掛 DexClassLoader
和系統 PathClassLoader
的 DexPathList
中的 dexElements
陣列
4.將上述兩個 dexElements
陣列進行融合
5.將融合之後的的 dexElements
設定到系統 PathClassLoader
中
至此,系統也能夠訪問外掛 apk
中的 class
了.
就講到這裡,具體可以看原始碼。
那麼接下來,如何啟動外掛中的Activity呢?
我的Demo中,由於我們在寫宿主程式碼的時候,並不能直接引用外掛的類,所以我們只能通過如下方式:

image.png
那麼又如何啟動宿主自身的Activity其他呢?可以按照上面的方式。
或者也可以用普通的方式:

image.png
而宿主的 manifest
裡,依然只有一個 Activity
,其他的都可以不經註冊直接啟動,剩下的這一個是為了作為 launch Activity
:

image.png
4.坑坑更健康
前方高能,驚天巨坑
細心的讀者一定發現了,我在宿主裡面用的是 android.app.Activity
,而不是 AppCompatActivity
。
包括宿主內的第二個 Main2Activity
,依然是 android.app.Activity
。
因為我發現,如果換成 AppCompatActivity
,我啟動宿主的時候,就會報莫名其妙的異常。
03-09 18:39:19.069 16437-16437/study.hank.com.myhookplugindevdemo E/AndroidRuntime: FATAL EXCEPTION: main Process: study.hank.com.myhookplugindevdemo, PID: 16437 java.lang.RuntimeException: Unable to start activity ComponentInfo{study.hank.com.myhookplugindevdemo/study.hank.com.myhookplugindevdemo.ui.MainActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2443) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503) at android.app.ActivityThread.-wrap11(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5529) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635) Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference at android.support.v7.app.AppCompatDelegateImplV9.createSubDecor(AppCompatDelegateImplV9.java:410) at android.support.v7.app.AppCompatDelegateImplV9.ensureSubDecor(AppCompatDelegateImplV9.java:323) at android.support.v7.app.AppCompatDelegateImplV9.setContentView(AppCompatDelegateImplV9.java:284) at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:139) at study.hank.com.myhookplugindevdemo.ui.MainActivity.onCreate(MainActivity.java:22) at android.app.Activity.performCreate(Activity.java:6278) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2396) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503) at android.app.ActivityThread.-wrap11(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5529) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635)
諮詢了度娘,一無所獲,然後請教了大佬,得到了靠譜答案, AppCompatActivity
在啟動的時候會進行上下文檢查,於是報出了上面的問題。使用 Activity
就好了, 不用使用 AppCompatActivity
.
實際上後續我也查了兩者的區別, AppCompatActivity
是為了相容低版本裝置而設計的,他和 Activity
的區別是, AppCompatActivity
擁有預設的 ActionBar
,也擁有自己的 Theme
類。而 Activity
預設不帶 ActionBar
, Theme
的使用也和前者不同.
所以我到目前為止也很疑惑,不過倒並不影響我們外掛化開發,用 android.app.Activity
和 AppCompatActivity
開發的 Activity
也並沒有出現什麼相容問題.
其實在 我的 手把手講解 Android外掛化啟動Activity 中,也出現過一次類似的問題,使用 android.app.Activity
沒問題,但是換成 AppCompatActivity
,則會報上面一樣的錯誤,相當詭異,但是也同樣不影響開發.
有知道原因的兄弟們記得留言啊,一起討論一下.
結語
外掛化開發這個話題,看起來高深莫測,實際上玩起來也並不簡單。實現的方式也不止一種。
目前就我瞭解,看來有兩種解決方案,用宿主的真實Activity去代理外掛Activity,另一種就是用hook去繞過manifest檢查. 兩種方案各有優劣,hook可能會失效,因為谷歌最近釋出了 禁用反射的API名單,而且androidStudio也在使用反射的時候提示,反射可能失效。但是,還是那句話,天塌下來砸不到我們的頭上,自然有大佬頂著,到時候,如果谷歌真的禁用反射,國內的巨佬們自然有新的解決辦法,到時候跟隨大流就好了。 而代理Activity的方式,則多了一個PluginLib層,需要維護,好處就是,不用看谷歌臉色。
hook外掛化四部曲:
手把手講解 Android Hook-Activity的啟動流程
手把手講解 Android Hook-實現無清單啟動Activity
手把手講解 Android Hook無清單啟動Activity的應用歡迎大家留言指點.