replugin原理笔记

news2024/11/26 0:52:06

Replugin源码目录主要有4个工程组成,其组成如下图所示,包括2个gradle工程,2个Android library工程。

replugin-host-gradle
replugin-host-library
replugin-plugin-gradle
replugin-plugin-library
Replugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案。Replugin的hook点只有一处,确保了框架的兼容性、稳定性。使用Replugin宿主和插件都需引入其gradle插件,但是宿主、插件的改造接入都非常简便。

1 宿主的改造

replugin-host-gradle,宿主工程构建时需要依赖的gradle插件,主要作用是进行宿主应用的构建任务,生成带插件坑位的AndroidManifest.xml,插件坑位可在宿主工程的gradle文件的repluginHostConfig中进行配置。以及生成包含插件信息的plugins-builtin.json文件。其主要任务包括:
生成带 RePlugin 插件坑位的 AndroidManifest.xml(允许自定义数量)
生成 RepluginHostConfig 类,方便插件框架读取并自定义其属性
生成 plugins-builtin.json,json中含有插件应用的信息,包名,插件名,插件路径等。
主要实现在RePlugin.groovy的apply方法中:

public void apply(Project project) {
···
def appID = variant.generateBuildConfig.appPackageName
def newManifest = ComponentsGenerator.generateComponent(appID, config)
···
//json generate task
def generateBuiltinJsonTaskName = scope.getTaskName(AppConstant.TASK_GENERATE, "BuiltinJson")
def generateBuiltinJsonTask = project.task(generateBuiltinJsonTaskName)

generateBuiltinJsonTask.doLast {
    FileCreators.createBuiltinJson(project, variant, config)
}
generateBuiltinJsonTask.group = AppConstant.TASKS_GROUP
variant.outputs.each { output ->
    output.processManifest.doLast {
        output.processManifest.outputs.files.each { File file ->
            def manifestFile = null;
            //在gradle plugin 3.0.0之前,file是文件,且文件名为AndroidManifest.xml
            //在gradle plugin 3.0.0之后,file是目录,且不包含AndroidManifest.xml,需要自己拼接
            //除了目录和AndroidManifest.xml之外,还可能会包含manifest-merger-debug-report.txt等不相干的文件,过滤它
            if ((file.name.equalsIgnoreCase("AndroidManifest.xml") && !file.isDirectory()) || file.isDirectory()) {
                if (file.isDirectory()) {
                    //3.0.0之后,自己拼接AndroidManifest.xml
                    manifestFile = new File(file, "AndroidManifest.xml")
                } else {
                    //3.0.0之前,直接使用
                    manifestFile = file
                }
                //检测文件是否存在
                if (manifestFile != null && manifestFile.exists()) {
                    println "${AppConstant.TAG} handle manifest: ${manifestFile}"
                    def updatedContent = manifestFile.getText("UTF-8").replaceAll("</application>", newManifest + "</application>")
                    manifestFile.write(updatedContent, 'UTF-8')
                }
            }
        }
    }
}

replugin-host-gradle 插件的构建任务基于{productFlavors}{buildTypes}组合出多维构建任务,为每一个组合在AndroidManifest.xml中生成四大组件坑位。生成四大组件坑位的代码在ComponentsGenerator中.

def newManifest = ComponentsGenerator.generateComponent(appID, config)

 ComponentsGenerator的generateComponent方法主要实现在AndroidManifest中四大组件坑位的写入。

/* 透明坑 */
config.countTranslucentStandard.times {
    activity(
            "${name}": "${applicationID}.${infix}N1NRTS${it}",
            "${cfg}": "${cfgV}",
            "${exp}": "${expV}",
            "${ori}": "${oriV}",
            "${theme}": "${themeTS}")
}

循环进行坑位的生成,其原理基于Groovy的MarkupBuilder,组装出四大组件坑位的xml。宿主app编译完成后,可以在build\intermediates\manifests\full\ AndroidManifest.xml中看到四大组件的坑位。

2 插件的改造

replugin-plugin-gradle主要在插件应用的编译器基于Transform api 注入到编译流程中, 再通过Javassist对编译中间环节的 Java 字节码文件进行修改,以便实现编译期动态修改插件应用的目的。

replugin-plugin-gradle插件基于Gradle的Transform API,在编译期的构建任务流中,在class转为dex之前,插入一个Transform,基于Javassist技术实现对字节码文件的注入,修改Activity的继承关系、Provider和LocalBroadcastManager的调用代码为插件库的调用代码。

Javassist是一个Java字节码类库。Java的字节码是包含Java类与接口,并按照一定的顺序存在class文件中。
插件的工作原理主要在ReClassPlugin.grooovy文件中:

public class ReClassPlugin implements Plugin<Project> {

    @Override
public void apply(Project project) {
······
def transform = new ReClassTransform(project)
// 将 transform 注册到 android
android.registerTransform(transform)
······
}

在apply方法中,创建了很多的Task,主要用于插件调试。直接看到apply方法的最后的ReClassTransform。

public class ReClassTransform extends Transform {

Transform 是 Android Gradle API ,允许第三方插件在class文件转为dex文件前操作编译完成的class文件,这个API的引入是为了简化class文件的自定义操作而无需对Task进行处理。

ReClassTransform.groovy中的关键代码是transform方法中调用了doTransform方法,在其中遍历注入器进行注入。

def doTransform(Collection<TransformInput> inputs,
                TransformOutputProvider outputProvider,
                Object config,
                def injectors) {

    /* 初始化 ClassPool */
    Object pool = initClassPool(inputs)

    /* 进行注入操作 */
    Util.newSection()
    Injectors.values().each {
        if (it.nickName in injectors) {
            println ">>> Do: ${it.nickName}"
            // 将 NickName 的第 0 个字符转换成小写,用作对应配置的名称
            def configPre = Util.lowerCaseAtIndex(it.nickName, 0)
            doInject(inputs, pool, it.injector, config.properties["${configPre}Config"])
        } else {
            println ">>> Skip: ${it.nickName}"
        }
    }

    if (config.customInjectors != null) {
        config.customInjectors.each {
            doInject(inputs, pool, it)
        }
    }

    /* 重打包 */
    repackage()

    /* 拷贝 class 和 jar 包 */
    copyResult(inputs, outputProvider)

    Util.newSection()
}

 Replugin中的注入器如下图所示,每个注入器执行特定的注入任务,分别处理相应的class文件和jar文件。

Activity注入器LoaderActivityInjector,其中定义了Activity替换规则。即如果我在插件中写了一个WelcomeActivity extends Activity,则通过该注入器,最终继承关系会被改成WelcomeActivity extends PluginActivity。PluginActivity、PluginFragmentActivity等的定义在replugin-plugin-library工程中。

/* LoaderActivity 替换规则 */
def private static loaderActivityRules = [
        'android.app.Activity'                    : 'com.qihoo360.replugin.loader.a.PluginActivity',
        'android.app.TabActivity'                 : 'com.qihoo360.replugin.loader.a.PluginTabActivity',
        'android.app.ListActivity'                : 'com.qihoo360.replugin.loader.a.PluginListActivity',
        'android.app.ActivityGroup'               : 'com.qihoo360.replugin.loader.a.PluginActivityGroup',
        'android.support.v4.app.FragmentActivity' : 'com.qihoo360.replugin.loader.a.PluginFragmentActivity',
        'android.support.v7.app.AppCompatActivity': 'com.qihoo360.replugin.loader.a.PluginAppCompatActivity',
        'android.preference.PreferenceActivity'   : 'com.qihoo360.replugin.loader.a.PluginPreferenceActivity',
        'android.app.ExpandableListActivity'      : 'com.qihoo360.replugin.loader.a.PluginExpandableListActivity'
]

 LoaderActivityInjector的关键代码在handleActivity方法中。

private def handleActivity(ClassPool pool, String activity, String classesDir) {
        def clsFilePath = classesDir + File.separatorChar + activity.replaceAll('\\.', '/') + '.class'
        if (!new File(clsFilePath).exists()) {
            return
        }

        println ">>> Handle $activity"

        def stream, ctCls
        try {
            stream = new FileInputStream(clsFilePath)
            ctCls = pool.makeClass(stream);
/*
             // 打印当前 Activity 的所有父类
            CtClass tmpSuper = ctCls.superclass
            while (tmpSuper != null) {
                println(tmpSuper.name)
                tmpSuper = tmpSuper.superclass
            }
*/
            // ctCls 之前的父类
            def originSuperCls = ctCls.superclass

            /* 从当前 Activity 往上回溯,直到找到需要替换的 Activity */
            def superCls = originSuperCls
            while (superCls != null && !(superCls.name in loaderActivityRules.keySet())) {
                // println ">>> 向上查找 $superCls.name"
                ctCls = superCls
                superCls = ctCls.superclass
            }

            // 如果 ctCls 已经是 LoaderActivity,则不修改
            if (ctCls.name in loaderActivityRules.values()) {
                // println "    跳过 ${ctCls.getName()}"
                return
            }

            /* 找到需要替换的 Activity, 修改 Activity 的父类为 LoaderActivity */
            if (superCls != null) {
                def targetSuperClsName = loaderActivityRules.get(superCls.name)
                // println "    ${ctCls.getName()} 的父类 $superCls.name 需要替换为 ${targetSuperClsName}"
                CtClass targetSuperCls = pool.get(targetSuperClsName)

                if (ctCls.isFrozen()) {
                    ctCls.defrost()
                }
                ctCls.setSuperclass(targetSuperCls)

                // 修改声明的父类后,还需要方法中所有的 super 调用。
                ctCls.getDeclaredMethods().each { outerMethod ->
                    outerMethod.instrument(new ExprEditor() {
                        @Override
                        void edit(MethodCall call) throws CannotCompileException {
                            if (call.isSuper()) {
                                if (call.getMethod().getReturnType().getName() == 'void') {
                                    call.replace('{super.' + call.getMethodName() + '($$);}')
                                } else {
                                    call.replace('{$_ = super.' + call.getMethodName() + '($$);}')
                                }
                            }
                        }
                    })
                }

                ctCls.writeFile(CommonData.getClassPath(ctCls.name))
                println "    Replace ${ctCls.name}'s SuperClass ${superCls.name} to ${targetSuperCls.name}"
            }

        } catch (Throwable t) {
            println "    [Warning] --> ${t.toString()}"
        } finally {
            if (ctCls != null) {
                ctCls.detach()
            }
            if (stream != null) {
                stream.close()
            }
        }
    }

在Javassist中CtClass是一个class文件的抽象表述。一个CtClass(compile-time class)的实例是一个可以用来操作class文件的句柄。ClassPool是用来管理字节码的编辑,ClassPool是一个存放着代表class文件的CtClass类容器,它扫描读取了class文件并且构造了CtClass类,并且是后面响应修改class文件的入口。

如果想要修改一个class文件,用户必须实例化一个ClassPool类,并且使用ClassPool的get方法取得代表这个那个class文件的CtClass的引用。

在injectClass方法中遍历插件的AndroidManifest.xml中声明的所有Activity,在handleActivity方法中根据loaderActivityRules定义的替换规则,修改此Activity的父类为对应插件库中的父类。除了修改Activity的继承规则,还要对方法体进行修改。

3 插件Activity的启动过程

3.1 classLoader的hook

public static final void init(Application application) {
    setApplicationContext(application);

    PluginManager.init(application);

    sPluginMgr = new PmBase(application);
    sPluginMgr.init();

    Factory.sPluginManager = PMF.getLocal();
    Factory2.sPLProxy = PMF.getInternal();

    PatchClassLoaderUtils.patch(application);
}

 PatchClassLoaderUtils.patch(application)方法,进行classLoader的hook,替换为RePluginClassLoader。

// 获取mPackageInfo.mClassLoader
ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
if (oClassLoader == null) {
    if (LOGR) {
        LogRelease.e(PLUGIN_TAG, "pclu.p: nf mpi. mb cl=" + oBase.getClass() + "; mpi cl=" + oPackageInfo.getClass());
    }
    return false;
}

// 外界可自定义ClassLoader的实现,但一定要基于RePluginClassLoader类
ClassLoader cl = RePlugin.getConfig().getCallbacks().createClassLoader(oClassLoader.getParent(), oClassLoader);

// 将新的ClassLoader写入mPackageInfo.mClassLoader
ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);

 

接着还会进行守护进程(guardService)的启动,守护进程主要是通过启动ContentProvider组件来启动。在守护进程中进行加载插件,及相关信息保存。

3.2 Activity的启动、类加载

启动插件的Activity通过Replugin.startActivity(Context context, Intent intent)启动,执行PluginLibraryInternalProxy的startActivity方法

    public boolean startActivity(Context context, Intent intent, String plugin, String activity, int process, boolean download) {
        if (LOG) {
            LogDebug.d(PLUGIN_TAG, "start activity: intent=" + intent + " plugin=" + plugin + " activity=" + activity + " process=" + process + " download=" + download);
        }

        // 是否启动下载
        // 若插件不可用(不存在或版本不匹配),则直接弹出“下载插件”对话框
        // 因为已经打开UpdateActivity,故在这里返回True,告诉外界已经打开,无需处理
        if (download) {
            if (PluginTable.getPluginInfo(plugin) == null) {
                if (LOG) {
                    LogDebug.d(PLUGIN_TAG, "plugin=" + plugin + " not found, start download ...");
                }

                // 如果用户在下载即将完成时突然点按“取消”,则有可能出现插件已下载成功,但没有及时加载进来的情况
                // 因此我们会判断这种情况,如果是,则重新加载一次即可,反之则提示用户下载
                // 原因:“取消”会触发Task.release方法,最终调用mDownloadTask.destroy,导致“下载服务”的Receiver被注销,即使文件下载了也没有回调回来
                // NOTE isNeedToDownload方法会调用pluginDownloaded再次尝试加载
                if (isNeedToDownload(context, plugin)) {
                    return RePlugin.getConfig().getCallbacks().onPluginNotExistsForActivity(context, plugin, intent, process);
                }
            }
        }

        /* 检查是否是动态注册的类 */
        // 如果要启动的 Activity 是动态注册的类,则不使用坑位机制,而是直接动态类。
        // 原因:宿主的某些动态注册的类不能运行在坑位中(如'桌面'插件的入口Activity)
        if (LOG) {
            LogDebug.d("loadClass", "isHookingClass(" + plugin + " , " + activity + ") = "
                    + Factory2.isDynamicClass(plugin, activity));
        }

        if (Factory2.isDynamicClass(plugin, activity)) {
            intent.putExtra(IPluginManager.KEY_COMPATIBLE, true);
            intent.setComponent(new ComponentName(IPC.getPackageName(), activity));
            context.startActivity(intent);
            return true;
        }

        // 如果插件状态出现问题,则每次弹此插件的Activity都应提示无法使用,或提示升级(如有新版)
        // Added by Jiongxuan Zhang
        if (PluginStatusController.getStatus(plugin) < PluginStatusController.STATUS_OK) {
            if (LOG) {
                LogDebug.d(PLUGIN_TAG, "PluginLibraryInternalProxy.startActivity(): Plugin Disabled. pn=" + plugin);
            }
            return RePlugin.getConfig().getCallbacks().onPluginNotExistsForActivity(context, plugin, intent, process);
        }

        // 若为首次加载插件,且是“大插件”,则应异步加载,同时弹窗提示“加载中”
        // Added by Jiongxuan Zhang
        if (!RePlugin.isPluginDexExtracted(plugin)) {
            PluginDesc pd = PluginDesc.get(plugin);
            if (pd != null && pd.isLarge()) {
                if (LOG) {
                    LogDebug.d(PLUGIN_TAG, "PM.startActivity(): Large Plugin! p=" + plugin);
                }
                return RePlugin.getConfig().getCallbacks().onLoadLargePluginForActivity(context, plugin, intent, process);
            }
        }

        // WARNING:千万不要修改intent内容,尤其不要修改其ComponentName
        // 因为一旦分配坑位有误(或压根不是插件Activity),则外界还需要原封不动的startActivity到系统中
        // 可防止出现“本来要打开宿主,结果被改成插件”,进而无法打开宿主Activity的问题

        // 缓存打开前的Intent对象,里面将包括Action等内容
        Intent from = new Intent(intent);

        // 帮助填写打开前的Intent的ComponentName信息(如有。没有的情况如直接通过Action打开等)
        if (!TextUtils.isEmpty(plugin) && !TextUtils.isEmpty(activity)) {
            from.setComponent(new ComponentName(plugin, activity));
        }

        ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);
        if (cn == null) {
            if (LOG) {
                LogDebug.d(PLUGIN_TAG, "plugin cn not found: intent=" + intent + " plugin=" + plugin + " activity=" + activity + " process=" + process);
            }
            return false;
        }

        // 将Intent指向到“坑位”。这样:
        // from:插件原Intent
        // to:坑位Intent
        intent.setComponent(cn);

        if (LOG) {
            LogDebug.d(PLUGIN_TAG, "start activity: real intent=" + intent);
        }

        context.startActivity(intent);

        // 通知外界,已准备好要打开Activity了
        // 其中:from为要打开的插件的Intent,to为坑位Intent
        RePlugin.getConfig().getEventCallbacks().onPrepareStartPitActivity(context, from, intent);

        return true;
    }

其中会先检查插件的状态,若插件未加载,会回调让用户去加载。

ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);

