Android插件化——談?wù)勎依斫獾目游?/h1>

坑位的概念

第一次聽說坑位的概念是在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等瓮孙。

  1. 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;
        }
    }
  1. 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;
    }

注釋寫的比較清楚,簡單說下原理:

  1. 獲取Application的LoadedApk對象mLoadedApk
  2. 獲取LoadedApk的屬性ClassLoader mClassLoader
  3. 通過反射進(jìn)行替換啼肩,這里寫死了一些內(nèi)容橄妆,比如遇到名稱為MainActivity2的Activity則替換成MainActivity3

測試

  1. Application初始化:
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        HookUtils.hookClassLoader(this);
    }
}
  1. 設(shè)置坑位
    AndroidManifest注冊一個不存在的Activity


    坑位
  2. 啟動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)
    }

  1. 結(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í)的心侍芝,多看看研铆,多想想。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者

  • 序言:七十年代末州叠,一起剝皮案震驚了整個濱河市棵红,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咧栗,老刑警劉巖逆甜,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異致板,居然都是意外死亡交煞,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門斟或,熙熙樓的掌柜王于貴愁眉苦臉地迎上來素征,“玉大人,你說我怎么就攤上這事≈擅” “怎么了纸淮?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長亚享。 經(jīng)常有香客問我咽块,道長,這世上最難降的妖魔是什么欺税? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任侈沪,我火速辦了婚禮,結(jié)果婚禮上晚凿,老公的妹妹穿的比我還像新娘亭罪。我一直安慰自己,他們只是感情好歼秽,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布应役。 她就那樣靜靜地躺著,像睡著了一般燥筷。 火紅的嫁衣襯著肌膚如雪箩祥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天肆氓,我揣著相機(jī)與錄音袍祖,去河邊找鬼。 笑死谢揪,一個胖子當(dāng)著我的面吹牛蕉陋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拨扶,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼凳鬓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了患民?” 一聲冷哼從身側(cè)響起村视,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酒奶,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奶赔,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惋嚎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了站刑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片另伍。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出摆尝,到底是詐尸還是另有隱情温艇,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布堕汞,位于F島的核電站勺爱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏讯检。R本人自食惡果不足惜琐鲁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望人灼。 院中可真熱鬧围段,春花似錦、人聲如沸投放。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灸芳。三九已至涝桅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間耗绿,已是汗流浹背苹支。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留误阻,地道東北人债蜜。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像究反,于是被迫代替她去往敵國和親寻定。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 導(dǎo)語 插件化技術(shù)最早從2012年誕生至今精耐,已經(jīng)走過了5個年頭狼速。從最初只支持Activity的動態(tài)加載發(fā)展到可以完全...
    junesolar閱讀 1,535評論 0 9
  • 題記 寫這篇關(guān)于Replugin插件化框架的分析,旨在引導(dǎo)讀者去快速的了解RePlugin的大概實(shí)現(xiàn)原理卦停,文中會拋...
    Ihesong閱讀 1,680評論 0 1
  • 是時候來一波Android插件化了 是時候來一波Android插件化了前言Android開發(fā)演進(jìn)模塊化介紹插件化介...
    流水不腐小夏閱讀 4,779評論 3 51
  • CSS選擇器:類選擇器: .classname為形式向胡,同一個文檔中可以出現(xiàn)多個同樣的classname。ID選擇器...
    garble閱讀 170評論 0 0
  • 1惊完、這幾天僵芹,停止了作業(yè)。因?yàn)橥蝗坏念j廢起來小槐,覺得網(wǎng)絡(luò)寫作里的詼諧幽默我沒有拇派,那些靈修類的文章背后我也缺乏系統(tǒng)的學(xué)習(xí)...
    夏木o閱讀 82評論 0 0