腾讯Shadow浅析及应用及BlackShadow

Android · 03-15 · 1051 人浏览

物种起源

最近在做新东西的时候,由于业务模式的问题,如有更新,发版本并不合适我们的场景,这就需要用到动态化的需求。当时就需要评估技术方案了

  1. 类似于BlackBox的虚拟化
  2. 造轮子
  3. 腾讯Shadow

处于快速,稳定性和轻量级原则,选择了腾讯的Shadow,毕竟也是大肠项目,非常可靠。

撸起 ** 开干

刚把项目拉下的时候,有点懵逼,离离原上谱啊家人们,这么多模块。

5ba15d07e72c27cadd2fa1de0874f115.jpg

源码层及运行模式

经过一番摸索,其实整体的流程也是比较简单的,我感觉核心还是在gradle的plugin

运行模式

Shadow 支持两种运行模式,动态非动态

先来说说动态模式,动态模式分为3个模块

  1. manager :主要负责模块的安装、管理、装载
  2. loader :主要负责加载Plugin,核心的加载逻辑
  3. runtime :主要负责提供插件的坑位,例如:PluginDefaultProxyActivity等。

这样做的好处就是全动态化,Shadow内核出现了什么bug或者需要支持什么,可以通过动态下发loader或者manager模块动态支持,安装到用户手机里的宿主不需要发版本更新。

再说说非动态模式
非动态模式几乎不需要模块,但是Shadow内核会打包在宿主内,如果需要更新内核,则需要更新APP,整体项目维护的复杂度降低。

两者一对比各有好坏,其实按照我个人的见解,其实一般情况下使用非动态模式就足够了。包括我后面做的时候都是使用非动态模式去做的。
原因:整个框架已经经过了腾讯的大量用户摩擦,其实并没有太多的需要自己兼容或者处理的东西,而且非动态会降低项目的维护成本,以及加载成功率。(有同行线上经过测试,非动态加载比动态加载的成功率会更高),其实也可以理解,因为动态本身就是一个不稳定的事情。

源码

以下不做喂饭分析,许多细节需要自己去扒,这里只梳理整个流程及原理。

首先先看一下动态的源码,关注以下几个模块

  • sample-host
  • sample-manager
  • sample-loader
  • sample-runtime

先关注怎么启动一个插件
sample-host#com.tencent.shadow.sample.host.PluginLoadActivity#startPlugin

HostApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);

Bundle bundle = new Bundle();
bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH, PluginHelper.getInstance().pluginZipFile.getAbsolutePath());
bundle.putString(Constant.KEY_PLUGIN_PART_KEY, getIntent().getStringExtra(Constant.KEY_PLUGIN_PART_KEY));
bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME, getIntent().getStringExtra(Constant.KEY_ACTIVITY_CLASSNAME));

HostApplication.getApp().getPluginManager()
        .enter(PluginLoadActivity.this, Constant.FROM_ID_START_ACTIVITY, bundle, new EnterCallback() {
            @Override
            public void onShowLoadingView(final View view) {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mViewGroup.addView(view);
                    }
                });
            }

            @Override
            public void onCloseLoadingView() {
                finish();
            }

            @Override
            public void onEnterComplete() {

            }
        });

其中loadPluginManager则是使用了Shadow官方提供的PluginManagerUpdater接口实现了FixedPathPmUpdater,使用固定路径加载manager。
DynamicPluginManager中则是反射加载manager中的实现类,这样就动态加载了manager。也就是sample-manager。DynamicPluginManager的源码就自己去翻阅了,实际上就是加载classloader然后反射,非常简单的一个。

    public void loadPluginManager(File apk) {
        if (mPluginManager == null) {
            mPluginManager = Shadow.getPluginManager(apk);
        }
    }


    public static PluginManager getPluginManager(File apk) {
        final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
        File tempPm = fixedPathPmUpdater.getLatest();
        if (tempPm != null) {
            return new DynamicPluginManager(fixedPathPmUpdater);
        }
        return null;
    }

回到startPlugin,后续调用了PluginManager中的enter。

HostApplication.getApp().getPluginManager()
        .enter(PluginLoadActivity.this, Constant.FROM_ID_START_ACTIVITY, bundle, new EnterCallback())

上面分析了,manager是由DynamicPluginManager反射创建的,所以最终来到是sample-manager#com.tencent.shadow.sample.manager.SamplePluginManager

    @Override
    public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {
        if (fromId == Constant.FROM_ID_NOOP) {
            //do nothing.
        } else if (fromId == Constant.FROM_ID_START_ACTIVITY) {
            onStartActivity(context, bundle, callback);
        } 
        ......省略
    }


 private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
        final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
        final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
        final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
        if (className == null) {
            throw new NullPointerException("className == null");
        }
        final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);

        if (callback != null) {
            final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
            callback.onShowLoadingView(view);
        }

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);

                    loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
                    loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_MAIN_APP);
                    callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
                    callApplicationOnCreate(PART_KEY_PLUGIN_MAIN_APP);

                    Intent pluginIntent = new Intent();
                    pluginIntent.setClassName(
                            context.getPackageName(),
                            className
                    );
                    if (extras != null) {
                        pluginIntent.replaceExtras(extras);
                    }
                    Intent intent = mPluginLoader.convertActivityIntent(pluginIntent);
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    mPluginLoader.startActivityInPluginProcess(intent);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                if (callback != null) {
                    callback.onCloseLoadingView();
                }
            }
        });
    }

