插件化框架DynamicLoadApk與DroidPlugin中代理解析

一京办、前言:

隨著APP到開發(fā)到后期眾多功能模塊的加入瞒津,方法數越來越多面臨著超出65535的可能。雖然谷歌后來提供了multidex分包功能,但它還是有一些局限性比如分包過大響應慢彪腔、占用過大內存、低版本兼容等問題进栽。

頻繁的增加新模塊更新每次都要重新下載APP德挣、出現(xiàn)緊急BUG需要修復,在這種時候使用插件化開發(fā)就很有必要了快毛。

在去年阿里的云棲大會上格嗅,阿里宣布他們使用的插件化框架Atlas將于今年年初開源。最近我在看關于插件開發(fā)的技術唠帝,還想著Atlas什么時候開源屯掖,沒過幾天就發(fā)現(xiàn)Atlas框架在這個月13號開源了。Atlas使用入門教程

上面都是題外話襟衰,本文主要是對以下兩款框架中代理應用解析:

二贴铜、功能比較:

(1)DynamicLoadApk
  • 簡介:

插件不依賴宿主,但插件必須遵守一定的規(guī)則右蒲。
提供了宿主和插件交互的方式阀湿。
兼容性好很大程度是通過代理的方式進行插件化。
</br>

  • 原理:

1瑰妄、通過反射調用AssetManager的addAssetPath方法陷嘴,將一個插件apk中的資源加載到AssetManager中,然后再通過AssetManager來創(chuàng)建一個新的Resources對象间坐,就可以通過這個Resources對象來訪問插件apk中的資源了灾挨。
2、在宿主Manifest中預注冊代理組件Activity竹宋,當啟動插件組件時首先啟動一個代理組件劳澄,然后通過這個代理組件來構建、啟動插件組件蜈七。
插件里的activity其實是通過反射初始化的普通對象是沒有生命周期的秒拔,通過代理activity去調用插件activity實現(xiàn)的生命周期方法。

(2)DroidPlugin
  • 簡介:

宿主和插件完全隔離飒硅。
插件不依賴宿主可以獨立運行砂缩。
接入簡單,插件不需要修改三娩。
</br>

  • 原理:

1庵芭、基于動態(tài)代理的Hook,劫持了系統(tǒng)的大部分與系統(tǒng)服務進程通訊的方法雀监,欺騙系統(tǒng)以為只有一個apk在運行双吆,瞞過插件讓其認為自己已經安裝。
2、基于Android的多個apk可以運行在同一個進程的原理好乐。
3匾竿、需要預注冊Activity等組件實現(xiàn)免注冊。

通過以上簡單的介紹曹宴,可以很清楚的發(fā)現(xiàn)它們的核心實現(xiàn)都是基于代理來完成核心功能的搂橙。

三歉提、代理:

代理主要分為動態(tài)代理與靜態(tài)代理笛坦,動態(tài)代理與靜態(tài)代理相比較,最大的區(qū)別是接口中聲明的所有方法都被轉移到調用處理器一個集中的方法中處理苔巨。
DynamicLoadApk通過靜態(tài)代理技術偽裝成Activity版扩,DroidPlugin通過動態(tài)代理技術Hook劫持了系統(tǒng)管理。

以下我以自己的理解以故事的形式來描述靜態(tài)代理與動態(tài)代理侄泽。

靜態(tài)代理以賣香煙為例:
  • 小明去便利店買煙礁芦,店長讓店小二去取煙然后偷偷的取出假煙。在店小二把煙交給店長的時候店長用他嫻熟的技法掉了包并交給了小明悼尾,然而小明并不知道自己買了假煙反而歡喜的拿回家了柿扣。
  • 這個故事里小明是客戶、便利店是代理接口提供賣煙的功能闺魏、店長是代理通過操作店小二并做一些手腳實現(xiàn)賣煙功能未状、店小二是委托類他負責去倉庫取煙。

這個故事里還有個問題析桥,小明再去這個店買假水果的時候司草,店長必須要重新學習掉包水果的技能很是麻煩。在這個時候動態(tài)代理站出來了泡仗。

動態(tài)代理以賣香煙為例:
  • 小明活到了22世紀這時候都是無人售賣店埋虹,他又去買煙了。店長嫌每次學技術太麻煩為了快速賺錢研發(fā)了掉包處理器娩怎,小明投幣到自動售賣機后掉包處理器全自動掉包后交給了小明搔课,最后小明又歡樂的回家了。
  • 這個故事里小明還是客戶截亦、無人售賣店是代理接口爬泥、掉包處理器是代理(只要監(jiān)聽到有人調用售賣機買東西自動化掉包)、自動售賣機是委托類它負責取貨魁巩。

