坑位的概念
第一次聽說坑位的概念是在360開源插件化框架RePlugin淘邻,我印象最深刻的就是在演講過程中提到的只Hook了一處以及獨(dú)創(chuàng)坑位概念。雖然下載了源碼并且也大致了解了原理函荣,但是自己好像還是有些模糊,感覺抓不到重點(diǎn)掌敬。昨天在看Hook AMS來實(shí)現(xiàn)啟動一個不在AndroidManifest注冊的Activity正卧,因?yàn)榘姹締栴}蠢熄,網(wǎng)上代碼基本上都不行了。突然想起這個坑位法炉旷,決定自己嘗試一次签孔!
原理
- 坑位的概念是指在AndroidManifest中注冊,但并沒有真實(shí)的實(shí)現(xiàn)類窘行,只作為其他Activity啟動的坑位
- Hook點(diǎn)為ClassLoader饥追,Android中的ClassLoader有兩個,分別為DexClassLoader和PathClassLoader罐盔,用于加載APK的是PathClassLoader但绕,也是Android里面默認(rèn)的類加載器,這個也就是需要Hook的地方惶看。
過程如下:
啟動流程
這個原理是真心簡單捏顺,這里需要有關(guān)于ClassLoader和Activity啟動流程的知識。
我們知道在啟動一個新的Activity時纬黎,AMS會對其進(jìn)行很多檢測幅骄,例如是否在AndroidManifest中注冊,是否有權(quán)限啟動等等莹桅。如果這些都通過昌执,那么需要判斷當(dāng)前的進(jìn)程是否存在,不存在需要先調(diào)用
ActivityThread.main()
方法诈泼,開啟線程循環(huán)以及啟動Application。最終會通過ActivityThread的Handler發(fā)送一條為“BIND_APPLICATION”的消息煤禽,通過這個消息铐达,Handler來處理這次Application
的創(chuàng)建過程。這里會創(chuàng)建Application檬果、LoadedApk等瓮孙。
- LoadedApk對象是APK文件在內(nèi)存中的表示。 Apk文件的相關(guān)信息选脊,諸如Apk文件的代碼和資源杭抠,甚至代碼里面的Activity,Service等組件的信息我們都可以通過此對象獲取恳啥。注意:這里會創(chuàng)建一個ClassLoader作為類加載器偏灿,也就是我們需要Hook的。
LoadedApk.java
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
}
return mClassLoader;
}
}
- Activity的創(chuàng)建是通過反射創(chuàng)建钝的,使用的就是上面提到的ClassLoader翁垂,所以我們只需要Hook住這個ClassLoader铆遭,通過類的雙親委派機(jī)制來實(shí)現(xiàn)我們自己的邏輯即可。
源碼分析部分省略沿猜,位置在ActivityThread處理LAUNCH_ACTIVITY的消息類型處枚荣。
代碼實(shí)現(xiàn)
Hook代碼:
public static void hookClassLoader(Application context) {
try {
// 獲取Application類的mLoadedApk屬性值
Object mLoadedApk = getFieldValue(context.getClass().getSuperclass(), context, "mLoadedApk");
if (mLoadedApk != null) {
// 獲取其mClassLoader屬性值以及屬性字段
final ClassLoader mClassLoader = (ClassLoader) getFieldValue(mLoadedApk.getClass(), mLoadedApk, "mClassLoader");
if (mClassLoader != null) {
Field mClassLoaderField = getField(mLoadedApk.getClass(), "mClassLoader");
// 替換成自己的ClassLoader
mClassLoaderField.set(mLoadedApk, new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 替換Activity
if (name.endsWith("MainActivity2")) {
Log.d(TAG, "loadClass: name = " + name);
name = name.replace("MainActivity2", "MainActivity3");
Log.d(TAG, "loadClass: 替換后name = " + name);
}
return mClassLoader.loadClass(name);
}
});
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 反射獲取屬性值
*
* @param c class
* @param o 對象
* @param fieldName 屬性名稱
* @return 值
* @throws NoSuchFieldException e
* @throws IllegalAccessException e
*/
public static Object getFieldValue(Class c, Object o, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = getField(c, fieldName);
if (field != null) {
return field.get(o);
} else {
return null;
}
}
/**
* 反射獲取對象屬性
*
* @param aClass c
* @param fieldName 屬性名稱
* @return 屬性
* @throws NoSuchFieldException e
*/
private static Field getField(Class<?> aClass, String fieldName) throws NoSuchFieldException {
Field field = aClass.getDeclaredField(fieldName);
if (field != null) {
field.setAccessible(true);
}
return field;
}
注釋寫的比較清楚,簡單說下原理:
- 獲取Application的LoadedApk對象mLoadedApk
- 獲取LoadedApk的屬性ClassLoader mClassLoader
- 通過反射進(jìn)行替換啼肩,這里寫死了一些內(nèi)容橄妆,比如遇到名稱為
MainActivity2
的Activity則替換成MainActivity3
測試
- Application初始化:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
HookUtils.hookClassLoader(this);
}
}
-
設(shè)置坑位
AndroidManifest注冊一個不存在的Activity
坑位 啟動Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val listener = object : View.OnClickListener {
override fun onClick(v: View?) {
val intent = Intent()
intent.component = ComponentName("com.example.administrator.test", "com.example.administrator.test.MainActivity2")
startActivity(intent)
}
}
// Example of a call to a native method
sample_text.text = "MainActivity"
bt_test.setOnClickListener(listener)
}
-
結(jié)果
結(jié)果
結(jié)果
可以看到,通過這種方式實(shí)現(xiàn)了不在AndroidManifest中注冊祈坠,但是可以啟動Activity的效果害碾。這里可以應(yīng)用到插件化中,如Replugin颁虐,編譯時自動注入坑位蛮原,運(yùn)行時進(jìn)行確定坑位。當(dāng)然了另绩,這里只是做一些微小的實(shí)現(xiàn)儒陨,如果想要真正完成完美的插件化,那真是革命尚未成功笋籽,同志仍需努力蹦漠。
總結(jié)
當(dāng)真正讀懂摸個框架源碼的時候,我常常會想:為什么我沒有想到這種方式车海?可能是缺少經(jīng)驗(yàn)笛园,也可能是思維固化了吧。保持一顆學(xué)習(xí)的心侍芝,多看看研铆,多想想。