到目前为止,我们的运行层面,还是在宿主的主进程,接下来就会开始进入插件的加载流程。

可以看到进入了installPlugin方法,这段代码啪啪啪搞这么长,就是用来解析并且加载由Shadow的Gradle Plugin,打包过后的插件包。

InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);


public InstalledPlugin installPlugin(String zip, String hash, boolean odex) throws IOException, JSONException, InterruptedException, ExecutionException {
        final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
        final String uuid = pluginConfig.UUID;
        List<Future> futures = new LinkedList<>();
        List<Future<Pair<String, String>>> extractSoFutures = new LinkedList<>();
        if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {
            Future odexRuntime = mFixedPool.submit(new Callable() {
                @Override
                public Object call() throws Exception {
                    oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME,
                            pluginConfig.runTime.file);
                    return null;
                }
            });
            futures.add(odexRuntime);
            Future odexLoader = mFixedPool.submit(new Callable() {
                @Override
                public Object call() throws Exception {
                    oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER,
                            pluginConfig.pluginLoader.file);
                    return null;
                }
            });
            futures.add(odexLoader);
        }
        for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
            final String partKey = plugin.getKey();
            final File apkFile = plugin.getValue().file;
            Future<Pair<String, String>> extractSo = mFixedPool.submit(() -> extractSo(uuid, partKey, apkFile));
            futures.add(extractSo);
            extractSoFutures.add(extractSo);
            if (odex) {
                Future odexPlugin = mFixedPool.submit(new Callable() {
                    @Override
                    public Object call() throws Exception {
                        oDexPlugin(uuid, partKey, apkFile);
                        return null;
                    }
                });
                futures.add(odexPlugin);
            }
        }

        for (Future future : futures) {
            future.get();
        }
        Map<String, String> soDirMap = new HashMap<>();
        for (Future<Pair<String, String>> future : extractSoFutures) {
            Pair<String, String> pair = future.get();
            soDirMap.put(pair.first, pair.second);
        }
        onInstallCompleted(pluginConfig, soDirMap);

        return getInstalledPlugins(1).get(0);
    }

里面包含了runtime、loader,以及各个需要运行的插件,并且包含一份配置文件,如下图:
Xnip2024-03-16_23-07-47.jpg

通过解析config.json,获取到所有的信息并且解析存入本地数据库。

继续分析

紧跟着loadPlugin,可以看到此处会先进行loadPluginLoaderAndRuntime,这里面做的事情就是绑定一个服务,getPluginProcessServiceName()方法返回的是:com.tencent.shadow.sample.host.PluginProcessPPS,从注册信息可以看出

loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_MAIN_APP);

protected void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
        loadPluginLoaderAndRuntime(uuid, partKey);
        Map map = mPluginLoader.getLoadedPlugin();
        if (!map.containsKey(partKey)) {
            mPluginLoader.loadPlugin(partKey);
        }
    }

    private void loadPluginLoaderAndRuntime(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
        if (mPpsController == null) {
            bindPluginProcessService(getPluginProcessServiceName(partKey));
            waitServiceConnected(10, TimeUnit.SECONDS);
        }
        loadRunTime(uuid);
        loadPluginLoader(uuid);
    }
        <service
            android:name="com.tencent.shadow.sample.host.PluginProcessPPS"
            android:process=":plugin" />

此处会进行启动一个PluginProcessPPS的服务,并且该服务运行的进程为:plugin,与宿主不是同一个进程

然后经过bindPluginProcessService,waitServiceConnected,进行启动与等待启动完成。

继续分析,等待启动完毕之后,会先后调用loadRunTime、loadPluginLoader两个方法。

此处就是一个ipc调用,实现是由PluginProcessPPS完成,由此可以看出,宿主通过启动PluginProcessPPS,并且通过ipc,让PluginProcessPPS,也就是plugin进程(非宿主进程),完成loadRunTime、loadPluginLoader

    loadRunTime(uuid);
    loadPluginLoader(uuid);

    public final void loadRunTime(String uuid) throws RemoteException, FailedException {
        if (mLogger.isInfoEnabled()) {
            mLogger.info("loadRunTime mPpsController:" + mPpsController);
        }
        PpsStatus ppsStatus = mPpsController.getPpsStatus();
        if (!ppsStatus.runtimeLoaded) {
            mPpsController.loadRuntime(uuid);
        }
    }

    public final void loadPluginLoader(String uuid) throws RemoteException, FailedException {
        if (mLogger.isInfoEnabled()) {
            mLogger.info("loadPluginLoader mPluginLoader:" + mPluginLoader);
        }
        if (mPluginLoader == null) {
            PpsStatus ppsStatus = mPpsController.getPpsStatus();
            if (!ppsStatus.loaderLoaded) {
                mPpsController.loadPluginLoader(uuid);
            }
            IBinder iBinder = mPpsController.getPluginLoader();
            mPluginLoader = new BinderPluginLoader(iBinder);
        }
    }