接着寻找坑位,Replugin插件化技术最关键的2个技术点,一个是四大组件占坑,二是只有一个hook点classloader。 在PluginCommImpl加载插件的Activity时,通过调用远程方法进行坑位分配。

  public ComponentName loadPluginActivity(Intent intent, String plugin, String activity, int process) {

        ActivityInfo ai = null;
        String container = null;
        PluginBinderInfo info = new PluginBinderInfo(PluginBinderInfo.ACTIVITY_REQUEST);

        try {
            // 获取 ActivityInfo(可能是其它插件的 Activity,所以这里使用 pair 将 pluginName 也返回)
            ai = getActivityInfo(plugin, activity, intent);
            if (ai == null) {
                if (LOG) {
                    LogDebug.d(PLUGIN_TAG, "PACM: bindActivity: activity not found");
                }
                return null;
            }

            // 存储此 Activity 在插件 Manifest 中声明主题到 Intent
            intent.putExtra(INTENT_KEY_THEME_ID, ai.theme);
            if (LOG) {
                LogDebug.d("theme", String.format("intent.putExtra(%s, %s);", ai.name, ai.theme));
            }

            // 根据 activity 的 processName,选择进程 ID 标识
            if (ai.processName != null) {
                process = PluginClientHelper.getProcessInt(ai.processName);
            }

            // 容器选择(启动目标进程)
            IPluginClient client = MP.startPluginProcess(plugin, process, info);
            if (client == null) {
                return null;
            }

            // 远程分配坑位
            container = client.allocActivityContainer(plugin, process, ai.name, intent);
            if (LOG) {
                LogDebug.i(PLUGIN_TAG, "alloc success: container=" + container + " plugin=" + plugin + " activity=" + activity);
            }
        } catch (Throwable e) {
            if (LOGR) {
                LogRelease.e(PLUGIN_TAG, "l.p.a spp|aac: " + e.getMessage(), e);
            }
        }

        // 分配失败
        if (TextUtils.isEmpty(container)) {
            return null;
        }

        PmBase.cleanIntentPluginParams(intent);

        // TODO 是否重复
        // 附上额外数据,进行校验
//        intent.putExtra(PluginManager.EXTRA_PLUGIN, plugin);
//        intent.putExtra(PluginManager.EXTRA_ACTIVITY, activity);
//        intent.putExtra(PluginManager.EXTRA_PROCESS, process);
//        intent.putExtra(PluginManager.EXTRA_CONTAINER, container);

        PluginIntent ii = new PluginIntent(intent);
        ii.setPlugin(plugin);
        ii.setActivity(ai.name);
        ii.setProcess(IPluginManager.PROCESS_AUTO);
        ii.setContainer(container);
        ii.setCounter(0);
        return new ComponentName(IPC.getPackageName(), container);
    }

