前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android插件化——Activity的启动

Android插件化——Activity的启动

作者头像
Oceanlong
发布2018-07-03 13:17:56
7400
发布2018-07-03 13:17:56
举报

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

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

思路参考:VirtualAPK

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

代码语言:javascript
复制
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中,我们可以看到:

代码语言:javascript
复制
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);

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

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

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

代码语言:javascript
复制
    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方法。它原本是这样的:

代码语言:javascript
复制
    /**
     * 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作了如下操作:

代码语言:javascript
复制
    @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了一个方法:

代码语言:javascript
复制
    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时,方式是这样的:

代码语言:javascript
复制
Intent intent = new Intent();
intent.setClassName(this, "com.didi.virtualapk.demo.aidl.BookManagerActivity");
startActivity(intent);

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

这里就要说到execStartActivity了。

代码语言:javascript
复制
    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做了一些处理。让我们来看看它的实现:

代码语言:javascript
复制
    /**
     * 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

代码语言:javascript
复制
    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);

的实现正是:

代码语言:javascript
复制
 public static String getTargetActivity(Intent intent) {
        return intent.getStringExtra(Constants.KEY_TARGET_ACTIVITY);
    }

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


PS:如何绕过AndroidManifest?

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

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

代码语言:javascript
复制
    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中,做了检查。最终的异常抛出形式:

代码语言:javascript
复制
    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也会走到:

代码语言:javascript
复制
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。

代码语言:javascript
复制
<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方法中,

代码语言:javascript
复制
mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.12.14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PS:如何绕过AndroidManifest?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档