Android進階(九)Activity外掛化和VirtualApk分析

猥瑣發育_別浪發表於2019-02-21

一、外掛化特點

1、優點

  • 將特定功能打包為外掛,當使用者需要使用某個特定功能時,才進行下載並開啟
  • 發版更靈活,可隨時發版
  • 組織架構更靈活,每個團隊負責自身的外掛開發
  • 開發中除錯速度更快,直接將外掛推入手機執行

2、侷限性

  • 穩定性不夠,通過hook方式,存在相容問題
  • 外掛化開發如果改動過大可能就需要發版

二、Activity啟動Hook點分析

Activity啟動過程重點是應用程式跟AMS進行通訊,處理完成後AMS再交給應用程式繼續處理。需要Hook的點就是在AMS呼叫之前跟MAS呼叫完成之後。

1、execStartActivity

#Instrumentation
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, String resultWho,
        Intent intent, int requestCode, Bundle options, UserHandle user) {
    .....
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        //呼叫AMS繼續啟動Activity
        int result = ActivityManager.getService()
            .startActivityAsUser(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, resultWho,
                    requestCode, 0, null, options, user.getIdentifier());
        //檢查啟動Activity的結果
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}
複製程式碼

在Activity啟動時,通過Instrumentation的checkStartActivityResult去檢查啟動的Activity的結果,如果外掛的Activity未在清單檔案中註冊,則會丟擲ActivityNotFoundException。需要解決的就是如何通過驗證?

2、ActivityThread

#ActivityThread
private class H extends Handler {
...
   public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    //呼叫了performLaunchActivity方法
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                ...
              }
...
}
複製程式碼
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {       
    ...
    //建立要啟動Activity的上下文環境
    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        //用類載入器來建立Activity的例項
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);//1
      ...
    } catch (Exception e) {
      ...
    }
  ...
    return activity;
}

複製程式碼

需要解決的是將需要載入的外掛Activity建立出來

三、VirtualApk原理分析

VirtualApk

1、初始化

在Application進行初始化操作

PluginManager.getInstance(base).init();
複製程式碼

在初始化操作時Hook了Instrumentation、ActivityThread的mH類的Callback、IActivityManager、DataBindingUtil

#PluginManager
protected PluginManager(Context context) {
    ......
    hookCurrentProcess();
}

protected void hookCurrentProcess() {
    hookInstrumentationAndHandler();
    hookSystemServices();
    hookDataBindingUtil();
}
複製程式碼

(1)hookInstrumentationAndHandler

#PluginManager
protected void hookInstrumentationAndHandler() {
    try {
        ActivityThread activityThread = ActivityThread.currentActivityThread();
        Instrumentation baseInstrumentation = activityThread.getInstrumentation();

        final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
        
        Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
        Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
        Reflector.with(mainHandler).field("mCallback").set(instrumentation);
        this.mInstrumentation = instrumentation;
        Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}

public class VAInstrumentation extends Instrumentation implements Handler.Callback {......}
複製程式碼
  • 建立VAInstrumentation,是Instrumentation的子類,實現了Handler.Callback方法
  • 通過反射將VAInstrumentation設定給ActivityThread, hook住了Instrumentation
  • 通過反射設定了Handler.Callback,攔截了ActivityThread的H的Callback

(2)hookSystemServices

protected void hookSystemServices() {
    try {
        Singleton<IActivityManager> defaultSingleton;
        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            defaultSingleton = Reflector.on(ActivityManager.class).field("IActivityManagerSingleton").get();
        } else {
            defaultSingleton = Reflector.on(ActivityManagerNative.class).field("gDefault").get();
        }
        IActivityManager origin = defaultSingleton.get();
        IActivityManager activityManagerProxy = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },
            createActivityManagerProxy(origin));

        // Hook IActivityManager from ActivityManagerNative
        Reflector.with(defaultSingleton).field("mInstance").set(activityManagerProxy);

        if (defaultSingleton.get() == activityManagerProxy) {
            this.mActivityManager = activityManagerProxy;
            Log.d(TAG, "hookSystemServices succeed : " + mActivityManager);
        }
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}
public class ActivityManagerProxy implements InvocationHandler {......}
複製程式碼
  • 建立了IActivityManager的動態代理物件ActivityManagerProxy
  • 通過反射來替換掉AMS的代理物件IActivityManager,來接管Activity啟動等操作

2、外掛載入

