1. 程式人生 > >滴滴外掛化工具VirtualAPK原始碼解析之Activity

滴滴外掛化工具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的啟動就完成了!