Small插件化框架源码深入分析

2016-2017年是插件化遍地开花的一年,各家大厂都开源了自己的插件化框架、热修复技术,网上也已经有许多介绍和分析的文章。但是好像对Small源码的深入分析还不多,所以这里凭自己的理解来梳理一下Small整个插件化流程,帮助想使用Small的人来了解这个框架同时也是自己对这个框架知识点的一个巩固,同时文章里的一些术语可能是small专有的,阅读前可以去官网去了解下,最后有疏漏的地方欢迎大家评论拍砖。

Small框架原理简介

Small是Github上一款开源的插件化框架。实现Android插件化的核心技术是:动态加载类、动态加载资源和动态注册组件。
插件化原理:这里就简单的带入一下,因为各家的实现基础原理都是差不多,类的加载通过反射把插件包中的dex插入到BaseDexClassLoaderpathList数组中保证类能够正确被找到,资源也通过反射调用AssetManageraddAssetPaths方法保证资源能够被正确的加载,JNI中的so包也可以通过反射插入到BaseDexClassLoadernativeLibraryDirectories数组中。当然由于用了很多的反射也需要适配的很多不同版本的API,以及各种国内手机厂商ROM,这会在后面详细介绍。
分离插件包的技术:gradle插件,用来实现small中的libapp打包。将在下一篇详细介绍,gradle插件相关源码gradle-small-plugin

Small初始化

Application的构造方法中执行Small.preSetUp(this)。由于插件中所有的ContentProviders必须在宿主中的AndroidManifest中声明,此时它的install过程会抛出ClassNotFoundException,所以必须在这之前捕获这个异常对它进行延时安装。

public static void preSetUp(Application context) {
        if (sContext != null) {
            return;
        }
        sContext = context;

        // Register default bundle launchers
        registerLauncher(new ActivityLauncher());
        registerLauncher(new ApkBundleLauncher());
        registerLauncher(new WebBundleLauncher());
        Bundle.onCreateLaunchers(context);
}

sContext作为一个全局的Application静态变量,通过判断perSetUp在同一个进程中只可能初始化一次,然后可以看到执行了三次registerLauncher方法来把三种不同的BundleLauncher(包括ActivityLauncherApkBundleLauncherWebBundleLauncher)加入到一个ArrayList中。

Small设置setUp

setUp是small功能的主入口,在github的sample代码中可以看到LaunchActivityonStart方法中执行setUp方法。

public static void setUp(Context context, OnCompleteListener listener) {
       if (sContext == null) {
            // Tips for CODE-BREAKING
            throw new UnsupportedOperationException(
                    "Please call `Small.preSetUp' in your application first");
        }

        if (sHasSetUp) {
            if (listener != null) {
                listener.onComplete();
            }
            return;
        }

        Bundle.loadLaunchableBundles(listener);
        sHasSetUp = true;
}

判断是否已经进行过前面的perSetUp步骤,并且在这里setUp完成后会有个标志sHasSetUp确保应用进程存活的情况再次进入应用不会再走一遍setUp的流程。接下来进入Bundle.loadLaunchableBundles(listenner)方法。

/**
  * Load bundles from manifest
*/
protected static void loadLaunchableBundles(Small.OnCompleteListener listener) {
        Context context = Small.getContext();

        boolean synchronous = (listener == null);
        if (synchronous) {
            loadBundles(context);
            return;
        }

        // Asynchronous
        if (sThread == null) {
            sThread = new LoadBundleThread(context);
            sHandler = new LoadBundleHandler(listener);
            sThread.start();
        }
}

small在这里会根据之前setUp中是否有Listener进行同步和异步去加载bundle的区分,没有意外情况一般都会使用异步,因为里面dexload涉及了IO操作会比较耗时。small启动了一个线程去执行相关的加载bundle操作,以及用Handler来回调通知Listener是否已经加载完毕。

Small加载Bundle

根据步骤,先来查看加载Bundle的线程:

private static class LoadBundleThread extends Thread {

        Context mContext;

        public LoadBundleThread(Context context) {
            mContext = context;
        }

        @Override
        public void run() {
            loadBundles(mContext);
            sHandler.obtainMessage(MSG_COMPLETE).sendToTarget();
        }
}

private static class LoadBundleHandler extends Handler {
        private Small.OnCompleteListener mListener;

        public LoadBundleHandler(Small.OnCompleteListener listener) {
            mListener = listener;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_COMPLETE:
                    if (mListener != null) {
                        mListener.onComplete();
                    }
                    mListener = null;
                    sThread = null;
                    sHandler = null;
                    break;
            }
        }
    }

LoadBundleThread中调用了loadBundles(context)和调用LoadBundleHandler发送加载完成的message,通知Listener所有的插件已经加载完毕。接下来看下loadBundles方法:

private static void loadBundles(Context context) {
        JSONObject manifestData;
        try {
            File patchManifestFile = getPatchManifestFile();// 获取bundle.json
            String manifestJson = getCacheManifest();//从SharedPreferences里尝试获取bundle.json
            if (manifestJson != null) {
                // Load from cache and save as patch
                if (!patchManifestFile.exists()) patchManifestFile.createNewFile();
                PrintWriter pw = new PrintWriter(new FileOutputStream(patchManifestFile));
                pw.print(manifestJson);
                pw.flush();
                pw.close();
                // Clear cache
                setCacheManifest(null);
            } else if (patchManifestFile.exists()) {
                // Load from patch
                BufferedReader br = new BufferedReader(new FileReader(patchManifestFile));
                StringBuilder sb = new StringBuilder();
                String line;
                while ((line = br.readLine()) != null) {
                    sb.append(line);
                }

                br.close();
                manifestJson = sb.toString();
            } else {
                // Load from built-in `assets/bundle.json'
                InputStream builtinManifestStream = context.getAssets().open(BUNDLE_MANIFEST_NAME);
                int builtinSize = builtinManifestStream.available();
                byte[] buffer = new byte[builtinSize];
                builtinManifestStream.read(buffer);
                builtinManifestStream.close();
                manifestJson = new String(buffer, 0, builtinSize);
            }

            // Parse manifest file
            manifestData = new JSONObject(manifestJson);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        Manifest manifest = parseManifest(manifestData);
        if (manifest == null) return;

        setupLaunchers(context);

        loadBundles(manifest.bundles);
}

先通过getPatchManifestFile方法得到读取补丁的patch所在的目录下bundle.json文件。然后这里有一系列的判断,优先读取缓存中的bundle.json并且覆盖patch中的,其次优先从patch读取文件,最后才会从工程目录中的assets中读取,第一次安装应该都是从assets中读取。接着读取到bundle.json文件转换成Json对象然后解析把所有的Bundle插件包加载到List中传给loadBundles(List<Bundle>)方法。在这里加载Bundle之前会先执行setupLaunchers(Context)方法,它会根据之前注册的BundleLauncher顺序来执行各自的setUp方法,最后执行加载所有在json文件中列出的Bundles。

    private static void loadBundles(List<Bundle> bundles) {
        sPreloadBundles = bundles;

        // Prepare bundle
        // 遍历所有插件执行prepareForLaunch阶段后面会介绍
        for (Bundle bundle : bundles) {
            bundle.prepareForLaunch();
        }

        // Handle I/O
        if (sIOActions != null) {
            //创建一个固定线程数量的线程池,用来执行插件中Dex的加载
            ExecutorService executor = Executors.newFixedThreadPool(sIOActions.size());
            for (Runnable action : sIOActions) {
                executor.execute(action);
            }
            executor.shutdown();
            try {
                if (!executor.awaitTermination(LOADING_TIMEOUT_MINUTES, TimeUnit.MINUTES)) {
                    throw new RuntimeException("Failed to load bundles! (TIMEOUT > "
                            + LOADING_TIMEOUT_MINUTES + "minutes)");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sIOActions = null;
        }

        // Wait for the things to be done on UI thread before `postSetUp`,
        // as on 7.0+ we should wait a WebView been initialized. (#347)
        while (sRunningUIActionCount != 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // Notify `postSetUp' to all launchers
        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.postSetUp();
        }

        // Wait for the things to be done on UI thread after `postSetUp`,
        // like creating a bundle application.
        while (sRunningUIActionCount != 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // Free all unused temporary variables
        for (Bundle bundle : bundles) {
            if (bundle.parser != null) {
                bundle.parser.close();
                bundle.parser = null;
            }
            bundle.mBuiltinFile = null;
            bundle.mExtractPath = null;
        }
    }
    
    protected void prepareForLaunch() {
        if (mIntent != null) return;

        if (mApplicableLauncher == null && sBundleLaunchers != null) {
            for (BundleLauncher launcher : sBundleLaunchers) {
                if (launcher.resolveBundle(this)) {
                    mApplicableLauncher = launcher;
                    break;
                }
            }
        }
    }

这里先看下他们的父类一个抽象类BundleLauncher,作者已经在类的开头注释中介绍的比较详细了这里简单梳理下它的lifecycle:

  • onCreate:在初始化Application之前hook应用中的一些方法和变量,由于调用会在Application的onCreate之前所以里面的执行方法必须比较轻量,以免产出ANR
  • setUp :做一些静态的初始化工作比如Launcher的初始化,准备需要加载的bundle
  • preloadBundle:处理bundle的校验和相关准备工作,验证包名、签名并收集bundle的version以便以后upgrade
  • loadBundle:对通过校验的bundle开始真正开始加载
  • postSetUp:在加载bundle之后对把所有插件之中的Dex、Resource和NativeLib的信息,通过反射保证Bundle的内容正常的加入到应用中。
  • prelaunchBundle:当使用small.openUri来启动插件中的Activity前解析它的class和query参数。
  • launchBundle:在prelaunchBundle通过后,真正的去启动Activity
    接着我们就可以很轻松的理解它的三个子类:

ActivityLauncher.java

这个类主要用于启动宿主Host中的Activity,或者是定义在bundle.json中没有指定pkg字段的以及pkg自定为main的bundle,当启动Activity的时候会取bundle的uri作为ClassName。这个子类并没有onCreate阶段直接从setUp开始

@Override
    public void setUp(Context context) {
        super.setUp(context);

        // Read the registered classes in host's manifest file
        PackageInfo pi;
        try {
            pi = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), PackageManager.GET_ACTIVITIES);
        } catch (PackageManager.NameNotFoundException ignored) {
            // Never reach
            return;
        }
        ActivityInfo[] as = pi.activities;
        if (as != null) {
            sActivityClasses = new HashSet<String>();
            for (ActivityInfo ai : as) {
                sActivityClasses.add(ai.name);
            }
        }
    }

这里主要做的就是读取我们宿主app的manifest文件中activity的name字段并存储到一个HashSet里面,为后面的判断activity作用。然后是preloadBundle阶段

@Override
public boolean preloadBundle(Bundle bundle) {
        if (sActivityClasses == null) return false;

        String pkg = bundle.getPackageName();
        return (pkg == null || pkg.equals("main"));
}

通过传入的bundle取出pkg字段,如前面类功能描述一致如果字段为空或者是main则返回true保证之后的resolveBundle可以执行load操作,插件中的Activity不会出现pkg字段为空或者main的情况所以这里一般都会返回false,同时resolveBundle也不会去执行load操作。当我们调用Small.openUri来启动Activity的时候会执行launchBundle其中的prelaunchBundle方法

@Override
public void prelaunchBundle(Bundle bundle) {
        super.prelaunchBundle(bundle);
        Intent intent = new Intent();
        bundle.setIntent(intent);

        // Intent extras - class
        String activityName = bundle.getActivityName();
        if (!sActivityClasses.contains(activityName)) {
            if (activityName.endsWith("Activity")) {
                throw new ActivityNotFoundException("Unable to find explicit activity class " +
                        "{ " + activityName + " }");
            }

            String tempActivityName = activityName + "Activity";
            if (!sActivityClasses.contains(tempActivityName)) {
                throw new ActivityNotFoundException("Unable to find explicit activity class " +
                        "{ " + activityName + "(Activity) }");
            }

            activityName = tempActivityName;
        }
        intent.setComponent(new ComponentName(Small.getContext(), activityName));

        // Intent extras - params
        String query = bundle.getQuery();
        if (query != null) {
            intent.putExtra(Small.KEY_QUERY, '?'+query);
        }
}

在这里我们能通过bundle.getActivityName获取bundle的入口Activity类名(如果uri为空默认取MainActivity)然后就是从之前收集的Activity的HashSet中查询是否包含了这个类同时设置Component和query条件。

ApkBundleLauncher.java

Small的核心代码都在这个类中,这个是Small实现插件化相关Hook的关键类,包括劫持Instrumentation借壳启动Activity,加载插件dex、资源、jniLibs等。

onCreate阶段

ApkBundleLauncher中实现了onCreate方法

@Override
public void onCreate(Application app) {
        super.onCreate(app);

        Object/*ActivityThread*/ thread;
        List<ProviderInfo> providers;
        Instrumentation base;
        ApkBundleLauncher.InstrumentationWrapper wrapper;
        Field f;

        // Get activity thread
        thread = ReflectAccelerator.getActivityThread(app);

        // Replace instrumentation
        try {
            f = thread.getClass().getDeclaredField("mInstrumentation");
            f.setAccessible(true);
            base = (Instrumentation) f.get(thread);
            wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
            f.set(thread, wrapper);
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
        }

        // Inject message handler
        ensureInjectMessageHandler(thread);

        // Get providers
        try {
            f = thread.getClass().getDeclaredField("mBoundApplication");
            f.setAccessible(true);
            Object/*AppBindData*/ data = f.get(thread);
            f = data.getClass().getDeclaredField("providers");
            f.setAccessible(true);
            providers = (List<ProviderInfo>) f.get(data);
        } catch (Exception e) {
            throw new RuntimeException("Failed to get providers from thread: " + thread);
        }

        sActivityThread = thread;
        sProviders = providers;
        sHostInstrumentation = base;
        sBundleInstrumentation = wrapper;
}

private static void ensureInjectMessageHandler(Object thread) {
        try {
            Field f = thread.getClass().getDeclaredField("mH");
            f.setAccessible(true);
            Handler ah = (Handler) f.get(thread);
            f = Handler.class.getDeclaredField("mCallback");
            f.setAccessible(true);

            boolean needsInject = false;
            if (sActivityThreadHandlerCallback == null) {
                needsInject = true;
            } else {
                Object callback = f.get(ah);
                if (callback != sActivityThreadHandlerCallback) {
                    needsInject = true;
                }
            }

            if (needsInject) {
                // Inject message handler
                sActivityThreadHandlerCallback = new ActivityThreadHandlerCallback();
                f.set(ah, sActivityThreadHandlerCallback);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace message handler for thread: " + thread);
        }
    }

通过从ActivityThread中反射取到Instrumentation替换成我们自己的InstrumentationWrapper来控制Activity的各个生命周期以及启动。拦截系统ActivityThread中的Handler变量mH,在ActivityThreadHandlerCallback中通过handle指定的msg.what来处理LAUNCH_ACTIVITYCREATE_SERVICE。 同时也获取所有注册过的Providers用于后面的异常捕获然后延时安装。

接着先来看下Small是如何通过预注册一批虚拟Activity来确保插件中的Activity能够有正常的生命周期,其中控制生命周期和启动的Instrumentation会被InstrumentationWrapper反射替换,由于代码量比较多所以拆分各个小段来分析:

InstrumentationWrapper

替换系统的Instrumentation控制Activity的启动和它的生命周期方法,拦截Exception

(1).用宿主中预注册的STUB Activity替换真正启动的Activity。
/** @Override V21+
         * Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode, android.os.Bundle options) {
     wrapIntent(intent);
     ensureInjectMessageHandler(sActivityThread);
     return ReflectAccelerator.execStartActivity(mBase,
                    who, contextThread, token, target, intent, requestCode, options);
}

/** @Override V20-
         * Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode) {
     wrapIntent(intent);
     ensureInjectMessageHandler(sActivityThread);
     return ReflectAccelerator.execStartActivity(mBase,
                    who, contextThread, token, target, intent, requestCode);
}

这里有API版本区别21以上和20以下中的execStartActivity方法参数多了一个。然后是wrapIntent()方法。

        private void wrapIntent(Intent intent) {
            ComponentName component = intent.getComponent();
            String realClazz;
            if (component == null) {
                // Try to resolve the implicit action which has registered in host.
                component = intent.resolveActivity(Small.getContext().getPackageManager());
                if (component != null) {
                    // A system or host action, nothing to be done.
                    return;
                }

                // Try to resolve the implicit action which has registered in bundles.
                realClazz = resolveActivity(intent);
                if (realClazz == null) {
                    // Cannot resolved, nothing to be done.
                    return;
                }
            } else {
                realClazz = component.getClassName();
                if (realClazz.startsWith(STUB_ACTIVITY_PREFIX)) {
                    // Re-wrap to ensure the launch mode works.
                    realClazz = unwrapIntent(intent);
                }
            }

            if (sLoadedActivities == null) return;

            ActivityInfo ai = sLoadedActivities.get(realClazz);
            if (ai == null) return;

            // Carry the real(plugin) class for incoming `newActivity' method.
            intent.addCategory(REDIRECT_FLAG + realClazz);
            String stubClazz = dequeueStubActivity(ai, realClazz);
            intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
        }

首先拿到真正需要启动的ActivityIntent中的ComponentName来获取到其ClassName,通过判断如果是隐式Intent然后去宿主的Manifest中寻找对应注册的Action或者它是一个系统级别的Action,small不做处理直接返回。假如Intent注册在插件中调用resolveActivity()方法,遍历插件中所有所有的IntentFilter的一个集合,取到与之匹配自定义Action的ClassName并返回。如果是显式Intent判断是否启动的是真正的Activity通过unwrapIntent()方法确保启动的能够真正替换Activity成功。
然后通过ClassName找到对应的ActivityInfo,这里small会把ClassName存在Intentcategory中为了后面可以方便取出做一些逻辑判断包括前面提到的unwrapIntent()方法会用到这个。
接着通过dequeueStubActivity()方法获取一个可用的STUB Activity作为真正需要启动Activity的载体来绕过系统注册机制,并且把stub的ClassName设置给component,small会用一套预注册的STUB Activitys来保证所有的启动模式都被覆盖到,并且除了standard模式每组都有4个Activity来复用。

<!-- Small中的AndroidManifest -->
<application>
        <!-- Stub Activities -->
        <!-- 1 standard mode -->
        <activity android:name=".A" android:launchMode="standard"/>
        <activity android:name=".A1" android:theme="@android:style/Theme.Translucent"/>
        <!-- 4 singleTop mode -->
        <activity android:name=".A10" android:launchMode="singleTop"/>
        <activity android:name=".A11" android:launchMode="singleTop"/>
        <activity android:name=".A12" android:launchMode="singleTop"/>
        <activity android:name=".A13" android:launchMode="singleTop"/>
        <!-- 4 singleTask mode -->
        <activity android:name=".A20" android:launchMode="singleTask"/>
        <activity android:name=".A21" android:launchMode="singleTask"/>
        <activity android:name=".A22" android:launchMode="singleTask"/>
        <activity android:name=".A23" android:launchMode="singleTask"/>
        <!-- 4 singleInstance mode -->
        <activity android:name=".A30" android:launchMode="singleInstance"/>
        <activity android:name=".A31" android:launchMode="singleInstance"/>
        <activity android:name=".A32" android:launchMode="singleInstance"/>
        <activity android:name=".A33" android:launchMode="singleInstance"/>

        <!-- Web Activity -->
        <activity android:name=".webkit.WebActivity"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="stateHidden|adjustPan"
            android:hardwareAccelerated="true"/>
    </application>
(2).在所有Activity的onCreate、onStop、onDestory生命周期都进行了hook。

先来看callActivityOnCreate:

        @Override
        /** Prepare resources for REAL */
        public void callActivityOnCreate(Activity activity, android.os.Bundle icicle) {
            do {
                if (sLoadedActivities == null) break;
                ActivityInfo ai = sLoadedActivities.get(activity.getClass().getName());
                if (ai == null) break;

                applyActivityInfo(activity, ai);
                ReflectAccelerator.ensureCacheResources();
            } while (false);
            sHostInstrumentation.callActivityOnCreate(activity, icicle);

            // Reset activity instrumentation if it was modified by some other applications #245
            if (sBundleInstrumentation != null) {
                try {
                    Field f = Activity.class.getDeclaredField("mInstrumentation");
                    f.setAccessible(true);
                    Object instrumentation = f.get(activity);
                    if (instrumentation != sBundleInstrumentation) {
                        f.set(activity, sBundleInstrumentation);
                    }
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

small会在这里会把插件中启动的Activity相关ActivityInfo中的softInputModescreenOrientation的通过代码进行设置,在API24及以上系统有时会把保存Resource的WeakReference释放导致资源不可用,所以还需要把Resource的缓存再次存入WeakReference。最后确保hook的instrumentation没有在这个阶段被其他应用修改,再一次进行反射赋值。
再看callActivityOnStop:

        @Override
        public void callActivityOnStop(Activity activity) {
            sHostInstrumentation.callActivityOnStop(activity);

            if (!Small.isUpgrading()) return;

            // If is upgrading, we are going to kill self while application turn into background,
            // and while we are back to foreground, all the things(code & layout) will be reload.
            // Don't worry about the data missing in current activity, you can do all the backups
            // with your activity's `onSaveInstanceState' and `onRestoreInstanceState'.

            // Get all the processes of device (1)
            ActivityManager am = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
            List<RunningAppProcessInfo> processes = am.getRunningAppProcesses();
            if (processes == null) return;

            // Gather all the processes of current application (2)
            // Above 5.1.1, this may be equals to (1), on the safe side, we also
            // filter the processes with current package name.
            String pkg = activity.getApplicationContext().getPackageName();
            final List<RunningAppProcessInfo> currentAppProcesses = new ArrayList<>(processes.size());
            for (RunningAppProcessInfo p : processes) {
                if (p.pkgList == null) continue;

                boolean match = false;
                int N = p.pkgList.length;
                for (int i = 0; i < N; i++) {
                    if (p.pkgList[i].equals(pkg)) {
                        match = true;
                        break;
                    }
                }
                if (!match) continue;

                currentAppProcesses.add(p);
            }
            if (currentAppProcesses.isEmpty()) return;

            // The top process of current application processes.
            RunningAppProcessInfo currentProcess = currentAppProcesses.get(0);
            if (currentProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) return;

            // Seems should delay some time to ensure the activity can be successfully
            // restarted after the application restart.
            // FIXME: remove following thread if you find the better place to `killProcess'
            new Thread() {
                @Override
                public void run() {
                    try {
                        sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (RunningAppProcessInfo p : currentAppProcesses) {
                        android.os.Process.killProcess(p.pid);
                    }
                }
            }.start();
        }

作者已经给了详细的注释,主要为插件升级做一些相关处理,在Activity的onStop阶段并且应用退到了后台,当发现有插件升级时杀死自己的进程,保证用户再次进入应用时可以走setUp逻辑保证更新的bundle能够被正确加载。最后callActivityOnDestroy:

        @Override
        public void callActivityOnDestroy(Activity activity) {
            do {
                if (sLoadedActivities == null) break;
                String realClazz = activity.getClass().getName();
                ActivityInfo ai = sLoadedActivities.get(realClazz);
                if (ai == null) break;
                inqueueStubActivity(ai, realClazz);
            } while (false);
            sHostInstrumentation.callActivityOnDestroy(activity);
        }

在这里如果插件启动的Activity不是LAUNCH_MULTIPLE那么small会通过调用inqueueStubActivity获取当前被使用的StubActivity进行释放以便后面的复用。

(3).最后对异常的捕获也做了处理onException:
        @Override
        public boolean onException(Object obj, Throwable e) {
            if (sProviders != null && e.getClass().equals(ClassNotFoundException.class)) {
                boolean errorOnInstallProvider = false;
                StackTraceElement[] stacks = e.getStackTrace();
                for (StackTraceElement st : stacks) {
                    if (st.getMethodName().equals("installProvider")) {
                        //拦截到installProvider方法中抛出的ClassNotFoundException
                        errorOnInstallProvider = true;
                        break;
                    }
                }

                if (errorOnInstallProvider) {
                    // We'll reinstall this content provider later, so just ignores it!!!
                    // FIXME: any better way to get the class name?
                    String msg = e.getMessage();
                    final String prefix = "Didn't find class \"";
                    if (msg.startsWith(prefix)) {
                        String providerClazz = msg.substring(prefix.length());
                        //获取到异常中的className
                        providerClazz = providerClazz.substring(0, providerClazz.indexOf("\""));
                        for (ProviderInfo info : sProviders) {
                            //对比之前收集Manifest中的providers
                            if (info.name.equals(providerClazz)) {
                                if (mLazyInitProviders == null) {
                                    mLazyInitProviders = new ArrayList<ProviderInfo>();
                                }
                                //把匹配的provider加入到队列,等待后面延时去安装
                                mLazyInitProviders.add(info);
                                break;
                            }
                        }
                    }
                    return true;
                }
            }

            return super.onException(obj, e);
        }

由于Small需要把插件包中的所有provider预先注册在宿主的AndroidManifest中,但是系统会在ApplicationonCreate生命周期执行之前根据注册的provider信息进行install,这样会导致类找不到而抛出异常,所以这里通过onException拦截当前的ClassNotFoundException并且判断如果是从方法installProvider中抛出的话我们进行捕获处理不抛出异常,把异常中的provider也就是在宿主中定义的其他插件包中的provider的类名存进数组,在后续加载完毕插件的dex和处理完classloader之后再进行install操作。

ActivityThreadHandlerCallback

通过反射在ActivityTread中拿到主线程的HandlermH,对ActivityServiceBroadcastReceiverProvider的相关启动接受做一些处理。

    private static class ActivityThreadHandlerCallback implements Handler.Callback {

        private static final int LAUNCH_ACTIVITY = 100;//对应ActivityThread源码中的msg.what
        private static final int CREATE_SERVICE = 114;//创建service的msg.what

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case LAUNCH_ACTIVITY:
                    redirectActivity(msg);
                    break;

                case CREATE_SERVICE:
                    ensureServiceClassesLoadable(msg);
                    break;

                default:
                    break;
            }

            return false;
        }
        /** 通过这个方法我们替换之前的stubActivity变成realActivity**/
        private void redirectActivity(Message msg) {
            Object/*ActivityClientRecord*/ r = msg.obj;
            Intent intent = ReflectAccelerator.getIntent(r);
            /** unwrapIntent方法会取到之前在InstrumentationWrapper中wrapperIntent时
            在intent.category存入的realActivity_class **/
            String targetClass = unwrapIntent(intent);
            boolean hasSetUp = Small.hasSetUp();
            if (targetClass == null) {
                // The activity was register in the host.
                if (hasSetUp) return; // nothing to do

                if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
                    // The launcher activity will setup Small.
                    return;
                }

                // Launching an activity in remote process. Set up Small for it.
                Small.setUpOnDemand();
                return;
            }

            if (!hasSetUp) {
                // Restarting an activity after application recreated,
                // maybe upgrading or somehow the application was killed in background.
                Small.setUp();
            }

            // Replace with the REAL activityInfo
            ActivityInfo targetInfo = sLoadedActivities.get(targetClass);
            ReflectAccelerator.setActivityInfo(r, targetInfo);
        }

        private void ensureServiceClassesLoadable(Message msg) {
            // Cause Small is only setup in current application process, if a service is specified
            // with a different process('android:process=xx'), then we should also setup Small for
            // that process so that the service classes can be successfully loaded.
            Small.setUpOnDemand();
        }
    }

Small拦截了启动Activity和创建Service的Handler回调,通过redirectActivity把StubActivity替换成我们真正需要启动的Activity,ensureServiceClassesLoadable则保证在其他进程中的Service能够正确启动。

SetUp阶段

接下来是setUp阶段:

    @Override
    public void setUp(Context context) {
        super.setUp(context);

        Field f;

        // AOP for pending intent
        try {
            f = TaskStackBuilder.class.getDeclaredField("IMPL");
            f.setAccessible(true);
            final Object impl = f.get(TaskStackBuilder.class);
            InvocationHandler aop = new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Intent[] intents = (Intent[]) args[1];
                    //拿到所有的Intent,进行stubActivity替换
                    for (Intent intent : intents) {
                        sBundleInstrumentation.wrapIntent(intent);
                        intent.setAction(Intent.ACTION_MAIN);
                        intent.addCategory(Intent.CATEGORY_LAUNCHER);
                    }
                    return method.invoke(impl, args);
                }
            };
            //进行动态代理
            Object newImpl = Proxy.newProxyInstance(context.getClassLoader(), impl.getClass().getInterfaces(), aop);
            f.set(TaskStackBuilder.class, newImpl);
        } catch (Exception ignored) {
            ignored.printStackTrace();
        }
    }

通过反射和动态代理处理所有通过TaskStackBuilder创建的pendingIntent,进行warpIntent用StubActivity替换RealActivity。如果没有使用TaskStackBuilder创建PendingIntent,那么需要对Intent执行Small.wrapIntent(Intent)

preloadBundle阶段

preloadBundle是通过父类SoBundleLauncher实现的

    @Override
    public boolean preloadBundle(Bundle bundle) {
        String packageName = bundle.getPackageName();
        if (packageName == null) return false;

        // 1.Check if supporting
        String[] types = getSupportingTypes();
        if (types == null) return false;

        boolean supporting = false;
        String bundleType = bundle.getType();
        if (bundleType != null) {
            // Consider user-defined type in `bundle.json'
            for (String type : types) {
                if (type.equals(bundleType)) {
                    supporting = true;
                    break;
                }
            }
        } else {
            // Consider explicit type specify in package name as following:
            //  - com.example.[type].any
            //  - com.example.[type]any
            String[] pkgs = packageName.split("\\.");
            int N = pkgs.length;
            String aloneType = N > 1 ? pkgs[N - 2] : null;
            String lastComponent = pkgs[N - 1];
            for (String type : types) {
                if ((aloneType != null && aloneType.equals(type))
                        || lastComponent.startsWith(type)) {
                    supporting = true;
                    break;
                }
            }
        }
        if (!supporting) return false;

        // 2.Initialize the extract path
        File extractPath = getExtractPath(bundle);
        if (extractPath != null) {
            if (!extractPath.exists()) {
                extractPath.mkdirs();
            }
            bundle.setExtractPath(extractPath);
        }

        // 3.Select the bundle entry-point, `built-in' or `patch'
        File plugin = bundle.getBuiltinFile();
        BundleParser parser = BundleParser.parsePackage(plugin, packageName);
        File patch = bundle.getPatchFile();
        BundleParser patchParser = BundleParser.parsePackage(patch, packageName);
        if (parser == null) {
            if (patchParser == null) {
                return false;
            } else {
                parser = patchParser; // use patch
                plugin = patch;
            }
        } else if (patchParser != null) {
            if (patchParser.getPackageInfo().versionCode <= parser.getPackageInfo().versionCode) {
                Log.d(TAG, "Patch file should be later than built-in!");
                patch.delete();
            } else {
                parser = patchParser; // use patch
                plugin = patch;
            }
        }
        bundle.setParser(parser);

        // 4.Check if the plugin has not been modified
        long lastModified = plugin.lastModified();
        long savedLastModified = Small.getBundleLastModified(packageName);
        if (savedLastModified != lastModified) {
            // If modified, verify (and extract) each file entry for the bundle
            if (!parser.verifyAndExtract(bundle, this)) {
                bundle.setEnabled(false);
                return true; // Got it, but disabled
            }
            Small.setBundleLastModified(packageName, lastModified);
        }

        // 5.Record version code for upgrade
        PackageInfo pluginInfo = parser.getPackageInfo();
        bundle.setVersionCode(pluginInfo.versionCode);
        bundle.setVersionName(pluginInfo.versionName);

        return true;
    }

跟着作者的注释看可以比较清晰的了解这里主要工作就是:

  1. 检测Bundle的类型,默认是applib类型
  2. 初始化NativeLib的解压目录
  3. 查看Bundle是从本地插件包中加载还是从服务端下发的path Bundle中加载
  4. 检测Bundle是否被覆盖修改过,如果被修改则对内容进行校验和解压
  5. 收集Bundle的version便于以后插件包单独更新

loadBundle阶段

进过前面的一些列初始化和校验,终于开始加载Bundle插件包

    @Override
    public void loadBundle(Bundle bundle) {
        String packageName = bundle.getPackageName();

        BundleParser parser = bundle.getParser();
        //收集Bundle中Manifest所有注册的Activitys
        parser.collectActivities();
        PackageInfo pluginInfo = parser.getPackageInfo();

        // Load the bundle
        String apkPath = parser.getSourcePath();
        if (sLoadedApks == null) sLoadedApks = new ConcurrentHashMap<String, LoadedApk>();
        LoadedApk apk = sLoadedApks.get(packageName);
        if (apk == null) {
            //实例化我们的LoadApk 里面存放一些插件的信息
            apk = new LoadedApk();
            apk.packageName = packageName;
            apk.path = apkPath;
            apk.nonResources = parser.isNonResources();
            if (pluginInfo.applicationInfo != null) {
                apk.applicationName = pluginInfo.applicationInfo.className;
            }
            apk.packagePath = bundle.getExtractPath();
            apk.optDexFile = new File(apk.packagePath, FILE_DEX);

            // Load dex
            final LoadedApk fApk = apk;
            //创建一个线程来进行loadDex,这个里涉及到dexOpt所以会比较耗时
            Bundle.postIO(new Runnable() {
                @Override
                public void run() {
                    try {
                        fApk.dexFile = DexFile.loadDex(fApk.path, fApk.optDexFile.getPath(), 0);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            // Extract native libraries with specify ABI
            // 设置NativeLibrary存放路径
            String libDir = parser.getLibraryDirectory();
            if (libDir != null) {
                apk.libraryPath = new File(apk.packagePath, libDir);
            }
            sLoadedApks.put(packageName, apk);
        }

        if (pluginInfo.activities == null) {
            bundle.setLaunchable(false);
            return;
        }

        // Record activities for intent redirection
        // 收集Bundle中的Activity为了之后能正确的替换StubActivity
        if (sLoadedActivities == null) sLoadedActivities = new ConcurrentHashMap<String, ActivityInfo>();
        for (ActivityInfo ai : pluginInfo.activities) {
            sLoadedActivities.put(ai.name, ai);
        }

        // Record intent-filters for implicit action
        // 收集隐式启动的intent-filter 
        ConcurrentHashMap<String, List<IntentFilter>> filters = parser.getIntentFilters();
        if (filters != null) {
            if (sLoadedIntentFilters == null) {
                sLoadedIntentFilters = new ConcurrentHashMap<String, List<IntentFilter>>();
            }
            sLoadedIntentFilters.putAll(filters);
        }

        // Set entrance activity
        bundle.setEntrance(parser.getDefaultActivityName());
    }

根据Bundle配置的信息进行collectActivities也就是读取你插件中manifest中的activity注册信息(包括packagename、name、theme、label、icon、launchermode、screenorientation、windowsoftinputmode以及hardwareaccelerate),还有intent-filter的信息。然后设置入口activity也就是LauncherActivity。这里最重要的就是LoadDexBundle.postIO中把所有Bundle的LoadDex操作都放到了一个Runnable队列sIOActions中,这些任务action会在一个线程池中并行执行。然后收集Bundle的Activity、intent-filter,设置插件默认启动Activity。

postSetUp阶段

当Bundle加载完毕之后会执行postSetUp方法,把所有Bundle中的Dex、Resource和NativeLib通过反射Merge到宿主中。

    @Override
    public void postSetUp() {
        super.postSetUp();

        if (sLoadedApks == null) {
            Log.e(TAG, "Could not find any APK bundles!");
            return;
        }

        Collection<LoadedApk> apks = sLoadedApks.values();

        // Merge all the resources in bundles and replace the host one
        // 合并所有Bundle中的Resource
        final Application app = Small.getContext();
        String[] paths = new String[apks.size() + 1];
        paths[0] = app.getPackageResourcePath(); // add host asset path
        int i = 1;
        for (LoadedApk apk : apks) {
            if (apk.nonResources) continue; // ignores the empty entry to fix #62
            paths[i++] = apk.path; // add plugin asset path
        }
        if (i != paths.length) {
            paths = Arrays.copyOf(paths, i);
        }
        ReflectAccelerator.mergeResources(app, sActivityThread, paths);

        // Merge all the dex into host's class loader
        // 合并所有Bundle的Dex到宿主的ClassLoader中保证类都能被找到
        ClassLoader cl = app.getClassLoader();
        i = 0;
        int N = apks.size();
        String[] dexPaths = new String[N];
        DexFile[] dexFiles = new DexFile[N];
        for (LoadedApk apk : apks) {
            dexPaths[i] = apk.path;
            dexFiles[i] = apk.dexFile;
            if (Small.getBundleUpgraded(apk.packageName)) {
                // 如果是更新插件,删除optDex文件,保证重新生成
                if (apk.optDexFile.exists()) apk.optDexFile.delete();
                Small.setBundleUpgraded(apk.packageName, false);
            }
            i++;
        }
        ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);

        // Expand the native library directories for host class loader if plugin has any JNIs. (#79)
        // 合并所有Bundle的NativeLibrary
        List<File> libPathList = new ArrayList<File>();
        for (LoadedApk apk : apks) {
            if (apk.libraryPath != null) {
                libPathList.add(apk.libraryPath);
            }
        }
        if (libPathList.size() > 0) {
            ReflectAccelerator.expandNativeLibraryDirectories(cl, libPathList);
        }

        // Trigger all the bundle application `onCreate' event
        // 触发所有app.*中所有的Application onCreate方法
        for (final LoadedApk apk : apks) {
            String bundleApplicationName = apk.applicationName;
            if (bundleApplicationName == null) continue;

            try {
                final Class applicationClass = Class.forName(bundleApplicationName);
                //在线程池中新建一个线程来执行application oncreate方法
                Bundle.postUI(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            BundleApplicationContext appContext = new BundleApplicationContext(app, apk);
                            Application bundleApplication = Instrumentation.newApplication(
                                    applicationClass, appContext);
                            sHostInstrumentation.callApplicationOnCreate(bundleApplication);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // Lazy init content providers
        // 因为所有插件的Dex已经加载完毕,延时安装之前在异常中捕获的所有providers
        if (mLazyInitProviders != null) {
            try {
                Method m = sActivityThread.getClass().getDeclaredMethod(
                        "installContentProviders", Context.class, List.class);
                m.setAccessible(true);
                m.invoke(sActivityThread, app, mLazyInitProviders);
            } catch (Exception e) {
                throw new RuntimeException("Failed to lazy init content providers: " + mLazyInitProviders);
            }
        }

        // Free temporary variables
        sLoadedApks = null;
        sProviders = null;
        sActivityThread = null;
    }

这里会把之前所有的bundle的Resource、Dex以及可能存在的NativeLibrary都merge到宿主中。然后通过调用Instrumentation.callApplicationOnCreate触发所有bundleapplicationoncreate方法,之前收集的Provider也会在这里进行延时安装。

AssetBundleLauncher

这个BundleLauncher加载在bundle.json中非applib标记的插件包类型,可以是一个只负责加载assets的插件包,它的子类WebBundleLauncher就是负责加载Small中的WebActivity的。执行过程只有prelaunchBundleloadBundlelaunchBundle阶段,里面没有反射和Hook逻辑比较简单代码就不贴了。

Small更新插件

Sample中有详细的checkUpgrade方法里面代码比较简单详细,这里就不展开了,需要结合项目的就是下载下来插件的安全性校验了防止被恶意替换。

总结

优点: 轻量级框架;作者和使用者在持续维护中,整体代码非常简洁易读,结构也很清晰;对原有项目(已经组件化的话)侵入非常少只需要使用它的规范命名项目名称再配置一下bundle.json文件即可。

缺点: 作为一个比较新的开源项目,坑肯定是不少的,需要在实际项目中去踩坑;没有插件延时加载的功能,需要在第一次安装时加载全部的插件包(虽然做了并行加载,如果想要再加速启动速度需要自己去实现延时加载);service无法动态加载需要自己实现;在一些插件之间资源依赖上有一定的限制(下面有提到)

Small的插件化的建议

贴下作者在issue中写的,对于像使用small进行插件化的同学请仔细阅读

基本原则

宿主中不要放业务逻辑。只做加载插件以及调起主插件的操作。

重构步骤

  1. 拆lib.* - 公共模块插件

把各个 第三方库 拆出来做成一个个lib.*插件模块,包括统计、地图、网络、图片等库。
把老项目积累的 业务公共代码 (utils)分离出来封装成一个lib.utils插件
把基础的样式、主题分离出来封装成一个lib.style插件

  1. 拆app.* - 业务模块插件

把业务模块拆成app.模块,他们可以依赖lib.模块,显示调用lib.*中的各个API
相对独立的业务模块先拆,比如“详情页”、“关于我们”,如果剩下的业务不好拆,先放一个插件里
如果都不好拆,先把全部业务做成一个app.main主插件

  1. 下沉宿主 - 宿主分身模块

宿主分身模块要求以app+* 格式命名,他们将被宿主lib.*app.*模块自动依赖并允许这些模块透明地访问自己的代码或者资源。需要注意的是,分身模块最终是并入到宿主的而非插件,建议使用分身的情形有:

  • 必须在宿主占坑的manifest,包括:
    • 受限 Activity:
      • 包含了暂不支持的属性:process, configChanges 等
      • 可能使用 FLAG_ACTIVITY_CLEAR_TOP 标签来启动 (#415)
      • 任何 Provider, Service, BroadcastReceiver
  • 必须在宿主占坑的资源,包括:
    • 转场动画
    • 通知栏图标、自定义视图
    • 桌面快捷方式图标
    • 确信稳定的公共库与资源
  1. Sample示例

lib.style - 公共主题
lib.utils - 公共API、公共控件
lib.analytics - 第三方统计
app.main - 主插件
app.home - 首页
app.detail - 详情页
app+stub - 占坑组件、资源

更多技术文章同步在本人Blog,刚开始整理到这个Blog后续会更新更多内容。同时欢迎加入Small的技术讨论群(374601844)和加入开发

关于转载

欢迎转载此文,但是请务必在转载时加上原文作者&原文链接。谢谢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容