我们接下来看看PluginProcessPPS中做了些什么事,PluginProcessPPS并没有做些什么,而是继承了PluginProcessService,继续往下看

public class PluginProcessPPS extends PluginProcessService {
    public PluginProcessPPS() {
        LoadPluginCallback.setCallback(new LoadPluginCallback.Callback() {

            @Override
            public void beforeLoadPlugin(String partKey) {
                Log.d("PluginProcessPPS", "beforeLoadPlugin(" + partKey + ")");
            }

            @Override
            public void afterLoadPlugin(String partKey, ApplicationInfo applicationInfo, ClassLoader pluginClassLoader, Resources pluginResources) {
                Log.d("PluginProcessPPS", "afterLoadPlugin(" + partKey + "," + applicationInfo.className + "{metaData=" + applicationInfo.metaData + "}" + "," + pluginClassLoader + ")");
            }
        });
    }
}

先看下PluginProcessPPS中的loadRuntime,我将在代码中注释,这段稍微比较简单。

    void loadRuntime(String uuid) throws FailedException {
        checkUuidManagerNotNull();
        setUuid(uuid);
        if (mRuntimeLoaded) {
            throw new FailedException(ERROR_CODE_RELOAD_RUNTIME_EXCEPTION
                    , "重复调用loadRuntime");
        }
        try {
            if (mLogger.isInfoEnabled()) {
                mLogger.info("loadRuntime uuid:" + uuid);
            }
            // 此处getRuntime获取InstalledApk,此处实际就是获取runtime模块的安装信息(路径,基本信息),我们在上方installPlugin中已经全部解析好了,存入了本地数据库,此时只是单纯的get出来。
            InstalledApk installedApk;
            try {
                installedApk = mUuidManager.getRuntime(uuid);
            } catch (RemoteException e) {
                throw new FailedException(ERROR_CODE_UUID_MANAGER_DEAD_EXCEPTION, e.getMessage());
            } catch (NotFoundException e) {
                throw new FailedException(ERROR_CODE_FILE_NOT_FOUND_EXCEPTION, "uuid==" + uuid + "的Runtime没有找到。cause:" + e.getMessage());
            }

            // 这里重新转换了一下,其实是同一个东西
            InstalledApk installedRuntimeApk = new InstalledApk(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath);
            // 下面这几个方法,则是将runtime的模块加载进当前进程,并且装载进classloader,这样在使用时就不会出现class not found
            boolean loaded = DynamicRuntime.loadRuntime(installedRuntimeApk);
            if (loaded) {
                DynamicRuntime.saveLastRuntimeInfo(this, installedRuntimeApk);
            }
            mRuntimeLoaded = true;
        } catch (RuntimeException e) {
            if (mLogger.isErrorEnabled()) {
                mLogger.error("loadRuntime发生RuntimeException", e);
            }
            throw new FailedException(e);
        }
    }

loadPluginLoader前半段是几乎与loadRuntime做的事情一致,变成了加载loader模块,此处最后不一样的是,使用LoaderImplLoader加载了loader模块。

void loadPluginLoader(String uuid) throws FailedException {
        if (mLogger.isInfoEnabled()) {
            mLogger.info("loadPluginLoader uuid:" + uuid + " mPluginLoader:" + mPluginLoader);
        }
        checkUuidManagerNotNull();
        setUuid(uuid);
        if (mPluginLoader != null) {
            throw new FailedException(ERROR_CODE_RELOAD_LOADER_EXCEPTION
                    , "重复调用loadPluginLoader");
        }
        try {
            InstalledApk installedApk;
            try {
                installedApk = mUuidManager.getPluginLoader(uuid);
                if (mLogger.isInfoEnabled()) {
                    mLogger.info("取出uuid==" + uuid + "的Loader apk:" + installedApk.apkFilePath);
                }
            } catch (RemoteException e) {
                if (mLogger.isErrorEnabled()) {
                    mLogger.error("获取Loader Apk失败", e);
                }
                throw new FailedException(ERROR_CODE_UUID_MANAGER_DEAD_EXCEPTION, e.getMessage());
            } catch (NotFoundException e) {
                throw new FailedException(ERROR_CODE_FILE_NOT_FOUND_EXCEPTION, "uuid==" + uuid + "的PluginLoader没有找到。cause:" + e.getMessage());
            }
            File file = new File(installedApk.apkFilePath);
            if (!file.exists()) {
                throw new FailedException(ERROR_CODE_FILE_NOT_FOUND_EXCEPTION, file.getAbsolutePath() + "文件不存在");
            }

            PluginLoaderImpl pluginLoader = new LoaderImplLoader().load(installedApk, uuid, getApplicationContext());
            pluginLoader.setUuidManager(mUuidManager);
            mPluginLoader = pluginLoader;
        } catch (RuntimeException e) {
            if (mLogger.isErrorEnabled()) {
                mLogger.error("loadPluginLoader发生RuntimeException", e);
            }
            throw new FailedException(e);
        } catch (FailedException e) {
            throw e;
        } catch (Exception e) {
            if (mLogger.isErrorEnabled()) {
                mLogger.error("loadPluginLoader发生Exception", e);
            }
            String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
            throw new FailedException(ERROR_CODE_RUNTIME_EXCEPTION, "加载动态实现失败 cause:" + msg);
        }
    }

