Android插件化——Activity的启动

在之前的文章中,我们有讲过Android插件化加载资源。其核心思想是,通过仿照安装的流程,自行创建Resources,然后通过ResId去加载相应的资源。

同样,在启动插件Activity时,我们的思路也类似。通过仿照Activity的启动过程,我们自行创建Activity,“偷梁换柱”,交给系统去启动。

思路参考:VirtualAPK

为了成功地实施“偷梁换柱”我们首先要熟悉Activity的启动流程:

Activity.startActivity
Activity.startActivityForResult
Instrumentation.execStartActivity
ActivityManagerProxy.startActivity
---
ActivityManagerService.startActivity
ActivityStack.startActivityMayWait
ActivityStack.startActivityLocked
ActivityStack.startActivityUncheckedLocked
ActivityStack.resumeTopActivityLocked
ActivityStack.startPausingLocked
ApplicationThreadProxy.schedulePauseActivity
---
ApplicationThread.schedulePauseActivity
ActivityThread.queueOrSendMessage
H.handleMessage
ActivityThread.handlePauseActivity
ActivityManagerProxy.activityPaused
---
ActivityManagerService.activityPaused
ActivityStack.activityPaused
ActivityStack.completePauseLocked
ActivityStack.resumeTopActivityLokced
ActivityStack.startSpecificActivityLocked
ActivityStack.realStartActivityLocked
---
ApplicationThreadProxy.scheduleLaunchActivity
ApplicationThread.scheduleLaunchActivity
ActivityThread.queueOrSendMessage
H.handleMessage
ActivityThread.handleLaunchActivity
ActivityThread.performLaunchActivity
*AcitiviyB.onCreate

从上表中我们可以看到,Activity在启动时,第一次进入AMS之前只有四步。前两步是我们的外部接口,最后一步是Binder方法。

首先我们要明确一定,AMS是系统的服务,我们是不能改变的。如果AMS的行为被我们改变,手机中所有App的行为都会被改变,这就是病毒了。。。

我们可以看到在Activity的启动过程中,我们真正能改变的并不多。在交给AMS前,我们只能通过Intent带上我们自己的信息。然后在ActivityThread#performLaunchActivity中去修改行为。


我们知道Activity一如其他类一样,是由ClassLoader加载类的方式启动的。 在ActivityThread#performLaunchActivity中,我们可以看到:

            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);

Android在Instrumentation#newActivity中,用ClassLoader创建了Activity。

如果我们要插件式启动Activity,我们首先要改变这个行为。所以,VirtualAPK首先hook了Instrumentation的newActivity方法。我们要用自己的ClassLoader,自己的newActivity方法,去启动我们自己的Activity。

我们可以看一下VirtualAPK的代码以获得思路:

    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            // 获取已加载的插件,包含了插件的所有信息
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
            // 获取要启动的Activity的类名
            String targetClassName = PluginUtil.getTargetActivity(intent);

            if (targetClassName != null) {
                // 用插件的ClassLoader启动插件Activity
                Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
                // 设置intent
                activity.setIntent(intent);
                try {
                    // for 4.1+
                    // 设置插件ContextThemeWrapper的Resources
                    ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
                } catch (Exception ignored) {
                    // ignored.
                }
                return activity;
            }
        }
        return mBase.newActivity(cl, className, intent);
    }

这是VirtualAPK hook后的newActivity的代码,我自己添加了些简单的注释。我们可以看到,VirtualAPK将插件的ClassLoader和Resources封装在了LoadedPlugin中。所以,我们还是用Instrumentation即mBase来调用newActivity方法。只是我们传入了自己的ClassLoader类名Intent

这样创建的Activty因为没有安装,因此,是没有Resources的。打个不恰当的比方,它只是一个Activity的空壳。不过这个空壳是一个开始。我们接下来要做的工作,就是将它填满,让它和一个真的Activity一样。

获取插件的Resources的方法参考Android插件化——资源加载

在performLaunchActivity中,完成了newActivity后,在onCreate之前,我们会调用callActivityOnCreate方法。它原本是这样的:

    /**
     * Perform calling of an activity's {@link Activity#onCreate}
     * method.  The default implementation simply calls through to that method.
     * @param activity The activity being created.
     * @param icicle The previously frozen state (or null) to pass through to
     * @param persistentState The previously persisted state (or null)
     */
    public void callActivityOnCreate(Activity activity, Bundle icicle,
            PersistableBundle persistentState) {
        prePerformCreate(activity);
        activity.performCreate(icicle, persistentState);
        postPerformCreate(activity);
    }

