Android插件化之Small框架原理解析

Small框架原理简介

Small是Github上一款开源的插件化框架。实现Android插件化的核心技术是:动态加载类、动态加载资源和动态注册组件。

插件化原理:

类的加载通过反射把插件中的dex加载到BaseDexClassLoaderPathList数组中保证类能够正确被找到,资源也是通过反射调用AssetManageraddAssetPaths方法保证资源能够被正确的加载,JNI中对so包也是可以通过反射被插入到BaseDexClassLoadernativeLibraryDirectories数组中,当然用了很多反射也需要适配很多不同版本的API。<br />BaseDexClassLoader(插件中的dex)<br />addAssetPaths(资源中的dex)<br />nativeLibraryDirectories(JNI中的so文件)

image.png

分离插件包的技术:

gradle插件,用来实现small中的libapp打包。

<a name="5rj99"></a>

整体架构

Small里面比较核心的类有下面三个:

  • Small:接口类,提供用户能使用的各类接口
  • Bundle:代表插件类,保存了插件的全部信息
  • BundleLauncher:插件加载类,根据加载的不同插件类型,有多个子类,如下图:

[图片上传失败...(image-a18670-1574245903275)]
<a name="GJFuC"></a>

初始化

先来看一下宿主 App 中的初始化部分,主要在 ApplicationLaunchActivity 中进行。我们把在 Application 处理的称为第一阶段,在 LaunchActivity 中进行的称为第二阶段和第三阶段。

第一阶段:预处理

public class MainApplication extends Application {

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

        Small.preSetUp(this);
       //指定是否从 assets 读取插件
        Small.setLoadFromAssets(BuildConfig.LOAD_FROM_ASSETS);
    }

}

Application 构造函数中调用了 Small.preSetUp(this) 来进行一些设置的工作:

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

        sContext = context;

        // 注册BundleLauncher
        registerLauncher(new ActivityLauncher());
        registerLauncher(new ApkBundleLauncher());
        registerLauncher(new WebBundleLauncher());
        Bundle.onCreateLaunchers(context);
    }
  1. 注册各种插件,保存在各种sBundleLaunchers静态变量中
  2. 调用Bundle.onCreateLaunchers(context)方法来调用launcher.onCreate(app)方法
  3. 在几个BundleLauncher子类中,ApkBundleLauncher重新实现了onCreate方法

如下所示:

   protected static void onCreateLaunchers(Application app) {
        if (sBundleLaunchers == null) return;

        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.onCreate(app);
        }
    }

ApkBundleLauncher重写onCreate方法

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

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

        // 通过反射获取当前 ActivityThread 对象
        thread = ReflectAccelerator.getActivityThread(app);

        //替换 mInstrumentation 变量,关键点在这里,下面会详述
        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);
        }

    // 通过反射替换ActivityThread 的 Message Handler mH变量的 mCallback 为ActivityThreadHandlerCallback
    // 用于恢复Activity Info 到真实的Activity
        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;
    }

InstrumentationWrapper 继承自 Instrumentation 并覆盖了下面几个方法:

execStartActivity()
callActivityOnCreate()
callActivityOnStop()
callActivityOnDestroy()
onException()

Instrumentation 跟踪 Application 及 Activity 的整个生命周期,它的一些方法在 Application 及 Activity 所有生命周期函数的调用中,都会先调用这些方法,因此,得到了这个对象,我们就可以进入并跟踪 Application 和 Activity 的生命周期流程。<br />Small 想要做到动态注册 Activity,首先在宿主 Manifest 中注册一个命名特殊的占坑 Activity 来欺骗 startActivityForResult 以获得生命周期,再欺骗 performLaunchActivity 来获得插件 Activity 实例,又为了处理之间的信息传递,因此有了后面的 ActivityThreadHandlerCallback。<br />我们可以在 small/src/main/AndroidManifest.xml 中找到这些占坑位的 Activity: A、A1、A2….A33等。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.wequick.small">
    <!-- permission for web view -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <application>
        <!-- Stub Activities -->
        <!-- 1 standard mode -->
        <activity android:name=".A"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A1" android:theme="@android:style/Theme.Translucent"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <!-- 4 singleTop mode -->
        <activity android:name=".A10" android:launchMode="singleTop"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A11" android:launchMode="singleTop"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A12" android:launchMode="singleTop"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A13" android:launchMode="singleTop"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <!-- 4 singleTask mode -->
        <activity android:name=".A20" android:launchMode="singleTask"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A21" android:launchMode="singleTask"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A22" android:launchMode="singleTask"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A23" android:launchMode="singleTask"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <!-- 4 singleInstance mode -->
        <activity android:name=".A30" android:launchMode="singleInstance"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A31" android:launchMode="singleInstance"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A32" android:launchMode="singleInstance"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <activity android:name=".A33" android:launchMode="singleInstance"
            android:configChanges="locale|fontScale|keyboard|keyboardHidden|layoutDirection|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"/>
        <!-- Web Activity -->
        <activity android:name=".webkit.WebActivity"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="stateHidden|adjustPan"
            android:hardwareAccelerated="true"/>
    </application>