重点:
LoaderImplLoader是加载sample-loader的地方,通过load方法,同时在内部创建了一个binder,并且实现了以下几个方法

    void loadPlugin(String partKey) throws RemoteException;

    Map getLoadedPlugin() throws RemoteException;

    void callApplicationOnCreate(String partKey) throws RemoteException;

    Intent convertActivityIntent(Intent pluginActivityIntent) throws RemoteException;

    ComponentName startPluginService(Intent pluginServiceIntent) throws RemoteException;

    boolean stopPluginService(Intent pluginServiceIntent) throws RemoteException;

    boolean bindPluginService(Intent pluginServiceIntent, PluginServiceConnection connection, int flags) throws RemoteException;

    void unbindService(PluginServiceConnection conn) throws RemoteException;

    void startActivityInPluginProcess(Intent intent) throws RemoteException;

最终的实现是转给了mDynamicPluginLoader,也就是sample-loader,这里稍微有点绕,需要结合代码一起阅读会比较好。

com.tencent.shadow.dynamic.loader.impl.PluginLoaderBinder

@Throws(android.os.RemoteException::class)
    public override fun onTransact(
        code: Int,
        data: android.os.Parcel,
        reply: android.os.Parcel?,
        flags: Int
    ): Boolean {
        if (reply == null) {
            throw NullPointerException("reply == null")
        }
        when (code) {
            IBinder.INTERFACE_TRANSACTION -> {
                reply.writeString(PluginLoader.DESCRIPTOR)
                return true
            }
            PluginLoader.TRANSACTION_loadPlugin -> {
                data.enforceInterface(PluginLoader.DESCRIPTOR)
                val _arg0: String
                _arg0 = data.readString()!!
                try {
                    mDynamicPluginLoader.loadPlugin(_arg0)
                    reply.writeNoException()
                } catch (e: Exception) {
                    reply.writeException(wrapExceptionForBinder(e))
                }
                return true
            }
            PluginLoader.TRANSACTION_getLoadedPlugin -> {
                data.enforceInterface(PluginLoader.DESCRIPTOR)
                try {
                    val _result = mDynamicPluginLoader.getLoadedPlugin()
                    reply.writeNoException()
                    reply.writeMap(_result as Map<*, *>?)
                } catch (e: Exception) {
                    reply.writeException(wrapExceptionForBinder(e))
                }

                return true
            }
            PluginLoader.TRANSACTION_callApplicationOnCreate -> {
                data.enforceInterface(PluginLoader.DESCRIPTOR)
                val _arg0: String
                _arg0 = data.readString()!!
                try {
                    mDynamicPluginLoader.callApplicationOnCreate(_arg0)
                    reply.writeNoException()
                } catch (e: Exception) {
                    reply.writeException(wrapExceptionForBinder(e))
                }

                return true
            }
            PluginLoader.TRANSACTION_stopPluginService -> {
                data.enforceInterface(PluginLoader.DESCRIPTOR)
                val intent = if (0 != data.readInt()) {
                    Intent.CREATOR.createFromParcel(data)
                } else {
                    reply.writeException(NullPointerException("intent==null"))
                    return true
                }
                try {
                    val _result = mDynamicPluginLoader.stopPluginService(intent)
                    reply.writeNoException()
                    reply.writeInt(if (_result) 1 else 0)
                } catch (e: Exception) {
                    reply.writeException(wrapExceptionForBinder(e))
                }
                return true
            }
        }
        return super.onTransact(code, data, reply, flags)
    }

此时mPluginLoader,其实是 具有loader模块的binder版,此时先不关心内部实现,只要知道mPluginLoader能操纵loader模块即可。

注意,loadRuntime与loadPluginLoader都是在plugin进程,此时应该结束,要回到主进程了。

怕你们忘记了,还是回到这个流程上面来,我们已经经过了loadPluginLoaderAndRuntime方法,里面的loadRunTime、loadPluginLoader也均已完成,接下来就是通过mPluginLoader,获取已经加载的plugin,如果没有加载,则调用loadPlugin

loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_MAIN_APP);

protected void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
        loadPluginLoaderAndRuntime(uuid, partKey);
        Map map = mPluginLoader.getLoadedPlugin();
        if (!map.containsKey(partKey)) {
            mPluginLoader.loadPlugin(partKey);
        }
    }

    private void loadPluginLoaderAndRuntime(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
        if (mPpsController == null) {
            bindPluginProcessService(getPluginProcessServiceName(partKey));
            waitServiceConnected(10, TimeUnit.SECONDS);
        }
        loadRunTime(uuid);
        loadPluginLoader(uuid);
    }

