插件化-插件Activity的启动

本文出自: https://github.com/SusionSuc/AdvancedAndroid

在上一节分析中,我们已经知道宿主已经加载了插件的资源、类。也就是说在宿主中是可以使用插件中的类的。但是对于启动Activity这件事就比较特殊了:

在Android中一个Activity必须在AndroidManifest.xml中注册才可以被启动,可是很明显的是插件中的Activity是不可能提前在宿主的manifest文件中注册的。也就是说直接在宿主中启动一个插件的Acitvity必定失败。那怎么办呢 ?
VirtualApk的实现方式是通过hook系统启动Activity过程中的一些关键点,绕过系统检验,并添加插件Activity相关运行环境等一系列处理,使插件Activity可以正常运行。

接下来的分析会涉及到Activity的启动源码相关知识,如果你还不是很熟悉可以先去回顾一下。这篇文章讲的也比较仔细 : https://www.kancloud.cn/digest/androidframeworks/127782

hook Instrumentation

在Activtiy的启动过程中Instrumentation有着至关重要的作用。它可以向ActivityManager请求一个Acitivity的启动、构造一个Activity对象等。VirtualApk就hook了这个类:

    final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
    Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);

hook了这个类后,就有办法把一个非正常的插件Activtiy包装成一个正常的Activity了。先来看一下一小段Activity启动源代码:

    //Activity.java
    void startActivityForResult(@RequiresPermission Intent intent, int requestCode,@Nullable Bundle options) {
        ......
        mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this,intent, requestCode, options);
        ......
    }

    //Instrumentation.java
    ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        ......
         ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
        .....
    }

Instrumentation带着要启动的Activity的Intent就去找ActivityManager启动Activity了。但这就有一个问题,还记得上一节插件APK的解析吗?系统也会对宿主apk进行解析,并保存包中声明的Activity信息。如果这个intent所启动的插件Activity。并不在宿主的Activity信息集合中,那么就会报此Activity并未在manifest文件中注册,下面这一小段就是ActivityManagerService对要启动的Activity进行校验的源码:

    //PackageManagerService.java
    final PackageParser.Package pkg = mPackages.get(pkgName);
    if (pkg != null) {
        //从宿主的包中查询是否注册过这个intent相关信息。
        result = filterIfNotSystemUser(mActivities.queryIntentForPackage(intent, resolvedType, flags, pkg.activities, userId), userId);
    }
    ......

插件Activity并没有在manifest文件中注册,所以怎么办呢? VirtualApk采用的方式是 :

  1. 提前在Manifest文件中注册一些Activity。简称这种Activity为 : "占坑 Activity"
  2. 在向ActivityManagerService提出启动Activtiy请求时,把插件的activity intent换成已经在manifest文件中注册的占坑Activity的intent
  3. Instrumentation真正构造Activity时,再换回来。即构造要启动的插件Activity

接下来我们看一下具体操作, 首先在manifest文件中注册一些Activity

    <activity android:exported="false" android:name=".A$1" android:launchMode="standard"/>
    <activity android:exported="false" android:name=".A$2" android:launchMode="standard"

你在manifest中随意注册Activity是ok的。并不会发生异常。你的项目有没有出现删除一个Activity时,忘记了删除manifest文件中相关注册信息的情况呢?

启动插件activity时,替换为已经注册的acitvity

我们前面也看到了,Instrumentation.execStartActivity()会像ActivityManagerService发起启动Activity的请求,因此我们只要hook这个方法,并替换掉intent为提前在manifest注册的Activity就可以了:

    @Override
    public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {
        injectIntent(intent); // 替换 插件Activity intent 为提前在manifest文件中注册的 activity intent
        return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);
    }

    
    protected void injectIntent(Intent intent) {
        ......
       PluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    }

即对一个将要启动的Activity的intent做了一些处理,看看做了什么处理:

    public void markIntentIfNeeded(Intent intent) {
        .....
        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        //这个Activity是插件的Activity
        if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
            intent.putExtra(Constants.KEY_IS_PLUGIN, true);
            intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName); //保留好真正要启动的Activity信息
            intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
            dispatchStubActivity(intent);
        }
    }

    private void dispatchStubActivity(Intent intent) {
        //根据要启动的activity的启动模式、主题,去选择一个 符合条件的`占坑Activity`
        String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
        intent.setClassName(mContext, stubActivity); //把intent的启动目标设置为这个”占坑Activity“
    }

即如果要启动的Activity是一个插件的Activity,那么就选择一个合适的"占坑Activity"。来作为真正要启动的对象,并在intent中保存真正要启动的插件Acitvity的信息。

好,到这里我们知道对于插件Activity的启动,通过hookInstrumentation.execStartActivity(),实际上向ActivityManagerService请求的启动的是一个占坑的Activity

经过上面这些操作,ActivityManagerService做过一些列处理后,会让APP真正来启动这个占坑Activity:

    //ActivityThread.java : 真正开始实例化Activity,并开始走生命周期相关方法
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        //实例化一个Activity
        activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
        ...
    }

