滴滴插件化工具VirtualAPK源码解析之Activity

时间:2021-07-19 08:51:02

上篇文章已经介绍了VirtualAPK的配置和使用,请参考https://blog.csdn.net/shineflowers/article/details/80167302
今天看下从源码角度分析下VirtualAPK是怎么启动插件APK的吧!从上篇文章我们已经知道,启动插件APK的核心代码如下:

// 加载Plugin.apk插件
PluginManager pluginManager = PluginManager.getInstance(this);

// 此处是当查看插件apk是否存在,如果存在就去加载(比如修改线上的bug,把插件apk下载到sdcard的根目录下取名为Plugin.apk)
File file = new File(getExternalStorageDirectory(), "Plugin.apk");
if (file.exists()) {
    try {
        pluginManager.loadPlugin(file);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

PluginManager的loadPlugin方法:

 /** * load a plugin into memory, then invoke it's Application. * @param apk the file of plugin, should end with .apk * @throws Exception */
  public void loadPlugin(File apk) throws Exception {
      if (null == apk) {
          throw new IllegalArgumentException("error : apk is null.");
      }

      if (!apk.exists()) {
          throw new FileNotFoundException(apk.getAbsolutePath());
      }

      LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
      if (null != plugin) {
          this.mPlugins.put(plugin.getPackageName(), plugin);
          // try to invoke plugin's application
          plugin.invokeApplication();
      } else {
          throw  new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
      }
  }

调用了LoadedPlugin.create方法,生成一个LoadedPlugin对象,这个对象里面就包含了插件APK的packageInfo,资源相关(AssetManager,Resources),DexClassLoader(加载类),四大组件相关集合(mActivityInfos,mServiceInfos,mReceiverInfos,mProviderInfos)

LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws PackageParser.PackageParserException {
   this.mPluginManager = pluginManager;
   this.mHostContext = context;
   this.mLocation = apk.getAbsolutePath();
   this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
   this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
   this.mPackageInfo = new PackageInfo();
   this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
   this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
   this.mPackageInfo.signatures = this.mPackage.mSignatures;
   this.mPackageInfo.packageName = this.mPackage.packageName;
   if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
       throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
   }
   this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
   this.mPackageInfo.versionName = this.mPackage.mVersionName;
   this.mPackageInfo.permissions = new PermissionInfo[0];
   this.mPackageManager = new PluginPackageManager();
   this.mPluginContext = new PluginContext(this);
   this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
   this.mResources = createResources(context, apk);
   this.mAssets = this.mResources.getAssets();
   this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());

   tryToCopyNativeLib(apk);

   // Cache instrumentations
   Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
   for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {
       instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
   }
   this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
   this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);

   // Cache activities
   Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
   for (PackageParser.Activity activity : this.mPackage.activities) {
       activityInfos.put(activity.getComponentName(), activity.info);
   }
   this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
   this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);

   // Cache services
   Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
   for (PackageParser.Service service : this.mPackage.services) {
       serviceInfos.put(service.getComponentName(), service.info);
   }
   this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
   this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);

   // Cache providers
   Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
   Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
   for (PackageParser.Provider provider : this.mPackage.providers) {
       providers.put(provider.info.authority, provider.info);
       providerInfos.put(provider.getComponentName(), provider.info);
   }
   this.mProviders = Collections.unmodifiableMap(providers);
   this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
   this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);

   // Register broadcast receivers dynamically
   Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
   for (PackageParser.Activity receiver : this.mPackage.receivers) {
       receivers.put(receiver.getComponentName(), receiver.info);

       try {
           BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
           for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
               this.mHostContext.registerReceiver(br, aii);
           }
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
   this.mReceiverInfos = Collections.unmodifiableMap(receivers);
   this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);
}

private void tryToCopyNativeLib(File apk) {
   Bundle metaData = this.mPackageInfo.applicationInfo.metaData;
   if (metaData != null && metaData.getBoolean("VA_IS_HAVE_LIB")) {
       PluginUtil.copyNativeLib(apk, mHostContext, mPackageInfo, mNativeLibDir);
   }
}

替换Activity

好了,插件APK已经加载完成了,接下来就是启动插件APK的Acitivity,插件Activity必然没有在宿主中注册,这样启动不会报错么?当然是肯定的,我们看看startActivity做了什么操作:
跟进startActivity的调用流程,会发现其最终会进入Instrumentation的execStartActivity方法,在execStartActivity方法中有checkStartActivityResult方法:
滴滴插件化工具VirtualAPK源码解析之Activity
这个错误的提示大家应该很熟悉,就是Activity没有再AndroidManifest.xml注册过提示的错误。那么怎么解决呢?我们可以在checkStartActivityResult之前,提前将Activity的ComponentName进行替换为占坑的名字不就好了么?我们看到检查的操作时发生在Instrumentation.java中,那么我们选择hook Instrumentation。回过头来看PluginManager.java

private void hookInstrumentationAndHandler() {
   try {
       Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
       if (baseInstrumentation.getClass().getName().contains("lbe")) {
           // reject executing in paralell space, for example, lbe.
           System.exit(0);
       }

       final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
       Object activityThread = ReflectUtil.getActivityThread(this.mContext);
       ReflectUtil.setInstrumentation(activityThread, instrumentation);
       ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
       this.mInstrumentation = instrumentation;
   } catch (Exception e) {
       e.printStackTrace();
   }
}

可以看到首先通过反射拿到了原本的Instrumentation对象,然后自己创建了一个VAInstrumentation对象,接下来就直接反射将VAInstrumentation对象设置给ActivityThread对象即可。
这样就完成了hook Instrumentation,之后调用Instrumentation的任何方法,都可以在VAInstrumentation进行拦截并做一些修改。上面说到,startActivity会最终执行Instrumentation的execStartActivity方法,由于我们又直接hook Instrumentation,所以直接看VAInstrumentation的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;

}

