知識總結(jié) 插件化學(xué)習(xí) Activity加載分析

現(xiàn)在安卓插件化已經(jīng)很成熟婚夫,可以直接用別人開源的框架實現(xiàn)自己項目,但是學(xué)習(xí)插件化的實現(xiàn)原理是安卓研發(fā)工程師加深安卓系統(tǒng)理解的很好途徑蛀骇。

安卓插件化學(xué)習(xí) 插件Activity加載方式分析

實現(xiàn)一套插件化項目很容易评汰,但是投入生產(chǎn)環(huán)境,卻很難祠肥。自己以學(xué)習(xí)為目的武氓,主要分析其實現(xiàn)原理。

在工作和學(xué)習(xí)過程中雖然用到或了解到多家安卓插件化實現(xiàn)方式及原理,自己并沒有動手實現(xiàn)或參與公司插件化的研發(fā)县恕,so業(yè)余時間從基礎(chǔ)做起东羹,總結(jié)插件化實現(xiàn)原理,自己親自動手踩踩坑忠烛,實現(xiàn)原理及思路均來自開源項目及互聯(lián)網(wǎng)属提。

本文中首先來分析下插件actibity的加載原理,這里主要以任玉剛專專的DL開源項目中插件實現(xiàn)原理為參考美尸,采用靜態(tài)代理方式冤议,代理類反射調(diào)用沒有context的Activity。

思路分析

假如業(yè)界沒有插件化的實現(xiàn)思路师坎,如果自己接到一個插件化需求恕酸,要求可以動態(tài)加載安卓四大組件,這些類可以本地預(yù)制zip或是云端下載屹耐。

回到原點思考問題尸疆,怎么實現(xiàn)呢?

首先想到的肯定是ClassLoader惶岭,那邊安卓平臺的ClassLoader是如何應(yīng)用呢寿弱?可以查看Class源碼,發(fā)現(xiàn)安卓平臺SystemClassLoader是PathClassLoader按灶,具體原理看插件化基礎(chǔ)ClassLoader.

 /**
     * Encapsulates the set of parallel capable loader types.
     */
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");

        // String[] paths = classPath.split(":");
        // URL[] urls = new URL[paths.length];
        // for (int i = 0; i < paths.length; i++) {
        // try {
        // urls[i] = new URL("file://" + paths[i]);
        // }
        // catch (Exception ex) {
        // ex.printStackTrace();
        // }
        // }
        //
        // return new java.net.URLClassLoader(urls, null);

        // TODO Make this a java.net.URLClassLoader once we have those?
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

查看安卓系統(tǒng)源碼中Activity加載方式症革,會發(fā)現(xiàn)也是用ClassLoader完成的。

 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");

        ActivityInfo aInfo = r.activityInfo;
        ......

        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        }
        
        public ClassLoader getClassLoader() {
        synchronized (this) {
            if (mClassLoader == null) {
                createOrUpdateClassLoaderLocked(null /*addedPaths*/);
            }
            return mClassLoader;
        }
        
         private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
         ......
          if (!mIncludeCode) {
            if (mClassLoader == null) {
                StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
                mClassLoader = ApplicationLoaders.getDefault().getClassLoader(
                    "" /* codePath */, mApplicationInfo.targetSdkVersion, isBundledApp,
                    librarySearchPath, libraryPermittedPath, mBaseClassLoader);
                StrictMode.setThreadPolicy(oldPolicy);
            }

            return;
        }
         
    }
    
     public ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
                                      String librarySearchPath, String libraryPermittedPath,
                                      ClassLoader parent) {
     ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

        synchronized (mLoaders) {
            if (parent == null) {
                parent = baseParent;
            }          
                PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
                                                      zip,
                                                      librarySearchPath,
                                                      libraryPermittedPath,
                                                      parent,
                                                      targetSdkVersion,
                                                      isBundled);
          return pathClassloader;
        }                       

這里可以肯定安卓系統(tǒng)加載自己類及應(yīng)用層類的ClassLoader為PathClassLoader(打log也可以看出)鸯旁。那么繼續(xù)分析PathClassLoader看看能不能加載我們自己未安卓應(yīng)用的類?

 /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * The entries of the second list should be directories containing
     * native library files.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }

根據(jù)注釋噪矛,該類可以加載jar/zip/apk等壓縮包里的dex,自己動手寫代碼驗證下铺罢。答案肯定是可以的艇挨。

PathClassLoader沒有接口可以設(shè)置優(yōu)化后的dex防止地方,默認情況會用dexPath充當韭赘,這樣的話會有很多現(xiàn)在那我們想自定義優(yōu)化類path怎么辦缩滨?

看PathClassLoader的父類BasedexClassLoader會發(fā)現(xiàn),它還有個雙胞胎弟弟DexClassLoader泉瞻,為什么說是雙胞胎呢脉漏?應(yīng)為這兩個類自己都是啥事都沒敢,只是實現(xiàn)接口不太一樣袖牙,而DexClassLoader為我們提供了優(yōu)化后dex緩存path侧巨,實用更靈活。

但是網(wǎng)上有很多地方說PathClassLoader類只能加載已經(jīng)按照的應(yīng)用類鞭达,不能加載外部未按照的類司忱。并且有人說art虛擬機不行和dalvik虛擬機可以皇忿。根據(jù)自己親自實驗,PathClassLoader也是可以加載成功的坦仍,
只是dexOutputPath用了默認的路徑會有些限制禁添,至于網(wǎng)上很多不一樣的說法,個人理解可能不同的虛擬機實現(xiàn)或是不同系統(tǒng)版本可能有兼容性桨踪,未找到官方權(quán)威說法。

反射一個activity

按照原始問題思路芹啥,有了加載壓縮包中dex的ClassLoader锻离,那邊我們動態(tài)加載一個dex中的activity,看看能不能啟動一個activity墓怀。

1汽纠,準備dex包

寫一個簡單的apk,包含一個activity傀履,內(nèi)部做些簡單的事情虱朵。

@Override
    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Button btn = new Button(this);
            btn.setText("This is a plugin's Btn");
            setContentView(btn);
    }

2,創(chuàng)建ClassLoader

private DexClassLoader createDexClassLoader(String dexPath) {
        File dexOutputDir = context.getDir("dex", 0);
        this.dexOutputPath = dexOutputDir.getAbsolutePath();
        DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, nativeLibDir, context.getClassLoader());
        return loader;
    }
plugin3.png

雖然dexOutputPath可以隨意自定義钓账,但是還是建議放入/data/data下的應(yīng)用私有目錄中碴犬,防止別人修改自己的代碼。ClassLoader加載一次最好緩存起來梆暮,即加快下次的使用服协,也解決ClassLoader類隔離問題。

3啦粹,反射調(diào)用

            try {
                Class<?> clazz = getClassLoader().loadClass("com.canking.plugin.MainActivity");
                Object obj = clazz.newInstance();

                Method method = clazz.getDeclaredMethod("onCreate", Bundle.class);
                method.setAccessible(true);
                method.invoke(obj, new Bundle());
            } catch (Exception e) {
                Log.e("changxing", "load error:" + e.getMessage());
                e.printStackTrace();
            }

然而報錯了

分析:首先反射調(diào)用是沒問題的偿荷,完全可以從自己的壓縮包中加載類(activity)。但是在反射調(diào)用onCreate時類內(nèi)部報NullPointerException錯誤了唠椭。

這時發(fā)現(xiàn)new Button(this)時跳纳,this中的baseContext為null。這里分析贪嫂,一個正常的activity是什么時候才有Context呢寺庄?查看源碼找答案。

   private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
   ......
    Activity activity = null;
        try {
            //反射加載一個activity類
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        } catch (Exception e) {
           
        }

        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        
            if (activity != null) {
                //為activity構(gòu)造Context
                Context appContext = createBaseContextForActivity(r, activity);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window);
        ......
   }
   
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
            //為Activity的mBaseContext賦值
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
    }
    
    
     protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        //到這里Activity有了Context屬性撩荣。
        mBase = base;
    }

正常的的Activity被AMS反射調(diào)用铣揉,在attach后就有了Context,那我們自己反射的Activity要想有ConText餐曹,就要模擬AMS調(diào)用方式逛拱,構(gòu)造Context,但是這相當于再寫個系統(tǒng)台猴,不可實現(xiàn)朽合,那怎么辦俱两?