这个方法,VirtualApk也进行了hook。在调用它之前,VirtualApk作了如下操作:

    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        final Intent intent = activity.getIntent();
        if (PluginUtil.isIntentFromPlugin(intent)) {
            Context base = activity.getBaseContext();
            try {
                LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
                ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
                ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
                ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());

                // set screenOrientation
                ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
                if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                    activity.setRequestedOrientation(activityInfo.screenOrientation);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        mBase.callActivityOnCreate(activity, icicle);
    }

VirtualApk在这个方法中通过反射做了较多的工作:

  • 给 Activity的Context 赋予 插件的Resources
  • 给 ContextWrapper 赋予 插件的Context
  • 给 Activity 赋予 插件的Application
  • 给 ContextThemeWrapper 赋予 插件的Context

最后输入了activity的屏幕旋转信息。

通过上面两个方法,我们得知VirtualApk是如何加载一个包外的Activity。

看到这里也许会有所疑问,类似PluginUtil.getComponent(intent)这样的信息又是从哪里来的呢?

其实在AMS启动前,我们还HOOK了一个方法:

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
        // null component is an implicitly intent
        if (intent.getComponent() != null) {
            Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                    intent.getComponent().getClassName()));
            // resolve intent with Stub Activity if needed
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
        }

        ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                    intent, requestCode, options);

        return result;

    }

在这个方法中,我们在调用execStartActivity之前会对intent进行处理。

我们可以来看一看execStartActivity的细节

首先,在VirtualApk启动Activity时,方式是这样的:

Intent intent = new Intent();
intent.setClassName(this, "com.didi.virtualapk.demo.aidl.BookManagerActivity");
startActivity(intent);

我们可以看到,我们是通过ClassName隐式调用的。但其实,BookManagerActivity也根本不在我们的宿主apk中,甚至他的apk尚未安装。那么,为了和前面讲的通过loadClass的方式加载Activity顺利打通,我们是如何操作的呢?

这里就要说到execStartActivity了。

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
        // null component is an implicitly intent
        if (intent.getComponent() != null) {
            Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                    intent.getComponent().getClassName()));
            // resolve intent with Stub Activity if needed
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
        }

        ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                    intent, requestCode, options);

        return result;

    }

execStartActivity中首先我们调用了:transformIntentToExplicitAsNeeded根据方法名猜测,它对我们的隐式intent做了一些处理。让我们来看看它的实现:

    /**
     * transform intent from implicit to explicit
     */
    public Intent transformIntentToExplicitAsNeeded(Intent intent) {
        ComponentName component = intent.getComponent();
        if (component == null
            || component.getPackageName().equals(mContext.getPackageName())) {
            ResolveInfo info = mPluginManager.resolveActivity(intent);
            if (info != null && info.activityInfo != null) {
                component = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
                intent.setComponent(component);
            }
        }

        return intent;
    }

我们可以看到,这个方法主要用于在目标包名与宿主包名一致时调用,将隐式intent转为显式intent。

我们继续看execStartActivity

    public void markIntentIfNeeded(Intent intent) {
        if (intent.getComponent() == null) {
            return;
        }

        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        // search map and return specific launchmode stub activity
        if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
            intent.putExtra(Constants.KEY_IS_PLUGIN, true);
            intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
            intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
            dispatchStubActivity(intent);
        }
    }

    private void dispatchStubActivity(Intent intent) {
        ComponentName component = intent.getComponent();
        String targetClassName = intent.getComponent().getClassName();
        LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
        ActivityInfo info = loadedPlugin.getActivityInfo(component);
        if (info == null) {
            throw new RuntimeException("can not find " + component);
        }
        int launchMode = info.launchMode;
        Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
        themeObj.applyStyle(info.theme, true);
        String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
        Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
        intent.setClassName(mContext, stubActivity);
    }

两个方法可以一起看一下。在这两个方法中,首先对于包名进行了判断。如果包名与宿主不相等,且与已加载的插件包名相等。即将插件的包名和类名以及ActivityInfo放入intent中。

而我们在前面讲的newActivity方法中,调用的:

String targetClassName = PluginUtil.getTargetActivity(intent);

的实现正是:

 public static String getTargetActivity(Intent intent) {
        return intent.getStringExtra(Constants.KEY_TARGET_ACTIVITY);
    }

至此,整个Activity启动hook的流程就完全清晰了。


PS:如何绕过AndroidManifest?

我们知道,通常来说,我们新建一个Activity都需要在AndroidManifest中进行注册,否则会报错。但是,我们的插件App没有安装,我们是如何绕过检查的呢?

