知識總結 插件化學習 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;
    }

這裏寫圖片描述

雖然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開源項目中的靜態代理方式實現的插件方案。進一步動手代碼實驗,只要activiyt的每個回調接口都能回調到插件中的activity相同方法,並且插件中的對activity的每個設置都能夠回調到主apk中代理類處理,
這個插件方式就可以完美運行,至少針對目前的Activity沒問題。

這裏寫圖片描述

設置主插件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作者【任玉剛】早點想到並實現呢?答案是:“沒有對應的眼界,不夠勤快。”
用玉剛常說的一句好說就是”這個社會還沒到比聰明時代,想進步,就得比別人多用時間“。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章