(1)loadPlugin
一般會將某個功能外掛生成jar或者apk檔案,然後交給主工程通過PluginManager的loadPlugin進行載入

#PluginManager
public void loadPlugin(File apk) throws Exception {
    ......
    //將外掛檔案轉換為一個LoadedPlugin物件
    LoadedPlugin plugin = createLoadedPlugin(apk);
    
    if (null == plugin) {
        throw new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
    }
    //將外掛LoadedPlugin存入
    this.mPlugins.put(plugin.getPackageName(), plugin);
    ......
}
複製程式碼

(2)構建LoadedPlugin物件

#LoadedPlugin
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
    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;
    //建立PackageInfo物件
    this.mPackageInfo = new PackageInfo();
    this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
    this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
    ......
    this.mPackageManager = createPluginPackageManager();
    this.mPluginContext = createPluginContext(null);
    this.mNativeLibDir = getDir(context, Constants.NATIVE_DIR);
    this.mPackage.applicationInfo.nativeLibraryDir = this.mNativeLibDir.getAbsolutePath();
    //建立Resource
    this.mResources = createResources(context, getPackageName(), apk);
    //建立ClassLoader
    this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
    //拷貝so
    tryToCopyNativeLib(apk);

    // 快取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()]);

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

    // 快取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()]);

    // 快取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);

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

    // try to invoke plugin's application
    invokeApplication();
}
複製程式碼

建立PackageInfo、Resouces、ClassLoader物件,儲存Instrumentation、Activity、Service、Content Provider等資訊
(3)建立ClassLoader物件

#LoadedPlugin
protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
    File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
    String dexOutputPath = dexOutputDir.getAbsolutePath();
    //建立DexClassLoader用來載入外掛
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

    if (Constants.COMBINE_CLASSLOADER) {
        DexUtil.insertDex(loader, parent, libsDir);
    }

    return loader;
}
#DexUtil
public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    Object baseDexElements = getDexElements(getPathList(baseClassLoader));
    Object newDexElements = getDexElements(getPathList(dexClassLoader));
    //將宿主自身的dex檔案和外掛的dex檔案合併
    Object allDexElements = combineArray(baseDexElements, newDexElements);
    Object pathList = getPathList(baseClassLoader);
    //通過反射將合併後的dex檔案賦值給dexElements
    Reflector.with(pathList).field("dexElements").set(allDexElements);    
    insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
複製程式碼
  • 建立DexClassLoader物件
  • 將宿主和外掛Dex檔案合併,並通過反射賦值給dexElements
  • 然後外掛中的Activity等檔案就可以被載入了

3、定義佔位Activity

<activity android:exported="false" android:name="com.didi.virtualapk.delegate.StubActivity" android:launchMode="standard"/>
<!-- Stub Activities -->
<activity android:exported="false" android:name=".A$1" android:launchMode="standard"/>
<activity android:exported="false" android:name=".A$2" android:launchMode="standard"
    android:theme="@android:style/Theme.Translucent" />

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

<!-- Daemon Service running in child process -->
<service android:exported="false" 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:exported="false"
    android:name="com.didi.virtualapk.delegate.RemoteContentProvider"
    android:authorities="${applicationId}.VirtualAPK.Provider"
    android:process=":daemon" />
複製程式碼

在清單檔案中定了各種啟動模式的佔位Activity、Service、ContentProvider

4、將外掛Activity替換為佔位的Activity

(1)啟動Activity時會走到VAInstrumentation的execStartActivity方法

#VAInstrumentation
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {
    //替換為佔坑的Activity
    injectIntent(intent);
    //繼續走Instrumentation的execStartActivity方法
    return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);
}

protected void injectIntent(Intent intent) {
    //通過intent去匹配PluginManager中Activity的坑位
    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);
    }
}
複製程式碼

(2)將外掛Activity的相關資訊進行儲存

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);
    }
}
複製程式碼

(3)將外掛Activity替換為佔位的Activity進行啟動

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);
    //通過launchMode等資訊找到合適的佔位Activity
    String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
    Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
    intent.setClassName(mContext, stubActivity);
}
複製程式碼

接下來拿著佔位的Activity繼續跟AMS進行通訊

5、替換回目標外掛的Activity

(1)VAInstrumentation接收到ApplicationThread傳送的訊息