首先,我们要了解Android是在哪里对Activity是否有注册进行检查的。先看Instrumentation#execStartActivity的代码:

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        if (mActivityMonitors != null) {
            synchronized (mSync) {
                final int N = mActivityMonitors.size();
                for (int i=0; i<N; i++) {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    if (am.match(who, null, intent)) {
                        am.mHits++;
                        if (am.isBlocking()) {
                            return requestCode >= 0 ? am.getResult() : null;
                        }
                        break;
                    }
                }
            }
        }
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess();
            int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
        }
        return null;
    }

最后一句,** checkStartActivityResult(result, intent);**中抛出了

have you declared this activity in your AndroidManifest.xml? 的异常。 因此,我们可以想到,就是上一句,startActivity中,做了检查。最终的异常抛出形式:

    if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
          throw new ActivityNotFoundException(
                  "Unable to find explicit activity class "
                  + ((Intent)intent).getComponent().toShortString()
                  + "; have you declared this activity in your AndroidManifest.xml?");

这一流程,我们的方法execStartActivity也会走到:

ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                    intent, requestCode, options);

    private ActivityResult realExecStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        ActivityResult result = null;
        try {
            Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
            int.class, Bundle.class};
            result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,
                    "execStartActivity", parameterTypes,
                    who, contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            if (e.getCause() instanceof ActivityNotFoundException) {
                throw (ActivityNotFoundException) e.getCause();
            }
            e.printStackTrace();
        }

        return result;
    }

上面一段代码中,我们可以看到,我们还是会调用系统的execStartActivity方法。那么,被检查,在所难免。

于是VirtualApk在library中,加入了一个AndroidManifest.xml。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.didi.virtualapk.core">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application>
        <!-- Stub Activities -->
        <activity android:name=".A$1" android:launchMode="standard"/>
        <activity android:name=".A$2" android:launchMode="standard"
            android:theme="@android:style/Theme.Translucent" />

        <!-- Stub Activities -->
        <activity android:name=".B$1" android:launchMode="singleTop"/>
        <activity android:name=".B$2" android:launchMode="singleTop"/>
        <activity android:name=".B$3" android:launchMode="singleTop"/>
        <activity android:name=".B$4" android:launchMode="singleTop"/>
        <activity android:name=".B$5" android:launchMode="singleTop"/>
        <activity android:name=".B$6" android:launchMode="singleTop"/>
        <activity android:name=".B$7" android:launchMode="singleTop"/>
        <activity android:name=".B$8" android:launchMode="singleTop"/>

这就是常说的,占坑。 在上面有提到的dispatchStubActivity方法中,

mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);

即是负责把目标Activity修改为坑中的名称,骗过系统。至此,Activity启动的坑就被VirtualApk一一绕过去了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏分享达人秀

RecyclerView点击事件处理

前面两期学习了RecyclerView的简单使用,并为其item添加了分割线。在实际运用中,无论是List还是Grid效果,基本都会伴随着一些点击操作,...

1929
来自专栏Vamei实验室

安卓第六夜 凡高的自画像

在上一讲中,我已经制作了一个简单的Android应用。项目的主要文件包括: MainActivity.java activity_main.xml 在这一讲,我...

1927
来自专栏程序员互动联盟

【Android基础】Fragment 详解之Fragment介绍

Fragment在Android 3.0( API 11)引入,是为了支持在大屏上显示更加动态、灵活的UI,比如在平板和电视上。Fragment可以看作是嵌套的...

3488
来自专栏飞雪无情的博客

Android Activity的生命周期

通过上一节“Android系列之四:Android项目的目录结构”我们已经知道了什么是Activity,那么为什么我们创建一个Activity的导出类的时候为什...

593
来自专栏分享达人秀

探究Fragment生命周期

一个Activity可以同时组合多个Fragment,一个Fragment也可被多个Activity 复用。Fragment可以响应自己的输入事件,并拥有...

2423
来自专栏编程思想之路

Android5.0以后隐式启动ServiceBug

以前写过一篇关于进程间通信的博客 通信之进程间通信-AIDL 当时用的还是4.2的系统,跨进程 的服务可以根据action进行启动 ...

1967
来自专栏james大数据架构

Android LayoutInflater详解

在实际开发中LayoutInflater这个类还是非常有用的,它的作用类似于findViewById()。不同点是LayoutInflater是用来找res/l...

1929
来自专栏开发之途

Android Activity要点(2)

1082
来自专栏Android知识点总结

2-VVI-材料设计之TabLayout下标签

665
来自专栏程序员叨叨叨

一个SingleTask与跳转传值引发的血案

后来想到,Activity A使用了SingleTask的launchMode,猜想可能跟这个有关,在执行界面跳转的时候,不会生成新的Activity A实例,...

601

扫码关注云+社区