遇到問題,解決問題曹步。
插件中被反射的activity沒有了Context宪彩,我們可以把主apk的Acitvity的Context傳遞給插件Acitivity。

形成方案

有了以上分析讲婚,我們可以專門寫個主Apk中的Activity尿孔,用來處理插件中所需要的變量及資源,也可以調(diào)用插件中的部分方法筹麸。這樣這個類就變成類一個代理類活合。

這樣就形成了DL開源項目中的靜態(tài)代理方式實現(xiàn)的插件方案。進一步動手代碼實驗物赶,只要activity的每個回調(diào)接口都能回調(diào)到插件中的activity相同方法白指,并且插件中的對acitivity的每個設(shè)置都能夠回調(diào)到主apk中代理類處理,
這個插件方式就可以完美運行酵紫,至少針對目前的Activity沒問題告嘲。

plugin5.png

設(shè)置主插件Title為插件中Activity名字,讓它“更像”插件頁面。

 public CharSequence getActivityTitle(Context context, String activityName) {
        if (packageInfo.activities != null && packageInfo.activities.length > 0) {
            for (ActivityInfo info : packageInfo.activities) {
                if (info.name.equals(activityName)) {
                    return info.loadLabel(context.getPackageManager());
                }
            }
        }
        return "";
    }

loadLabel() 方法需要給加載PackageInfo設(shè)置壓縮包的sourceDir和publicSourceDir.

            //for activity name
            packageInfo.applicationInfo.sourceDir = dexPath;
            packageInfo.applicationInfo.publicSourceDir = dexPath;

實現(xiàn)總結(jié)

我們回調(diào)原點來從基礎(chǔ)分析DL項目靜態(tài)代理方式實現(xiàn)插件的實現(xiàn)過程奖地,回顧下橄唬,發(fā)現(xiàn)這種方式是最容易想到,那我們?yōu)槭裁礇]有比DL作者【任玉剛】早點想到并實現(xiàn)呢参歹?答案是:“沒有對應(yīng)的眼界轧坎,不夠勤快≡笫荆”
用玉剛常說的一句好說就是”這個社會還沒到比聰明時代缸血,想進步,就得比別人多用時間“械筛。

靜態(tài)代理方式雖然可以實現(xiàn)插件方式捎泻,但是用起來還是不方便,接下來我們進一步學(xué)習(xí)插件化埋哟,分析hook系統(tǒng)方法動態(tài)代理方式的思想的實現(xiàn)笆豁。

——————
歡迎轉(zhuǎn)載,請標明出處:常興E站 www.canking.win

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赤赊,一起剝皮案震驚了整個濱河市闯狱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抛计,老刑警劉巖哄孤,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吹截,居然都是意外死亡瘦陈,警方通過查閱死者的電腦和手機凝危,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晨逝,“玉大人蛾默,你說我怎么就攤上這事∽矫玻” “怎么了支鸡?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長趁窃。 經(jīng)常有香客問我苍匆,道長,這世上最難降的妖魔是什么棚菊? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮叔汁,結(jié)果婚禮上统求,老公的妹妹穿的比我還像新娘。我一直安慰自己据块,他們只是感情好码邻,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著另假,像睡著了一般像屋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上边篮,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天己莺,我揣著相機與錄音,去河邊找鬼戈轿。 笑死凌受,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的思杯。 我是一名探鬼主播胜蛉,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼色乾!你這毒婦竟也來了誊册?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤暖璧,失蹤者是張志新(化名)和其女友劉穎案怯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澎办,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡殴泰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年于宙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悍汛。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡捞魁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出离咐,到底是詐尸還是另有隱情谱俭,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布宵蛀,位于F島的核電站昆著,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏术陶。R本人自食惡果不足惜凑懂,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梧宫。 院中可真熱鬧接谨,春花似錦、人聲如沸塘匣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忌卤。三九已至扫夜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驰徊,已是汗流浹背笤闯。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棍厂,地道東北人望侈。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像勋桶,于是被迫代替她去往敵國和親脱衙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

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