</manifest>

所作的这一切都是为了实现动态注册 Activity,如果你把插件里面的 Activity 都在宿主的 AndroidManifest.xml 文件里面都申明一下,那么上面的这些 Hook 的工作就可以省去了。

第二阶段:加载插件

配置工作

调用了 Small.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;
    }

在 Small.setUp() 方法内部主要调用了 Bundle.loadLaunchableBundles(listener)。

    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.OnCompleteListener,这里会开启一个线程来调用 loadBundles() 方法。

 private static void loadBundles(Context context) {
    JSONObject manifestData;
    try {
        //获取 /data/data/<application package>/files 目录下的 bundle.json
        File patchManifestFile = getPatchManifestFile();
        //获取 SharedPreferences 存储的bundle.json文件
        String manifestJson = getCacheManifest();
        if (manifestJson != null) {
            // 加载SharedPreferences中的缓存的文件并保存到patchManifestFile文件中
            if (!patchManifestFile.exists()) patchManifestFile.createNewFile();
            PrintWriter pw = new PrintWriter(new FileOutputStream(patchManifestFile));
            pw.print(manifestJson);
            pw.flush();
            pw.close();
            // 清除SharedPreferences中的缓存
            setCacheManifest(null);
        } else if (patchManifestFile.exists()) {
            // 从patchManifestFile中读取数据
            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 {
            // 从 `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);
        }
        manifestData = new JSONObject(manifestJson);
    } catch (Exception e) {
        e.printStackTrace();
        return;
    }
    // 解析数据
    Manifest manifest = parseManifest(manifestData);
    if (manifest == null) return;
    setupLaunchers(context);
    loadBundles(manifest.bundles);
}

在 loadBundles() 方法中首先会解析 bundle.json 数据,这个数据可能会保存在三个地方,它们的读取是有优先级的,SharedPreferences缓存>App DATA File>Assets。<br />然后调用 setupLaunchers() 设置前面在 preSetup() 方法中注册的几个 BundleLauncher。

  protected static void setupLaunchers(Context context) {
        if (sBundleLaunchers == null) return;

        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.setUp(context);
        }
    }

关于launcher.setUp(context)方法究竟做了什么?

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

        // Read the registered classes in host's manifest file
        File sourceFile = new File(context.getApplicationInfo().sourceDir);
        BundleParser parser = BundleParser.parsePackage(sourceFile, context.getPackageName());
        parser.collectActivities();
        ActivityInfo[] as = parser.getPackageInfo().activities;
        if (as != null) {
            sActivityClasses = new HashSet<String>();
            for (ActivityInfo ai : as) {
                sActivityClasses.add(ai.name);
            }
        }
    }

这里是将在宿主App里面注册的 Activity 添加到 sActivityClasses 中去,这里包括了 app、app+stub、small下面 AndroidMenifest.xml里面注册的 Activity,当然就包括了前面说的占坑位的几个 Activity。

  1. ApkBundleLauncher.setUp()
    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];
                    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) {
            Log.e(TAG, "Failed to hook TaskStackBuilder. \n" +
                    "Please manually call `Small.wrapIntent` to ensure the notification intent can be opened. \n" +
                    "See https://github.com/wequick/Small/issues/547 for details.");
        }
    }

这里是对通过动态代理对所有经过 TaskStackBuilder 创建的 PendingIntent 进行 Hook,调用 wrapIntent 用占坑 Activity 来代替真正的 Activity。<br />另外还有个方法 Small.wrapIntent(Intent),不是通过TaskStackBuilder 创建的 PendingIntent 需要调用这个方法来进行处理。

  1. WebBundleLauncher.setUp()
    public void setUp(Context context) {
        super.setUp(context);
        if (Build.VERSION.SDK_INT < 24) return;

        Bundle.postUI(new Runnable() {
            @Override
            public void run() {
                // In android 7.0+, on firstly create WebView, it will replace the application
                // assets with the one who has join the WebView asset path.
                // If this happens after our assets replacement,
                // what we have done would be come to naught!
                // So, we need to push it enOOOgh ahead! (#347)
                new android.webkit.WebView(Small.getContext());
            }
        });
    }

在android 7.0以后的版本中,当第一次创建WebView的时候,它会用WebView的Assets路径替换掉原Application Assets路径,这里就提前在这里先创建一个WebView来避免这个事件的发生。<br />在 setupLaunchers(context) 方法执行完以后,就会调用 loadBundles(manifest.bundles) 方法来加载插件。

