知識總結 插件化學習 Activity加載分析

現在安卓插件化已經很成熟,可以直接用別人開源的框架實現自己項目,但是學習插件化的實現原理是安卓研發工程師加深安卓系統理解的很好途徑。

安卓插件化學習 插件Activity加載方式分析

實現一套插件化項目很容易,但是投入生產環境,卻很難。自己以學習為目的,主要分析其實現原理。

在工作和學習過程中雖然用到或了解到多家安卓插件化實現方式及原理,自己并沒有動手實現或參與公司插件化的研發,so業余時間從基礎做起,總結插件化實現原理,自己親自動手踩踩坑,實現原理及思路均來自開源項目及互聯網。

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

思路分析

假如業界沒有插件化的實現思路,如果自己接到一個插件化需求,要求可以動態加載安卓四大組件,這些類可以本地預制zip或是云端下載。

回到原點思考問題,怎么實現呢?

首先想到的肯定是ClassLoader,那邊安卓平臺的ClassLoader是如何應用呢?可以查看Class源碼,發現安卓平臺SystemClassLoader是PathClassLoader,具體原理看插件化基礎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());
    }

查看安卓系統源碼中Activity加載方式,會發現也是用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;
        }                       

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

 /**
     * 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);
    }

根據注釋,該類可以加載jar/zip/apk等壓縮包里的dex,自己動手寫代碼驗證下。答案肯定是可以的。

PathClassLoader沒有接口可以設置優化后的dex防止地方,默認情況會用dexPath充當,這樣的話會有很多現在那我們想自定義優化類path怎么辦?

看PathClassLoader的父類BasedexClassLoader會發現,它還有個雙胞胎弟弟DexClassLoader,為什么說是雙胞胎呢?應為這兩個類自己都是啥事都沒敢,只是實現接口不太一樣,而DexClassLoader為我們提供了優化后dex緩存path,實用更靈活。

但是網上有很多地方說PathClassLoader類只能加載已經按照的應用類,不能加載外部未按照的類。并且有人說art虛擬機不行和dalvik虛擬機可以。根據自己親自實驗,PathClassLoader也是可以加載成功的,
只是dexOutputPath用了默認的路徑會有些限制,至于網上很多不一樣的說法,個人理解可能不同的虛擬機實現或是不同系統版本可能有兼容性,未找到官方權威說法。

反射一個activity

按照原始問題思路,有了加載壓縮包中dex的ClassLoader,那邊我們動態加載一個dex中的activity,看看能不能啟動一個activity。

1,準備dex包

寫一個簡單的apk,包含一個activity,內部做些簡單的事情。

@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,創建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下的應用私有目錄中,防止別人修改自己的代碼。ClassLoader加載一次最好緩存起來,即加快下次的使用,也解決ClassLoader類隔離問題。

3,反射調用

            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();
            }

然而報錯了

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

這時發現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構造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反射調用,在attach后就有了Context,那我們自己反射的Activity要想有ConText,就要模擬AMS調用方式,構造Context,但是這相當于再寫個系統,不可實現,那怎么辦?

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

形成方案

有了以上分析,我們可以專門寫個主Apk中的Activity,用來處理插件中所需要的變量及資源,也可以調用插件中的部分方法。這樣這個類就變成類一個代理類。

這樣就形成了DL開源項目中的靜態代理方式實現的插件方案。進一步動手代碼實驗,只要activity的每個回調接口都能回調到插件中的activity相同方法,并且插件中的對acitivity的每個設置都能夠回調到主apk中代理類處理,
這個插件方式就可以完美運行,至少針對目前的Activity沒問題。

plugin5.png

設置主插件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設置壓縮包的sourceDir和publicSourceDir.

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

實現總結

我們回調原點來從基礎分析DL項目靜態代理方式實現插件的實現過程,回顧下,發現這種方式是最容易想到,那我們為什么沒有比DL作者【任玉剛】早點想到并實現呢?答案是:“沒有對應的眼界,不夠勤快?!?br> 用玉剛常說的一句好說就是”這個社會還沒到比聰明時代,想進步,就得比別人多用時間“。

靜態代理方式雖然可以實現插件方式,但是用起來還是不方便,接下來我們進一步學習插件化,分析hook系統方法動態代理方式的思想的實現。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容