#VAInstrumentation
@Override
public boolean handleMessage(Message msg) {
    if (msg.what == LAUNCH_ACTIVITY) {
        // ActivityClientRecord r
        Object r = msg.obj;
        try {
            Reflector reflector = Reflector.with(r);
            Intent intent = reflector.field("intent").get();
            intent.setExtrasClassLoader(mPluginManager.getHostContext().getClassLoader());
            //獲取ActivityInfo
            ActivityInfo activityInfo = reflector.field("activityInfo").get();       
            if (PluginUtil.isIntentFromPlugin(intent)) {
                int theme = PluginUtil.getTheme(mPluginManager.getHostContext(), intent);
                if (theme != 0) {
                    Log.i(TAG, "resolve theme, current theme:" + activityInfo.theme + "  after :0x" + Integer.toHexString(theme));
                    //更換thme
                    activityInfo.theme = theme;
                }
            }
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }

    return false;
}
複製程式碼

(2)通過VAInstrumentation的newActivity建立一個Activity物件

#VAInstrumentation
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);
        Log.i(TAG, String.format("newActivity[%s]", className));
        
    } catch (ClassNotFoundException e) {
        //佔位的Activity不存在,進入catch處理
        ComponentName component = PluginUtil.getComponent(intent);
        
        if (component == null) {
            return newActivity(mBase.newActivity(cl, className, intent));
        }
        //獲取目標外掛的Activity
        String targetClassName = component.getClassName();    
        LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);

        if (plugin == null) {
            // Not found then goto stub activity.
            boolean debuggable = false;
            try {
                Context context = this.mPluginManager.getHostContext();
                debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
            } catch (Throwable ex) {
    
            }

            if (debuggable) {
                throw new ActivityNotFoundException("error intent: " + intent.toURI());
            }
            
            Log.i(TAG, "Not found. starting the stub activity: " + StubActivity.class);
            return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent));
        }
        //通過Instrumentation的newActivity實現目標Activity的建立
        Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
        activity.setIntent(intent);

        // for 4.1+
        Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());

        return newActivity(activity);
    }

    return newActivity(mBase.newActivity(cl, className, intent));
}
複製程式碼

獲取目標Activity的ComponentName

#PluginUtil
public static ComponentName getComponent(Intent intent) {
    if (intent == null) {
        return null;
    }
    if (isIntentFromPlugin(intent)) {
        return new ComponentName(intent.getStringExtra(Constants.KEY_TARGET_PACKAGE),
            intent.getStringExtra(Constants.KEY_TARGET_ACTIVITY));
    }
    
    return intent.getComponent();
}
複製程式碼

獲取之前儲存到佔位Activity的相關引數資訊,並返回ComponentName,繼續執行Activity啟動操作

6、callActivityOnCreate

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
    injectActivity(activity);
    mBase.callActivityOnCreate(activity, icicle, persistentState);
}

protected void injectActivity(Activity activity) {
    final Intent intent = activity.getIntent();
    if (PluginUtil.isIntentFromPlugin(intent)) {
        Context base = activity.getBaseContext();
        try {
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
            Reflector.with(base).field("mResources").set(plugin.getResources());
            Reflector reflector = Reflector.with(activity);
            reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
            reflector.field("mApplication").set(plugin.getApplication());

            // set screenOrientation
            ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
            if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                activity.setRequestedOrientation(activityInfo.screenOrientation);
            }

            // for native activity
            ComponentName component = PluginUtil.getComponent(intent);
            Intent wrapperIntent = new Intent(intent);
            wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
            activity.setIntent(wrapperIntent);
            
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
}
複製程式碼

設定了修改了mResources、mBase(Context)、mApplication物件,最終執行了Activity的onCreate方法

7、Activity外掛化總結

(1)初始化時Hook住Instrumentation、ActivityThread.mH的Callback回撥 (2)在宿主工程的清單檔案中定義佔位Activity (3)載入外掛時,將外掛dex檔案和宿主dex檔案合併,反射賦值給PathList的dexElements,以便被ClassLoader載入 (4)啟動目標Activity過程中,VAInstrumentation將目標Activity替換為佔位Activity,並將目標Activity資訊作為引數儲存。從而通過對Activity的校驗,繼而繼續與AMS進行通訊。 (5)AMS處理完成後,傳遞到ApplicationThread中,通過VAInstrumentation攔截到該訊息,將佔位Activity替換為目標Activity,並將目標Activity進行建立,繼而進行後續操作。 (6)設定mResources、mBase(Context)、mApplication物件,最終呼叫到了Activity的onCreate方法

參考資料:

相關文章