加载插件

private static void loadBundles(List<Bundle> bundles) {
    sPreloadBundles = bundles;
    // 1. 为每个Bundle寻找合适的BundleLauncher
    for (Bundle bundle : bundles) {
        bundle.prepareForLaunch();
    }
    // Handle I/O
    if (sIOActions != null) {
        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;
    }
    // 在“postsetup”之前等待在ui线程上完成的操作
    // 与7.0+一样,我们应该等待一个webview被初始化。
    while (sRunningUIActionCount != 0) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    // Notify `postSetUp' to all launchers
    for (BundleLauncher launcher : sBundleLaunchers) {
        launcher.postSetUp();
    }
    // 在“postsetup”之后等待在ui线程上执行操作
    // 就像创建一个bundle应用程序。
    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;
    }
}

Bundle.loadBundles(List<Bundle> bundles)方法,这个方法的主要工作就是在注册的所有 BundleLauncher 中为 bundles 列表中的所有 Bundle 找到适合它们的 BundleLaunch

bundle.prepareForLaunch();

    protected void prepareForLaunch() {
        if (mIntent != null) return;

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

我们先来看一下 Bundle.prepareForLaunch() 方法,这里是要在 sBundleLaunchers 中为当前的 Bundle 找到一个合适的 BundleLauncher 并赋值给 mApplicableLauncher,并开始解析插件,这里又分别调用了 sBundleLaunchers 中各个 BundleLauncher 的 resolveBundle() 方法。

** resolveBundle() **

   public boolean resolveBundle(Bundle bundle) {
        if (!preloadBundle(bundle)) return false;

        loadBundle(bundle);
        return true;
    }

各个 BundleLauncher 都分别重新实现了 preloadBundle(bundle) 预载插件和 loadBundle(bundle) 方法,我们分别来看一下。

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

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

这里在 mPackageName 为 main 时才会返回true,ActivityLauncher 是用来启动宿主 Activity 的,它并没有实现 loadBundle 方法,因此就算 preloadBundle()方法返回true,它也不会有任何处理的。

  • SoBundleLauncher.preloadBundle()

因为 ApkBundleLauncher 没有覆盖 preloadBundle() 方法,那么就到了它的父类 SoBundleLauncher.preloadBundle()方法。

  @Override
public boolean preloadBundle(Bundle bundle) {
    String packageName = bundle.getPackageName();
    if (packageName == null) return false;
    // 获取支持的插件类型,ApkBundleLauncher 支持 `app` 和 `lib`,WebBundleLauncher 支持`web`
    String[] types = getSupportingTypes();
    if (types == null) return false;
    boolean supporting = false;
    String bundleType = bundle.getType();
    if (bundleType != null) {
        // 如果在 `bundle.json' 中设置了type,就去根据type来找到合适的BundleLauncher
        for (String type : types) {
            if (type.equals(bundleType)) {
                supporting = true;
                break;
            }
        }
    } else {
        // 如果没有指定type,就尝试根据包名来判断,看里面是否包含app、lib或者web等
        //  - 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;
            }
        }
    }
    //如果该BundleLauncher不支持该Bundle类型,直接返回
    if (!supporting) return false;
    // 获取提取路径,ApkBundleLauncher和AssetBundleLauncher分别有不同的定义。
    File extractPath = getExtractPath(bundle);
    if (extractPath != null) {
        if (!extractPath.exists()) {
            extractPath.mkdirs();
        }
        bundle.setExtractPath(extractPath);
    }
    // 获取基础插件文件/data/data/<包名>/app_small_base/<包名>.apk文件
    File plugin = bundle.getBuiltinFile();
    // 解析AndroidManifest.xml文件,得到插件的版本,主题风格,Activity,收集intent-filter等
    BundleParser parser = BundleParser.parsePackage(plugin, packageName);
    // 获取patch插件文件/data/data/<包名>/app_small_patch/<包名>.apk文件
    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) {
        // 防止patch版本过低
        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);
    // 检查插件是否被修改过
    long lastModified = plugin.lastModified();
    long savedLastModified = Small.getBundleLastModified(packageName);
    if (savedLastModified != lastModified) {
        // 如果被修改过,进行一些检验工作
        if (!parser.verifyAndExtract(bundle, this)) {
            bundle.setEnabled(false);
            return true; // Got it, but disabled
        }
        Small.setBundleLastModified(packageName, lastModified);
    }
    // 保存插件的版本
    PackageInfo pluginInfo = parser.getPackageInfo();
    bundle.setVersionCode(pluginInfo.versionCode);
    bundle.setVersionName(pluginInfo.versionName);
    return true;
}