直接看markIntentIfNeeded方法:

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);
    }

可以直接看最后一行,intent通过setClassName替换启动的目标Activity了!这个stubActivity是由mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj)返回。

很明显,传入的参数launchMode、themeObj都是决定选择哪一个占坑类用的。

public String getStubActivity(String className, int launchMode, Theme theme) {
        String stubActivity= mCachedStubActivity.get(className);
        if (stubActivity != null) {
            return stubActivity;
        }

        TypedArray array = theme.obtainStyledAttributes(new int[]{
                android.R.attr.windowIsTranslucent,
                android.R.attr.windowBackground
        });
        boolean windowIsTranslucent = array.getBoolean(0, false);
        array.recycle();
        if (Constants.DEBUG) {
            Log.d("StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
        }
        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
        switch (launchMode) {
            case ActivityInfo.LAUNCH_MULTIPLE: {
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                if (windowIsTranslucent) {
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                }
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TOP: {
                usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TASK: {
                usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
                usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                break;
            }

            default:break;
        }

        mCachedStubActivity.put(className, stubActivity);
        return stubActivity;
    }

通过launchMode去选择占坑的类。

stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);

STUB_ACTIVITY_STANDARD值为”%s.A$%d”, corePackage值为com.didi.virtualapk.core,usedStandardStubActivity为数字值,所以最终类名格式为:com.didi.virtualapk.core.A1
再看一眼,VirtualAPK中的AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.didi.virtualapk.core" android:versionCode="1" android:versionName="1.0.0" >

    <uses-sdk  android:minSdkVersion="15" android:targetSdkVersion="15" />

    <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="com.didi.virtualapk.core.A$1" android:launchMode="standard" />
        <activity  android:name="com.didi.virtualapk.core.A$2" android:launchMode="standard" android:theme="@android:style/Theme.Translucent" />

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

        <!-- Stub Activities -->
        <activity  android:name="com.didi.virtualapk.core.C$1" android:launchMode="singleTask" />
        <activity  android:name="com.didi.virtualapk.core.C$2" android:launchMode="singleTask" />
        <activity  android:name="com.didi.virtualapk.core.C$3" android:launchMode="singleTask" />
        <activity  android:name="com.didi.virtualapk.core.C$4" android:launchMode="singleTask" />
        <activity  android:name="com.didi.virtualapk.core.C$5" android:launchMode="singleTask" />
        <activity  android:name="com.didi.virtualapk.core.C$6" android:launchMode="singleTask" />
        <activity  android:name="com.didi.virtualapk.core.C$7" android:launchMode="singleTask" />
        <activity  android:name="com.didi.virtualapk.core.C$8" android:launchMode="singleTask" />

        <!-- Stub Activities -->
        <activity  android:name="com.didi.virtualapk.core.D$1" android:launchMode="singleInstance" />
        <activity  android:name="com.didi.virtualapk.core.D$2" android:launchMode="singleInstance" />
        <activity  android:name="com.didi.virtualapk.core.D$3" android:launchMode="singleInstance" />
        <activity  android:name="com.didi.virtualapk.core.D$4" android:launchMode="singleInstance" />
        <activity  android:name="com.didi.virtualapk.core.D$5" android:launchMode="singleInstance" />
        <activity  android:name="com.didi.virtualapk.core.D$6" android:launchMode="singleInstance" />
        <activity  android:name="com.didi.virtualapk.core.D$7" android:launchMode="singleInstance" />
        <activity  android:name="com.didi.virtualapk.core.D$8" android:launchMode="singleInstance" />

        <!-- Local Service running in main process -->
        <service android:name="com.didi.virtualapk.delegate.LocalService" />

        <!-- Daemon Service running in child process -->
        <service  android:name="com.didi.virtualapk.delegate.RemoteService" android:process=":daemon" >
            <intent-filter>
                <action android:name="${applicationId}.intent.ACTION_DAEMON_SERVICE" />
            </intent-filter>
        </service>

        <provider  android:name="com.didi.virtualapk.delegate.RemoteContentProvider" android:authorities="${applicationId}.VirtualAPK.Provider" android:process=":daemon" />
    </application>

</manifest>

到这里就可以看到,替换我们启动的Activity为占坑Activity,将我们原本启动的包名,类名存储到了Intent中。

还原Activity

我们用占坑的Activity替换了插件的Activity,但占坑的Activity不是我们最终要启动的目标Activity。这里需要了解下Activity的启动流程,这里省略很多很多字,具体的Activity的启动流程可以去看看源码或者其他的博客,这里不展开描述,可以理解为,
最终会回调到Instrumentation.newActivity(),那么直接看VAInstrumentation的newActivity方法:

@Override
    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);
            String targetClassName = PluginUtil.getTargetActivity(intent);

            Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));

            if (targetClassName != null) {
                Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
                activity.setIntent(intent);

                try {
                    // for 4.1+
                    ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
                } catch (Exception ignored) {
                    // ignored.
                }

                return activity;
            }
        }

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

核心就是首先从intent中取出我们的目标Activity,然后通过plugin的ClassLoader去加载(还记得在加载插件时,会生成一个LoadedPlugin对象,其中会对应其初始化一个DexClassLoader)。

这样就完成了Activity的“偷梁换柱”。

@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);
    }

这个方法中设置了前面获取到的mResources、mBase(Context)、mApplication对象和屏幕方向等等。
至此,插件APK的Activity的启动就完成了!