此处是不是疑惑mPluginLoader从哪里来?

上面经过分析调用PluginProcessService中的loadPluginLoader之后,会通过LoaderImplLoader创建loader,并且封装成一个binder,此处getPluginLoader就是获取的plugin进程中 具有loader模块的binder版

    public final void loadPluginLoader(String uuid) throws RemoteException, FailedException {
        if (mLogger.isInfoEnabled()) {
            mLogger.info("loadPluginLoader mPluginLoader:" + mPluginLoader);
        }
        if (mPluginLoader == null) {
            PpsStatus ppsStatus = mPpsController.getPpsStatus();
            if (!ppsStatus.loaderLoaded) {
                mPpsController.loadPluginLoader(uuid);
            }
            // 重点
            IBinder iBinder = mPpsController.getPluginLoader();
            mPluginLoader = new BinderPluginLoader(iBinder);
        }
    }

如果读到此处感到吃力,不能理解。那么说明基础不够,建议翻回去继续看源码反复阅读分析。不多赘述。

此时,像上面分析所说,获取已经加载的plugin,如果没有加载,则调用loadPlugin。

那么问题来了,loadPlugin会去到哪里呢?哪个进程?哪个代码?
如果不能说出来,建议翻回去继续看源码反复阅读分析。其实比较核心的还是进程之间的关系与调用。

此时loadPlugin会调用:plugin进程中已经加载的loader模块中的loadPlugin,此处是主进程,所以是通过ipc调用,查看binder的实现端

重点重点,请注意,此时又来到了plugin进程

com.tencent.shadow.dynamic.loader.impl.PluginLoaderBinder

@Throws(android.os.RemoteException::class)
    public override fun onTransact(
        code: Int,
        data: android.os.Parcel,
        reply: android.os.Parcel?,
        flags: Int
    ): Boolean {
        if (reply == null) {
            throw NullPointerException("reply == null")
        }
        when (code) {
            IBinder.INTERFACE_TRANSACTION -> {
                reply.writeString(PluginLoader.DESCRIPTOR)
                return true
            }
            PluginLoader.TRANSACTION_loadPlugin -> {
                data.enforceInterface(PluginLoader.DESCRIPTOR)
                val _arg0: String
                _arg0 = data.readString()!!
                try {
                    mDynamicPluginLoader.loadPlugin(_arg0)
                    reply.writeNoException()
                } catch (e: Exception) {
                    reply.writeException(wrapExceptionForBinder(e))
                }
                return true
            }
        }
        return super.onTransact(code, data, reply, flags)
    }

mDynamicPluginLoader实际上就是对sample-loader中的类稍微包装后的类,所以也就调用到了sample-loader中的loadPlugin

sample-loader#com.tencent.shadow.sample.plugin.loader.SamplePluginLoader#loadPlugin