远程调用找到坑位后,将要启动的目标Activity、插件名称、坑位等信息保存到PluginIntent中,PluginIntent没发现什么大用处,保存了信息,但又没把信息返回给调用方,loadPluginActivity方法将坑位信息返回。

坑位的分配在PluginProcessPer.java中的allocActivityContainer方法中,再看bindActivity方法中继续调用其他方法进行坑位分配。

final String bindActivity(String plugin, int process, String activity, Intent intent) {
        /* 获取插件对象 */
        Plugin p = mPluginMgr.loadAppPlugin(plugin);
        if (p == null) {
            if (LOG) {
                LogDebug.w(PLUGIN_TAG, "PACM: bindActivity: may be invalid plugin name or load plugin failed: plugin=" + plugin);
            }
            return null;
        }

        /* 获取 ActivityInfo */
        ActivityInfo ai =   p.mLoader.mComponents.getActivity(activity);
······
// 自定义进程`
 if (ai.processName.contains(PluginProcessHost.PROCESS_PLUGIN_SUFFIX2)) {
    String processTail = PluginProcessHost.processTail(ai.processName);
    container = mACM.alloc2(ai, plugin, activity, process, intent, processTail);
} else {
    container = mACM.alloc(ai, plugin, activity, process, intent);
}

 首先根据插件名称去获取加载的插件信息,插件信息中包含待启动Activity的类名、主题、启动模式等信息。

再看PluginContainers类中的alloc2方法,根据Activity不同的启动模式,分配不同的坑位,宿主的gradle在编译阶段会给不同启动模式的Activity都生成一定数量的坑位。所以在分配坑位时,也根据当前要启动的Activity的启动模式去对应的坑位集合中去寻找坑位。

tring alloc2(ActivityInfo ai, String plugin, String activity, int process, Intent intent, String processTail) {
    // 根据进程名称,取得该进程对应的 PluginContainerStates
    ProcessStates states = mProcessStatesMap.get(processTail);

    ActivityState state;

    String defaultPluginTaskAffinity = ai.applicationInfo.packageName;
    if (LOG) {
        LogDebug.d(TaskAffinityStates.TAG, String.format("插件 %s 默认 TaskAffinity 为 %s", plugin, defaultPluginTaskAffinity));
        LogDebug.d(TaskAffinityStates.TAG, String.format("%s 的 TaskAffinity 为 %s", activity, ai.taskAffinity));
    }

    /* SingleInstance */
    if (ai.launchMode == LAUNCH_SINGLE_INSTANCE) {
        synchronized (mLock) {
            state = allocLocked(ai, states.mLaunchModeStates.getStates(ai.launchMode, ai.theme), plugin, activity, intent);
        }

    /* TaskAffinity */
    } else if (!defaultPluginTaskAffinity.equals(ai.taskAffinity)) { // 非默认 taskAffinity
        synchronized (mLock) {
            state = allocLocked(ai, states.mTaskAffinityStates.getStates(ai), plugin, activity, intent);
        }

    /* other mode */
    } else {
        synchronized (mLock) {
            state = allocLocked(ai, states.mLaunchModeStates.getStates(ai.launchMode, ai.theme), plugin, activity, intent);
        }
    }

    if (state != null) {
        return state.container;
    }
    return null;
}

真正执行坑位的方法allocLocked方法

private final ActivityState allocLocked(ActivityInfo ai, HashMap<String, ActivityState> map,
                                        String plugin, String activity, Intent intent) {
    ······

    // 新分配:找空白的,第一个
    for (ActivityState state : map.values()) {
        if (state.state == STATE_NONE) {
            if (LOG) {
                LogDebug.d(PLUGIN_TAG, "PACM: alloc empty container=" + state.container);
            }
            state.occupy(plugin, activity);
            return state;
        }
    }
    ·······

    // never reach here
    return null;
}

 在allocLocked方法中坑位Activity和真实要启动的Activity之间的对应关系,被保存在ActivityState类中,plugin是插件名,activity是待启动的插件中的activity,container是坑位。

static final class ActivityState {

    final String container;

    int state;

    String plugin;

    String activity;

    long timestamp;

    final ArrayList<WeakReference<Activity>> refs;
    ···
}

坑位container到底是什么样的一个东西呢,搜索container在哪里进行了赋值,发现container只在ActivityState 的构造函数中进行了赋值。搜索构造函数的调用方,可以在LaunchModeStates看到相关代码。

    /**
     * 初始化 LaunchMode 和 Theme 对应的坑位
     *
     * @param containers  保存所有 activity 坑位的引用
     * @param prefix      坑位前缀
     * @param launchMode  launchMode
     * @param translucent 是否是透明的坑
     * @param count       坑位数
     */
    void addStates(Map<String, ActivityState> allStates, HashSet<String> containers, String prefix, int launchMode, boolean translucent, int count) {
        String infix = getInfix(launchMode, translucent);
        HashMap<String, ActivityState> states = mStates.get(infix);
        if (states == null) {
            states = new HashMap<>();
            mStates.put(infix, states);
        }

        for (int i = 0; i < count; i++) {
            String key = prefix + infix + i;

            // 只有开启“详细日志”时才输出每一个坑位的信息,防止刷屏
            if (RePlugin.getConfig().isPrintDetailLog()) {
                LogDebug.d(TAG, "LaunchModeStates.add(" + key + ")");
            }

            ActivityState state = new ActivityState(key);
            states.put(key, state);
            allStates.put(key, state);
            containers.add(key);
        }
    }

可以看出,在循环中container = prefix + infix +i;infix的值可以在getInfix方法查看。继续往上查找调用关系,查看PluginContainers.java中的init方法。

    final void init(int process, HashSet<String> containers) {
        if (process != IPluginManager.PROCESS_UI
                && !PluginProcessHost.isCustomPluginProcess(process)
                && !PluginManager.isPluginProcess()) {
            return;
        }

        //prefix = 包名+“.loader.a.Activity”
        String prefix = IPC.getPackageName() + CONTAINER_ACTIVITY_PART; 

        // 因为自定义进程可能也会唤起使用 UI 进程的坑,所以这里使用'或'条件
        if (process == IPluginManager.PROCESS_UI || PluginProcessHost.isCustomPluginProcess(process)) {

            /* UI 进程标识为 N1 */
            String suffix = "N1";

            // Standard
            mLaunchModeStates.addStates(mStates, containers, prefix + suffix, LAUNCH_MULTIPLE, true, HostConfigHelper.ACTIVITY_PIT_COUNT_TS_STANDARD);
            mLaunchModeStates.addStates(mStates, containers, prefix + suffix, LAUNCH_MULTIPLE, false, HostConfigHelper.ACTIVITY_PIT_COUNT_NTS_STANDARD);
        ······
        }
    }

基本可以看出container的根据不同启动模式、主题等有所不同,但基本类似于:包名+“.loader.a.Activity”+“N1”+“NR”+“NTS”+i,i代表数字编号。可以看出来就是replugin- host-gradle插件在AndroidManifest.xml文件中生成的坑位。

插件Activity的坑位找到后,就调用startActivity通知AMS去启动坑位Activity,中间还有一大堆的流程略过,然后要进行类的加载。这时Replugin中的RepluginClassLoader开始发挥作用了。

RepluginClassLoader的loadClass方法最终会调用PmBase类中的loadClass方法。

    final Class<?> loadClass(String className, boolean resolve) {
        // 加载Service中介坑位
        if (className.startsWith(PluginPitService.class.getName())) {
            if (LOG) {
                LogDebug.i(TAG, "loadClass: Loading PitService Class... clz=" + className);
            }
            return PluginPitService.class;
        }

        //
        if (mContainerActivities.contains(className)) {
            Class<?> c = mClient.resolveActivityClass(className);
            if (c != null) {
                return c;
            }
            // 输出warn日志便于查看
            // use DummyActivity orig=
            if (LOGR) {
                LogRelease.w(PLUGIN_TAG, "p m hlc u d a o " + className);
            }
            return DummyActivity.class;
        }
      ······
}

再看看关键的PluginProcessPer类的resolveActivityClass方法,找到要真正启动的Activity类,并进行加载。

    final Class<?> resolveActivityClass(String container) {
        String plugin = null;
        String activity = null;

        // 先找登记的,如果找不到,则用forward activity
        PluginContainers.ActivityState state = mACM.lookupByContainer(container);
        ······
        plugin = state.plugin;
        activity = state.activity;
        Plugin p = mPluginMgr.loadAppPlugin(plugin);
        ······
        ClassLoader cl = p.getClassLoader();
        Class<?> c = null;
        try {
            c = cl.loadClass(activity);
        } catch (Throwable e) {
            if (LOGR) {
                LogRelease.e(PLUGIN_TAG, e.getMessage(), e);
            }
        }
        return c;
    }

 

而lookupByContainer方法在PluginContainer类中,前面查找坑位的时候,也是用的PluginContainer中的alloc2、allocLocked方法。坑位与真实待启动Activity的对应关系就保存在ActivityState中。
找到插件中真正待启动Activity的类信息后,进行类加载,然后就是Android系统Activity的启动流程。从而通过占坑骗过了AMS的校验,通过hook系统ClassLoader完成真实插件Activity类的加载,进而通过系统完成插件Activity的启动。

4、 总结

前面只是针对Replugin中的一些关键技术、感兴趣点进行了粗略的分析。Replugin的很多技术精髓还需更加仔细的研读。但通过上面的一些分析,对Replugin的主要技术及设计思想能有一个大概的了解。能加深对Android系统及一些相关技术的理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/429424.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 基础回顾(下)

C 基础回顾&#xff08;下&#xff09; 目录C 基础回顾&#xff08;下&#xff09;前言模板和泛型编程动态内存与数据结构动态内存数据结构继承与多态继承多态简单的输入输出工具与技术命名空间异常处理多重继承与虚继承时间和日期前言 C之前学过一点&#xff0c;但是很长时间…

进销存管理系统是什么?进销存管理系统优点?

库存管理不当导致物资浪费/过期/损坏&#xff0c;增加企业成本和风险&#xff1b; 无法有效监控销售和采购流程&#xff0c;交易的准确性和时效性不到位&#xff1b; 财务管理混乱&#xff1b; ...... 你是否遇到过以上问题&#xff1f; 进销存管理系统&#xff08;Inventory …

Softing FG-200——将FF H1现场总线集成到工业以太网

基金会现场总线FF&#xff08;FOUNDATION Fieldbus&#xff09;是专为过程自动化设计的通信协议&#xff0c;包含低速总线H1&#xff08;31.25kbits/s&#xff09;标准和高速以太网HSE&#xff08;High Speed Ethernet&#xff0c;100Mbits/s&#xff09;标准。FF H1主要针对于…

200左右蓝牙耳机有哪些推荐?质量好的平价蓝牙耳机分享

现在蓝牙耳机基本上都是人手必备的存在了&#xff0c;对比上千元的蓝牙耳机&#xff0c;两百左右价位蓝牙耳机才是更多人的优先选择、废话不多说&#xff0c;下面我就来为大家推荐几款200元上下&#xff0c;质量和口碑都好的蓝牙耳机&#xff0c;准备入手蓝牙耳机的小伙伴可以作…

Mac配置QT

Mac配置QT 前言&#xff1a; 系统版本&#xff1a;Ventura 13.2.1 (22D68) 先安装homebrew&#xff0c;参考&#xff1a; https://blog.csdn.net/ZCC361571217/article/details/127333754 Mac配置&#xff1a; 安装Qt与Qt Creator&#xff1a; 通过Homebrew安装(若没Homeb…

VL817S与之前其他型号的区别与改动

相对于VL817C0以及VL817B0来说&#xff0c;VL817S使用外部供电不需要接入5V&#xff0c;HUB 5V 请参考参考设计接地。内部3.3 LDO输出请悬空。1。2V LX和FB请悬空。如下所示&#xff0c;详见参考设计。 1、3.3V和1.2V之间的时序要求是怎么样的&#xff1f; 下图是VL817(S) 上电…

无线技术有哪些专业术语,看完本文=半个无线专家

无线技术是指通过无线电波或光波等无线传输媒介&#xff0c;实现信息、数据或信号的传递和通信的技术领域。在无线技术领域中&#xff0c;有许多专业术语用于描述和标识不同的技术和概念。 以下是常见的无线技术专业术语的简介&#xff1a; Wi-Fi&#xff08;无线局域网&#…

磁盘这列(Raid)

RAID介绍 RAID技术通过把多个硬盘设备组合成一个容量更大的、安全性更好的磁盘阵列。把数据切割成许多区段后分别放在不同的物理磁盘上&#xff0c;然后利用分散读写技术来提升磁盘阵列整体的性能&#xff0c;同时把多个重要数据的副本同步到不同的物理设备上&#xff0c;从而…

ImageNet使用方法(细节)自用!

学习记录&#xff0c;自用。 1. 下载数据集 点击以下链接下载种子文件&#xff0c;然后使用迅雷进行下载&#xff0c;仅下载勾选的文件即可。 https://hyper.ai/datasets/4889/c107755f6de25ba43c190f37dd0168dbd1c0877e 2. 解压 找到下载好的ILSVRC2012_img_train.tar 和…

移动端布局rem与vw的区别

目录 1. rem 2. rem的弊端与优点 3. rem布局前注意点 4. vw 5. vw单位和%单位对比 6. vw布局前注意点 7. vue项目中使用vw 1. rem 先简单说下rem&#xff0c;官当文档是这样说的&#xff1a; rem是css中的长度单位&#xff0c;1rem 根元素html的font-size值。当页面…

【笔试强训选择题】Day6.习题(错题)解析

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、Day6习题&#xff08;错题&#xff09;解析 二、Day6习题&#xff08;原题&#xff09;练习 总结 前言 一、Day6习题&#xff08;错题&#xff09;解析…

chatgpt智能提效职场办公--ppt怎么做

作者&#xff1a;虚坏叔叔 博客&#xff1a;https://xuhss.com 早餐店不会开到晚上&#xff0c;想吃的人早就来了&#xff01;&#x1f604; 导入PPT有 1.通过菜单导入 打开PowerPoint 找到菜单栏中的 点击"插入" 总结 最后的最后 以上是chatgpt能力的冰山一角。…

<Linux> 常用指令

文章目录ls 指令pwd命令cd 指令touch指令mkdir指令&#xff08;重要&#xff09;&#xff1a;rmdir指令 && rm 指令man指令cp指令mv指令cat指令more指令less指令&#xff08;重要&#xff09;head指令tail指令date指令Cal指令find指令&#xff1a; -namegrep指令zip/un…

Springboot —— 根据docx填充生成word文件,并导出pdf

文章目录前言将docx模板填充数据生成doc文件1、依赖引入2、doc文件转换docx&#xff0c;并标注别名3、编写java代码实现数据填充docx文件填充数据导出pdf(web)1、依赖引入2、字体文件3、编写工具类4、编写测试接口请求测试参考资料前言 在项目中碰见一个需求&#xff0c;需要将…

[4] 实现无头单向非循环链表

目录 一、框架 二、实现各个方法 三、测试各个方法 四、源码 一、框架 一个单向链表的节点&#xff0c;有数值域和下一个节点的地址 我们可以设计一个链表类&#xff0c;在这个链表类设计一个节点内部类&#xff0c;这里设计成内部类的形式&#xff0c;因为链表是由节点组…

《论文阅读》SetGNER:General Named Entity Recognition as Entity Set Generation

0.总结 不知道是不是大模型的流行还是什么其他原因&#xff0c;导致现在网上都没有人来分享NER模型的相关论文了~本文方法简单&#xff0c;代码应该也比较简单&#xff08;但是没见作者放出来&#xff09;。推荐指数&#xff1a;★★☆☆☆ 1. 动机 处理三种不同场景的NER 与…

python笔记:datetime

处理日期和时间 1 常量 MINYEAR datetime允许的最小年份 MAXYEAR datetime允许的最大年份 2 数据类型 datetime.date带有属性year,month,daydatetime.time带有属性hour,minute,second,microsecond,tzinfodatetime.datetime带有属性year,month,day,hour,minute,second,m…

【网络安全】文件包含漏洞

文件包含漏洞文件包含漏洞原理文件包含漏洞经常出现的函数尝试查看etc/passwd敏感文件渗透过程上传phpinfo和webshell到服务器并使用工具连接其他方式包含日志文件getshell包含环境变量getshell文件包含漏洞原理 文件包含漏洞是指&#xff0c;程序开发人员一般会把重复使用的函…

【C语言学习4——整型数据类型】

C语言学习4——整型数据类型整型数据类型用sizeof关键词来测量大小三位二进制表示的数值范围数值的补码表示法各种整型类型的数值范围是多少无符号整型整型数据类型 在上一节当中&#xff0c;我们遇到了用int关键词&#xff08;整数integer的缩写&#xff09;来表示一个整数的…

Python Qt5 入门教程

Python Qt5 入门教程 Python Qt5是一个强大的GUI工具包&#xff0c;可以用来设计各种桌面应用程序&#xff0c;包括图形用户界面、数据库应用程序等。本教程将带你入门Python Qt5&#xff0c;从安装开始到图形界面的设计以及常见的控件和事件。 安装 Python Qt5需要使用PyQt5…