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= 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。



    
    
    
    
    

    
        
        
        

        
        
        
        
        
        
        
        
        

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

mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);

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

你可能感兴趣的:(Android插件化——Activity的启动)