文字說明也描述完了我想應該可以幫助小伙伴更容易理解代理的含義急灭,在第四節(jié)我將以代碼的方式更直觀的展示代理的實際應用。
</br>

四谷遂、源碼模擬

(1)DynamicLoadApk

通過查看了DynamicLoadApk的源碼葬馋,我以自己的理解在這里寫了簡化版的代理Activity的代碼,了解下面代碼可以更容易理解DynamicLoadApk。

/**
 * 客戶類 
 */
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 靜態(tài)代理 模擬跳轉到指定的插件Activity
        Intent dlIntent = new Intent();
        dlIntent.putExtra(DLProxyActivity.CLASS_NAME, PluginActivity.class.getName());
        DLPluginManager.startPluginActivity(this, dlIntent);
    }
}
/**
 * 靜態(tài)代理類工廠 
 */
public class DLPluginManager {
    public static void startPluginActivity(Context context, Intent dlIntent) {
        //封裝跳轉真正的Activity
        dlIntent.setClass(context, DLProxyActivity.class);
        context.startActivity(dlIntent);
    }
}
/**
 * 代理接口 實現(xiàn)模擬Activity的生命周期
 */
public interface DLPlugin {
    public void onStart();
    public void onResume();
    public void onStop();
    public void onDestroy();
    public void onCreate(Bundle savedInstanceState);
    public void attach(Activity proxyActivity);
}
/**
 * 靜態(tài)代理類 真正啟動的Activity
 */
public class DLProxyActivity extends Activity{

    protected DLPlugin mRemoteActivity;
    public static final String CLASS_NAME = "className";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        String mClass = getIntent().getStringExtra(DLProxyActivity.CLASS_NAME);
        try {
            // 通過反射創(chuàng)建委托類
            Class<?> localClass = getClassLoader().loadClass(mClass);
            Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
            Object instance = localConstructor.newInstance(new Object[] {});
            mRemoteActivity = (DLPlugin) instance;
            // 綁定委托類
            mRemoteActivity.attach(this);
            mRemoteActivity.onCreate(new Bundle());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 關聯(lián)生命周期
     */
    @Override
    protected void onStart() {
        //.....預處理
        mRemoteActivity.onStart();
        //.....事后處理
        super.onStart();
    }

    @Override
    protected void onResume() {
        mRemoteActivity.onResume();
        super.onResume();
    }

    @Override
    protected void onStop() {
        mRemoteActivity.onStop();
        super.onStop();
    }
}
/**
 * 委托類基類畴嘶,對代理類功能的封裝
 */
public abstract class DLBasePluginActivity extends Activity implements DLPlugin {

    // 代理activity
    protected Activity mProxyActivity;
    protected Activity that;
    
    @Override
    public void attach(Activity proxyActivity) {
        mProxyActivity = (Activity) proxyActivity;
        that = mProxyActivity;
    }
    
    @Override
    public void setContentView(int layoutResID) {
        mProxyActivity.setContentView(layoutResID);
    }
    
    @Override
    public View findViewById(int id) {
        return mProxyActivity.findViewById(id);    
    }
    
    public void startPluginActivity(Intent dlIntent) {
        DLPluginManager.startPluginActivity(that, dlIntent);
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {}
    
    @Override
    public void onStart() {}
    
    @Override
    public void onResume() {}
    
    @Override
    public void onStop() {}
    
    @Override
    public void onDestroy() {}
}
/**
 * 委托類蛋逾,具體處理業(yè)務。
 * 其實就是一個假的Activity并沒有真正的生命周期窗悯,每個方法的調用都是通過代理Activity來實現(xiàn)的区匣。
 */
public class PluginActivity extends DLBasePluginActivity{
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main2);
        findViewById(R.id.click).setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                Toast.makeText(that, "didid", 0).show();
            }
        });
    }
}

以上就是DynamicLoadApk啟動插件Activity的簡化版流程,想要更深入的理解可以查看DynamicLoadApk的源碼蒋院。

(2)DroidPlugin

DroidPlugin更偏向與framework層亏钩,要對Android源代碼有一定的理解。
我這里推薦一個在線查看Android源碼的地址:http://www.grepcode.com/