插件的解析由 BundleParser 类来完成。

  • ApkBundleLauncher.loadBundle()

为插件创建 LoadedApk 对象,加载dex文件以及lib库,提取Activity并放入sLoadedActivities列表,收集intentFilter并存入sLoadedIntentFilters列表。

@Override
public void loadBundle(Bundle bundle) {
    String packageName = bundle.getPackageName();
    BundleParser parser = bundle.getParser();
    // 收集activity
    parser.collectActivities();
    PackageInfo pluginInfo = parser.getPackageInfo();
    // 获取插件文件的路径
    String apkPath = parser.getSourcePath();
    if (sLoadedApks == null) sLoadedApks = new ConcurrentHashMap<String, LoadedApk>();
    LoadedApk apk = sLoadedApks.get(packageName);
    if (apk == null) {
        // 为该Bundle创建LoadedApk对象
        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);
        // 加载dex文件
        final LoadedApk fApk = apk;
        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);
                }
            }
        });
        // 提取特定ABI的 native libraries 路径
        String libDir = parser.getLibraryDirectory();
        if (libDir != null) {
            apk.libraryPath = new File(apk.packagePath, libDir);
        }
        sLoadedApks.put(packageName, apk);
    }
    if (pluginInfo.activities == null) {
        return;
    }
    // Record activities for intent redirection
    if (sLoadedActivities == null) sLoadedActivities = new ConcurrentHashMap<String, ActivityInfo>();
    for (ActivityInfo ai : pluginInfo.activities) {
        sLoadedActivities.put(ai.name, ai);
    }
    // 收集 intent-filters for implicit action
    ConcurrentHashMap<String, List<IntentFilter>> filters = parser.getIntentFilters();
    if (filters != null) {
        if (sLoadedIntentFilters == null) {
            sLoadedIntentFilters = new ConcurrentHashMap<String, List<IntentFilter>>();
        }
        sLoadedIntentFilters.putAll(filters);
    }
    // 设置该插件的manifest中定义的入口Activity 
    bundle.setEntrance(parser.getDefaultActivityName());
}
  • AssetBundleLauncher.loadBundle()

WebBundleLauncher 的 loadBundle() 方法也由它的父类 AssetBundleLauncher 来处理,由于 AssetBundleLauncher 是继承自 SoBundleLauncher,因此 preloadBundle() 也由 SoBundleLauncher 处理。<br />这个方法主要是将插件文件路径转化为index文件路径

@Override
public void loadBundle(Bundle bundle) {
    String packageName = bundle.getPackageName();
    // 获取插件路径
    File unzipDir = new File(getBasePath(), packageName);
    // 获取indexfile文件,WebBundleLauncher就是在unzipDir后面加上/index.html
    File indexFile = new File(unzipDir, getIndexFileName());
    // Prepare index url
    String uri = indexFile.toURI().toString();
    if (bundle.getQuery() != null) {
        uri += "?" + bundle.getQuery();
    }
    URL url;
    try {
        url = new URL(uri);
    } catch (MalformedURLException e) {
        Log.e(TAG, "Failed to parse url " + uri + " for bundle " + packageName);
        return;
    }
    String scheme = url.getProtocol();
    if (!scheme.equals("http") &&
            !scheme.equals("https") &&
            !scheme.equals("file")) {
        Log.e(TAG, "Unsupported scheme " + scheme + " for bundle " + packageName);
        return;
    }
    bundle.setURL(url);
}
BundleLauncher.postSetUp()

这里也会调用 BundleLauncher 各个子类的 BundleLauncher方法。<br />但是仅有 ApkBundleLauncher 覆盖了基类的空实现。

@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
    final Application app = Small.getContext();
    String[] paths = new String[apks.size() + 1];
    // 添加宿主app的资源路径
    paths[0] = app.getPackageResourcePath(); 
    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);
    // 合并插件的dex文件到宿主的class loader
    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)) {
            // If upgraded, delete the opt dex file for recreating
            if (apk.optDexFile.exists()) apk.optDexFile.delete();
            Small.setBundleUpgraded(apk.packageName, false);
        }
        i++;
    }
    ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);
    // 为宿主class loader扩展它的native library路径,这个路径包含了插件的native library路径
    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);
    }
    // 调用所有插件Application的`onCreate' 方法
    for (final LoadedApk apk : apks) {
        String bundleApplicationName = apk.applicationName;
        if (bundleApplicationName == null) continue;
        try {
            final Class applicationClass = Class.forName(bundleApplicationName);
            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
    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;
}
至此,插件的初始化部分介绍

至此,插件的初始化部分介绍完毕。

下节分析

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

推荐阅读更多精彩内容