@Override
    public Future<?> loadPlugin(final InstalledApk installedApk) throws LoadPluginException {
        LoadParameters loadParameters = getLoadParameters(installedApk);
        final String partKey = loadParameters.partKey;

        LoadPluginCallback.getCallback().beforeLoadPlugin(partKey);

        final Future<?> future = super.loadPlugin(installedApk);

        getMExecutorService().submit(new Runnable() {
            @Override
            public void run() {
                try {
                    future.get();
                    PluginParts pluginParts = getPluginParts(partKey);
                    String packageName = pluginParts.getApplication().getPackageName();
                    ApplicationInfo applicationInfo = pluginParts.getPluginPackageManager().getApplicationInfo(packageName, GET_META_DATA);
                    PluginClassLoader classLoader = pluginParts.getClassLoader();
                    Resources resources = pluginParts.getResources();

                    LoadPluginCallback.getCallback().afterLoadPlugin(partKey, applicationInfo, classLoader, resources);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });

        return future;
    }

此处其实复写了父类的loadPlugin,此处什么也没做,只是做一些事后的操作,关键还是super.loadPlugin(installedApk)这句代码

SamplePluginLoader继承了ShadowPluginLoader,所以是由ShadowPluginLoader完成了loadPlugin

    @Throws(LoadPluginException::class)
    open fun loadPlugin(
        installedApk: InstalledApk
    ): Future<*> {
        val loadParameters = installedApk.getLoadParameters()
        if (mLogger.isInfoEnabled) {
            mLogger.info("start loadPlugin")
        }
        // 在这里初始化PluginServiceManager
        mPluginServiceManagerLock.withLock {
            if (!::mPluginServiceManager.isInitialized) {
                mPluginServiceManager = PluginServiceManager(this, mHostAppContext)
            }

            mComponentManager.setPluginServiceManager(mPluginServiceManager)
        }

        return LoadPluginBloc.loadPlugin(
            mExecutorService,
            mComponentManager,
            mLock,
            mPluginPartsMap,
            mHostAppContext,
            installedApk,
            loadParameters
        )
    }

LoadPluginBloc这里面才是loadPlugin的核心逻辑

object LoadPluginBloc {
    @Throws(LoadPluginException::class)
    fun loadPlugin(
        executorService: ExecutorService,
        componentManager: ComponentManager,
        lock: ReentrantLock,
        pluginPartsMap: MutableMap<String, PluginParts>,
        hostAppContext: Context,
        installedApk: InstalledApk,
        loadParameters: LoadParameters
    ): Future<*> {
        if (installedApk.apkFilePath == null) {
            throw LoadPluginException("apkFilePath==null")
        } else {
            // 此处创建插件的classloader
            val buildClassLoader = executorService.submit(Callable {
                lock.withLock {
                    LoadApkBloc.loadPlugin(installedApk, loadParameters, pluginPartsMap)
                }
            })

            // 解析插件的四大组件信息
            val buildPluginManifest = executorService.submit(Callable {
                val pluginClassLoader = buildClassLoader.get()
                val pluginManifest = pluginClassLoader.loadPluginManifest()
                CheckPackageNameBloc.check(pluginManifest, hostAppContext)
                pluginManifest
            })

            // 构建插件的ApplicationInfo,运行时需要用到
            val buildPluginApplicationInfo = executorService.submit(Callable {
                val pluginManifest = buildPluginManifest.get()
                val pluginApplicationInfo = CreatePluginApplicationInfoBloc.create(
                    installedApk,
                    loadParameters,
                    pluginManifest,
                    hostAppContext
                )
                pluginApplicationInfo
            })

            // 构建插件的PackageManager
            val buildPackageManager = executorService.submit(Callable {
                val pluginApplicationInfo = buildPluginApplicationInfo.get()
                val hostPackageManager = hostAppContext.packageManager
                PluginPackageManagerImpl(
                    pluginApplicationInfo,
                    installedApk.apkFilePath,
                    componentManager,
                    hostPackageManager,
                )
            })

            // 构建插件的Resources
            val buildResources = executorService.submit(Callable {
                CreateResourceBloc.create(installedApk.apkFilePath, hostAppContext)
            })

            // 构建AppComponentFactory
            val buildAppComponentFactory = executorService.submit(Callable {
                val pluginClassLoader = buildClassLoader.get()
                val pluginManifest = buildPluginManifest.get()
                val appComponentFactory = pluginManifest.appComponentFactory
                if (appComponentFactory != null) {
                    val clazz = pluginClassLoader.loadClass(appComponentFactory)
                    ShadowAppComponentFactory::class.java.cast(clazz.newInstance())
                } else ShadowAppComponentFactory()
            })

            // 构建插件的application
            val buildApplication = executorService.submit(Callable {
                val pluginClassLoader = buildClassLoader.get()
                val resources = buildResources.get()
                val appComponentFactory = buildAppComponentFactory.get()
                val pluginManifest = buildPluginManifest.get()
                val pluginApplicationInfo = buildPluginApplicationInfo.get()

                CreateApplicationBloc.createShadowApplication(
                    pluginClassLoader,
                    loadParameters,
                    pluginManifest,
                    resources,
                    hostAppContext,
                    componentManager,
                    pluginApplicationInfo,
                    appComponentFactory
                )
            })

            // 这个是任务的总流程,会使用上面的所有构建,完成一个插件运行时所需要的东西
            val buildRunningPlugin = executorService.submit {
                if (File(installedApk.apkFilePath).exists().not()) {
                    throw LoadPluginException("插件文件不存在.pluginFile==" + installedApk.apkFilePath)
                }
                val pluginPackageManager = buildPackageManager.get()
                val pluginClassLoader = buildClassLoader.get()
                val resources = buildResources.get()
                val shadowApplication = buildApplication.get()
                val appComponentFactory = buildAppComponentFactory.get()
                val pluginManifest = buildPluginManifest.get()
                lock.withLock {
                    componentManager.addPluginApkInfo(
                        pluginManifest,
                        loadParameters,
                        installedApk.apkFilePath,
                    )
                    pluginPartsMap[loadParameters.partKey] = PluginParts(
                        appComponentFactory,
                        shadowApplication,
                        pluginClassLoader,
                        resources,
                        pluginPackageManager
                    )
                    PluginPartInfoManager.addPluginInfo(
                        pluginClassLoader, PluginPartInfo(
                            shadowApplication, resources,
                            pluginClassLoader, pluginPackageManager
                        )
                    )
                }
            }

            return buildRunningPlugin
        }
    }
}

现在loadPlugin流程已走完,以上操作时在plugin进程完成。

此时回到宿主进程
sample-manager#com.tencent.shadow.sample.manager.SamplePluginManager#onStartActivity

loadPlugin结束后,需要callApplicationOnCreate。

InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);

loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);
loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_MAIN_APP);

callApplicationOnCreate(PART_KEY_PLUGIN_BASE);
callApplicationOnCreate(PART_KEY_PLUGIN_MAIN_APP);

.......

