滴滴外掛化工具VirtualAPK原始碼解析之Activity
上篇文章已經介紹了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方法:
這個錯誤的提示大家應該很熟悉,就是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的啟動就完成了!