这时候我们真的要去实例化这个"占坑Activity"吗?当然不会,我们要实例化的是"插件Activity", 所有VirtualApkhook了Instrumentation.newActivity()

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            ....
            //这个 intent是否是插件的intent
            ComponentName component = PluginUtil.getComponent(intent);  
                      
            if (component == null) {//不是插件的intent,具体实例化逻辑,交给父类去处理
                return newActivity(mBase.newActivity(cl, className, intent));
            }

            //拿到启动前保存的真正要启动的插件Activity的信息
            String targetClassName = component.getClassName();
            
            //使用插件的classloader来构造插件Activity
            Activity activity = mParentInstrumentation.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);

            // for 4.1+
            Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());

            return newActivity(activity); //这个方法其实是对已经实例化过的Activity做了一个缓存。
        }

        return newActivity(mBase.newActivity(cl, className, intent));
    }

cl.loadClass(className)是要实例化占坑Activity,这里肯定是会抛类找不到异常的,因为对于占坑Activity,我们只是在manifest文件中做了声明,实际上并没有对应的类。所以对于插件Activity的启动会走到catch代码中。

这里有一个需要注意的点: 即加载插件的类使用的是plugin.getClassLoader()。那么可不可以直接使用宿主的ClassLoader呢?其实是可以的,在前面文章插件APK解析时,我们已经看到VirtualApk支持把插件类加载器的pathList合并到宿主的pathList中,
因此这里直接mParentInstrumentation.newActivity(cl, targetClassName, intent)也是可以的。

如何让一个插件Activity正常运行?

VirtualApk通过对Instrumentation的hook,成功启动了一个插件Acitivity。可是要知道的是这个Activtiy毕竟是个小黑孩。想要正常运行,还是需要插件框架来给予支持的。

资源

是的,首先就是资源, 插件Activity的资源是怎么设置的呢? 我们来看一下一个Activity的资源是怎么设置的(Android 8.0):

ActivityThread.performLaunchActivity()方法中,先来看一下为一个Activity设置Context的过程:

    ContextImpl appContext = createBaseContextForActivity(r);
    ...
    activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    ...
    activity.attach(appContext, this, getInstrumentation(), r.token,.....);
    ...
    mInstrumentation.callActivityOnCreate(activity, r.state);

在创建ContextImpl时就设置了ContextImpl的资源, 即Acitivity的资源:

    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, .....;
        context.setResources(packageInfo.getResources());
        return context;
    }

即一个Activity的资源实际上来自apk资源,这是当然。但对于插件Activity的设置就有问题了。看performLaunchActivity()的执行流程,即context的资源设置的是app打包的时的资源。宿主app打出来的包,肯定不会包含插件的资源的。
因此插件的Activity现在它所拥有的资源是宿主的资源而不是插件的资源,因此,我们需要把插件Activity的资源换成插件的。那在哪一步换呢? VirtualApk是在Instrumentation.callActivityOnCreate换的:

    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        injectActivity(activity);
        mBase.callActivityOnCreate(activity, icicle);
    }
    protected void injectActivity(Activity activity) {
        final Intent intent = activity.getIntent();
        if (PluginUtil.isIntentFromPlugin(intent)) { //插件的intent
            Context base = activity.getBaseContext();
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
            Reflector.with(base).field("mResources").set(plugin.getResources());  //把插件Activity的资源换成自己的。插件拿宿主的资源没什么用

            Reflector reflector = Reflector.with(activity);
            reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext())); //把插件的 context 也换掉
            reflector.field("mApplication").set(plugin.getApplication()); /
            ....
        }
    }

对于资源相关更详细的了解,你可以看一下这篇文章 : https://www.notion.so/pengchengdasf/VirtualAPK-1fce1a910c424937acde9528d2acd537

可以看到上面的代码不仅把插件的资源替换为了自己的,并且还为插件重新设置了Context

Context的替换

一个Activity在运行是离不开Context的, 它的Context也是一个连接点。比如我们会使用Activity的Context来获取一个ContentResolver、资源、Theme。所有我们需要对插件Activity的Context做一个hook。来方便我们对于插件Activity的特殊处理。

对于所有的插件Activity, 在VirtualApk中它们的Context为PluginContext:

class PluginContext extends ContextWrapper {

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) {
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    }
    .....
    @Override
    public ContentResolver getContentResolver() {
        return new PluginContentResolver(getHostContext());
    }
    ....
    @Override
    public Resources getResources() {
        return this.mPlugin.getResources();
    }

    @Override
    public AssetManager getAssets() {
        return this.mPlugin.getAssets();
    }

    @Override
    public Resources.Theme getTheme() {
        return this.mPlugin.getTheme();
    }

    @Override
    public void startActivity(Intent intent) {
        ComponentsHandler componentsHandler = mPlugin.getPluginManager().getComponentsHandler();
        componentsHandler.transformIntentToExplicitAsNeeded(intent);
        super.startActivity(intent);
    }
}

这里我只是列了一些重要的点:

  1. 插件Activity的Context获取ResourcesAssetsTheme都是特殊处理的
  2. 插件Activity获取的ContentResolver也是被hook过的
  3. ....

对于插件Activity的启动,本文就只看个大体流程和关键点,对于具体的细节可以去看VirtualApk源码。

我们用一张图来总结插件Activity的启动过程:

插件Activity的启动.png

欢迎关注我的Android进阶计划 : https://github.com/SusionSuc/AdvancedAndroid

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