protected void callApplicationOnCreate(String partKey) throws RemoteException {
        Map map = mPluginLoader.getLoadedPlugin();
        Boolean isCall = (Boolean) map.get(partKey);
        if (isCall == null || !isCall) {
            mPluginLoader.callApplicationOnCreate(partKey);
        }
}

又通过mPluginLoader转到plugin进程,调用sample-loader中SamplePluginManager的callApplicationOnCreate

此处就一目了然,获取已经构建好的application,进行attachBaseContext,同时初始化contentProvider,再进行application的onCreate,完成application的初始化。

ShadowPluginLoader

    fun callApplicationOnCreate(partKey: String) {
        fun realAction() {
            val pluginParts = getPluginParts(partKey)
            pluginParts?.let {
                val application = pluginParts.application
                application.attachBaseContext(mHostAppContext)
                mPluginContentProviderManager.createContentProviderAndCallOnCreate(
                    application, partKey, pluginParts
                )
                application.onCreate()
            }
        }
        if (isUiThread()) {
            realAction()
        } else {
            val waitUiLock = CountDownLatch(1)
            mUiHandler.post {
                realAction()
                waitUiLock.countDown()
            }
            waitUiLock.await();
        }
    }

剩下的启动流程就比较简单了,构建好插件的Intent,通过convertActivityIntent将插件的Activity,转换为runtime内的PluginDefaultProxyActivity,然后通过mPluginLoader转到plugin进程,进行startActivity。

由于runtime已经装载进classloader,所以不会出现class not found的情况。

// 构建好插件的Intent
Intent pluginIntent = new Intent();
pluginIntent.setClassName(
        context.getPackageName(),
        className
);
if (extras != null) {
    pluginIntent.replaceExtras(extras);
}
// 将插件的Activity转换为runtime内的PluginDefaultProxyActivity
Intent intent = mPluginLoader.convertActivityIntent(pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 转到plugin进程,进行startActivity
mPluginLoader.startActivityInPluginProcess(intent);

总结

以上就是shadow的动态模式启动流程,但是这仅仅是Shadow的启动流程,activity是如何正常运作的,其余组件是如何运作的,都是可以去继续深入研究的。

BlackShadow

接入shadow需要大量的二次开发工作,其实一般小型项目其实并不想关心太多的逻辑和管理,只想开袋即食,奈何Shadow也并没有提供这方面的能力,所有开发者接入都需要二次开发才可以使用,所以花了点时间在Shadow的基础上包装了一层,几乎不需要任何二次开发,即可通过几个简单的接口使用与管理Shadow,屏蔽了Shadow所有的技术细节。

相关

博客文章:
腾讯Shadow浅析及应用及BlackShadow

Tencent Shadow:
https://github.com/Tencent/Shadow

基于Shadow的技术方案

BlackShadow使用的是非动态方案,支持同时最多10个插件运行,分别都是各自单独的进程。install与launch都有boolean返回值,可反馈出插件是否安装/启动成功。

未实现

  • Activity栈的管理,目前统一打开standard Activity
  • 多个插件共用一个进程

如何使用?

建议直接clone本项目查看项目结构。

1. clone Shadow

nnjun仓库与Tencent仓库没有技术性差异。

git clone https://github.com/Tencent/Shadow.git
或者
git clone https://github.com/nnjun/Shadow.git (建议使用这个)

2. 编译本地仓库

拉下仓库后,进入Shadow目录,将Shadow发布到本地maven仓库

./gradlew publish

3. 修改项目Shadow版本

修改Shadow版本为本地的版本,如果是拉取nnjun仓库则不需要改

https://github.com/CodingGay/BlackShadow/blob/main/build.gradle#L3

BlackShadow使用方法

在Application#attachBaseContext中初始化

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        BlackShadow.get().init(this);
    }

安装与启动

    InstallResult installResult = BlackShadow.get().installPlugin("plugin-key", new File(pluginAPk));
    if (installResult.isSuccess()) {
        Intent intent = new Intent();
        intent.xxxxxxxxxxxxx
        BlackShadow.get().launchPlugin("plugin-key", intent);
    }

其余接口

    // 仅启动application
    public boolean callApplication(String pluginKey)

    // 获取所有已安装的plugin
    public List<InstalledPlugin> getInstalledPlugins()

    // 获取某个已安装的plugin
    public InstalledPlugin getInstalledPlugin(String pluginKey);

    // 卸载某个plugin
    public void uninstallPlugin(String pluginKey)

    // 停止某个plugin
    public void stopPlugin(String pluginKey)

    // 停止所有plugin
    public void stopAllPlugin()

    // 获取正在运行的plugin
    public List<RunningPlugin> getRunningPlugins()

插件包名与宿主包名不相同的需求

由于Shadow内核要求,plugin与宿主的包名必须一致,否则会出现问题,然而我方产品可能会存在不同的渠道包不同的包名,但是插件没有必要分开很多份,所以BlackShadow是支持插件与宿主不同的包名,处理的方法是在install时如果不一样,BlackShaodw会自动将插件的包名改成与宿主相同,不需要额外开发,直接进行install即可,BlackShadow会自动处理该问题。