通過查看源碼可以看到啟動Activity的時候欺旧,是通過ActivityManagerNative類里面的ActivityManagerProxy類來啟動Activity的姑丑,我們只要在這里動點手腳就可以了。

IActivityManager的實現(xiàn)類
初始化ActivityManagerProxy
創(chuàng)建ActivityManagerProxy

我們只要劫持了gDefault就可以做我們想要做的事情了辞友。

以啟動Activity為例:
/**
 * 客戶類 
 */
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 動態(tài)代理 以4.x以上版本為例
        try {
            Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
            // 找到Hook點 通過反射獲取gDefault字段
            Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
            // gDefault是私有的 需要暴利反射
            gDefaultField.setAccessible(true);
            // 因為是靜態(tài)方法所以傳入null就可以了
            Object gDefault = gDefaultField.get(null);
            // gDefault是一個 android.util.Singleton對象
            Class<?> singleton = Class.forName("android.util.Singleton");
            // 里面有一個成員變量IActivityManager mInstance 反射獲取它
            Field mInstanceField = singleton.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            // 獲取到IActivityManager對象
            Object rawIActivityManager = mInstanceField.get(gDefault);
            // 創(chuàng)建動態(tài)代理Hook
            ActivityManagerHook hooks = new ActivityManagerHook();
            Class<?> iActivityManagerInterface = Class.forName("android.app.IActivityManager");
            // 傳入委托類栅哀、委托類名
            Object proxy = hooks.newProxyInstance(rawIActivityManager, iActivityManagerInterface);
            // 替換成我們的代理
            mInstanceField.set(gDefault, proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
 * 動態(tài)代理IActivityManager
 */
public class ActivityManagerHook implements InvocationHandler {

    private Object mHookedObject;

    /**
     * @param hookedObject 委托類
     * @param clazz 代理接口
     * @return 代理
     */
    public Object newProxyInstance(Object hookedObject, Class<?> clazz){
        this.mHookedObject = hookedObject;
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[] { clazz }, this); 
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // 如果調用了startActivity方法 我們劫持傳過來的Intent替換成我想要跳轉的Activity
        if("startActivity".equals(method.getName()) && args[2] instanceof Intent){
            Intent i = new Intent();
            i.setClassName("com.example.plugindemo", "com.example.plugindemo.dynamicplugin.HomeActivity");
            args[2] = i;
        }
        Object result = method.invoke(mHookedObject, args);
        return result;
    }
}

五、結尾:

通過查看Android源碼并劫持它干一些事情称龙,我覺得還是挺有意思的留拾。好了就到這了希望這篇文章能幫助到各位小伙伴。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末鲫尊,一起剝皮案震驚了整個濱河市痴柔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌马昨,老刑警劉巖竞帽,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鸿捧,居然都是意外死亡屹篓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門匙奴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堆巧,“玉大人,你說我怎么就攤上這事泼菌〉簦” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵哗伯,是天一觀的道長荒揣。 經常有香客問我,道長焊刹,這世上最難降的妖魔是什么系任? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任恳蹲,我火速辦了婚禮,結果婚禮上俩滥,老公的妹妹穿的比我還像新娘嘉蕾。我一直安慰自己,他們只是感情好霜旧,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布错忱。 她就那樣靜靜地躺著,像睡著了一般挂据。 火紅的嫁衣襯著肌膚如雪以清。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天棱貌,我揣著相機與錄音玖媚,去河邊找鬼箕肃。 笑死婚脱,一個胖子當著我的面吹牛,可吹牛的內容都是我干的勺像。 我是一名探鬼主播障贸,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吟宦!你這毒婦竟也來了篮洁?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤殃姓,失蹤者是張志新(化名)和其女友劉穎袁波,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體蜗侈,經...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡篷牌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了踏幻。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枷颊。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖该面,靈堂內的尸體忽然破棺而出夭苗,到底是詐尸還是另有隱情,我是刑警寧澤隔缀,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布题造,位于F島的核電站,受9級特大地震影響猾瘸,放射性物質發(fā)生泄漏界赔。R本人自食惡果不足惜桥嗤,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仔蝌。 院中可真熱鬧泛领,春花似錦、人聲如沸敛惊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞧挤。三九已至锡宋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間特恬,已是汗流浹背执俩。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留癌刽,地道東北人役首。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像显拜,于是被迫代替她去往敵國和親衡奥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容