假如你也有这个需求,则需要自行修改Shaodw内核,或者直接使用nnjun仓库

https://github.com/nnjun/Shadow/commit/32636d2759bae1d1f241c8f43ffb769ff2ce5ef5

不是修改了包名了吗?为什么还需要修改内核?

因为Shadow的包名基准是由Shadow编译时生成的com.tencent.shadow.core.manifest_parser.PluginManifest文件来确定,BlackShadow只会修改Manifest中的包名,并不会修改PluginManifest.class内的硬编码包名,所以需要修改编译插件,否则无法运行。

如果你没有以上的场景,那么请无视上面这一段内容,直接使用即可。

  1. Hi there,

    My name is Mike from Monkey Digital,

    Allow me to present to you a lifetime revenue opportunity of 35%
    That's right, you can earn 35% of every order made by your affiliate for life.

    Simply register with us, generate your affiliate links, and incorporate them on your website, and you are done. It takes only 5 minutes to set up everything, and the payouts are sent each month.

    Click here to enroll with us today:
    https://www.monkeydigital.org/affiliate-dashboard/

    Think about it,
    Every website owner requires the use of search engine optimization (SEO) for their website. This endeavor holds significant potential for both parties involved.

    Thanks and regards
    Mike Vance

    Monkey Digital

  2. Howdy

    I have just took an in depth look on your blog.niunaijun.top for the current search visibility and saw that your website could use a boost.

    We will enhance your ranks organically and safely, using only state of the art AI and whitehat methods, while providing monthly reports and outstanding support.

    More info:
    https://www.digital-x-press.com/unbeatable-seo/

    Regards
    Mike Moore

    Digital X SEO Experts

  3. Hi there,

    I have reviewed your domain in MOZ and have observed that you may benefit from an increase in authority.

    Our solution guarantees you a high-quality domain authority score within a period of three months. This will increase your organic visibility and strengthen your website authority, thus making it stronger against Google updates.

    Check out our deals for more details.
    https://www.monkeydigital.co/domain-authority-plan/

    NEW: Ahrefs Domain Rating
    https://www.monkeydigital.co/ahrefs-seo/

    Thanks and regards
    Mike Flannagan

  4. Hi there

    Just checked your blog.niunaijun.top baclink profile, I noticed a moderate percentage of toxic links pointing to your website

    We will investigate each link for its toxicity and perform a professional clean up for you free of charge.

    Start recovering your ranks today:
    https://www.hilkom-digital.de/professional-linksprofile-clean-up-service/

    Regards
    Mike Alsopp
    Hilkom Digital SEO Experts
    https://www.hilkom-digital.de/

  5. This service is perfect for boosting your local business' visibility on the map in a specific location.

    We provide Google Maps listing management, optimization, and promotion services that cover everything needed to rank in the Google 3-Pack.

    More info:
    https://www.speed-seo.net/ranking-in-the-maps-means-sales/

    Thanks and Regards
    Mike Gill

    PS: Want a ONE-TIME comprehensive local plan that covers everything?
    https://www.speed-seo.net/product/local-seo-bundle/

  6. Good Day

    This is Mike Daniels

    Let me introduce to you our latest research results from our constant SEO feedbacks that we have from our plans:

    https://www.strictlydigital.net/product/semrush-backlinks/

    The new Semrush Backlinks, which will make your blog.niunaijun.top SEO trend have an immediate push.
    The method is actually very simple, we are building links from domains that have a high number of keywords ranking for them. 

    Forget about the SEO metrics or any other factors that so many tools try to teach you that is good. The most valuable link is the one that comes from a website that has a healthy trend and lots of ranking keywords.
    We thought about that, so we have built this plan for you

    Check in detail here:
    https://www.strictlydigital.net/product/semrush-backlinks/

    Cheap and effective

    Try it anytime soon

    Regards
    Mike Daniels

    mike@strictlydigital.net

  7. Hi there,

    My name is Mike from Monkey Digital,

    Allow me to present to you a lifetime revenue opportunity of 35%
    That's right, you can earn 35% of every order made by your affiliate for life.

    Simply register with us, generate your affiliate links, and incorporate them on your website, and you are done. It takes only 5 minutes to set up everything, and the payouts are sent each month.

    Click here to enroll with us today:
    https://www.monkeydigital.org/affiliate-dashboard/

    Think about it,
    Every website owner requires the use of search engine optimization (SEO) for their website. This endeavor holds significant potential for both parties involved.

    Thanks and regards
    Mike Cramer

    Monkey Digital

  8. Good Day

    I have just took a look on your SEO for blog.niunaijun.top for the current search visibility and saw that your website could use a boost.

    We will enhance your ranks organically and safely, using only state of the art AI and whitehat methods, while providing monthly reports and outstanding support.

    More info:
    https://www.digital-x-press.com/unbeatable-seo/

    Regards
    Mike Kingsman

    Digital X SEO Experts

  9. laoge 03-18

    第一第一

Theme Jasmine by Kent Liao