Android開源框架原始碼鑑賞:VirtualAPK

蘇策發表於2019-03-04

關於作者

郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。

文章目錄

  • 一 VirtualAPK的初始化流程
  • 二 VirtualAPK的的載入流程
  • 三 VirtualAPK啟動元件的流程
    • 3.1 Activity
    • 3.2 Service
    • 3.3 Broadcast Receiver
    • 3.4 Content Provider

更多Android開源框架原始碼分析文章請參見Android open framwork analysis

從2012年開始,外掛化技術得到了很大的發展,究其原因,主要是因為隨著業務的增長,主工程變得越來越難以維護,而且隨著公司業務的擴充套件,原來的主應用也逐漸分化了多個子應用,研發團隊也由 一個變成多個,但是子應用仍然需要主應用的流量入口優勢,種種業務場景的需求,極大地促進了外掛化技術的發展。

就目前而言,主流的外掛化框架有以下幾種:

Android開源框架原始碼鑑賞:VirtualAPK

從上圖對比可以看出,有著不錯的表現的重點是360的DroidPlugin框架和滴滴的VirtualAPK框架,這兩家公司的業務型別不同,導致了這兩套框架的側重點也有所不同,具體說來:

  • DroidPlugin:DroidPlugin側重於載入第三方獨立外掛,例如微信,並且外掛不能訪問宿主的程式碼和資源。這也比較符合260應用市場的業務特點。
  • VirtualAPK:VirtualAPK側重於載入業務模組,業務模組通常和宿主都有一定的耦合關係,例如需要訪問宿主提供的訂單、賬號等資料資訊等,這也比較符合滴滴業務型的業務特點。

也就是說如果我們需要去載入一個內部業務模組,並且這個業務模組很難從主工程中完全解耦,那麼我們會優先選擇VirtualAPK這種方案。

A powerful and lightweight plugin framework for Android

官方網站:https://github.com/didi/VirtualAPK

原始碼版本:0.9.1

按照國際慣例,在分析VirtualAPK的原始碼實現之前,先吹一波它的優點?。如下所示:

完善的功能

  • Activity:支援顯示和隱式呼叫,支援Activity的theme和LaunchMode,支援透明主題;
  • Service:支援顯示和隱式呼叫,支援Service的start、stop、bind和unbind,並支援跨程式bind外掛中的Service;
  • Receiver:支援靜態註冊和動態註冊的Receiver;
  • ContentProvider:支援provider的所有操作,包括CRUD和call方法等,支援跨程式訪問外掛中的Provider。
  • 自定義View:支援自定義View,支援自定義屬性和style,支援動畫;
  • PendingIntent:支援PendingIntent以及和其相關的Alarm、Notification和AppWidget;
  • 支援外掛Application以及外掛manifest中的meta-data;
  • 支援外掛中的so。

優秀的相容性

  • 相容市面上幾乎所有的Android手機,這一點已經在滴滴出行客戶端中得到驗證;
  • 資源方面適配小米、Vivo、Nubia等,對未知機型採用自適應適配方案;
  • 極少的Binder Hook,目前僅僅hook了兩個Binder:AMS和IContentProvider,hook過程做了充分的相容性適配;
  • 外掛執行邏輯和宿主隔離,確保框架的任何問題都不會影響宿主的正常執行。

極低的入侵性

  • 外掛開發等同於原生開發,四大元件無需繼承特定的基類;
  • 精簡的外掛包,外掛可以依賴宿主中的程式碼和資源,也可以不依賴;
  • 外掛的構建過程簡單,通過Gradle外掛來完成外掛的構建,整個過程對開發者透明。

? 注:吹了這麼多,其實這套框架還是有瑕疵的,具體的問題,在分析原始碼的時候我們會講。

要理解一套框架,首先需要從整體去把握它,理解它的構造和層次劃分,然後逐個去分析,VirtualAPK的整體架構圖如下圖所示:

Android開源框架原始碼鑑賞:VirtualAPK

整體的原始碼結構也並不複雜,如下圖所示:

Android開源框架原始碼鑑賞:VirtualAPK

一 VirtualAPK的初始化流程

在使用VirtualAPK之前,我們需要多VirtualAPK進行初始化,如下所示:

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    PluginManager.getInstance(base).init();
}
複製程式碼

我們來分析了一下VirtualAPK初始化的過程中做了哪些事情,如下所示:

public class PluginManager {
     private PluginManager(Context context) {
         
           Context app = context.getApplicationContext();
           // 獲取Context
           if (app == null) {
               this.mContext = context;
           } else {
               this.mContext = ((Application)app).getBaseContext();
           }
           //初始化
           prepare();
       }
   
       private void prepare() {
           Systems.sHostContext = getHostContext();
           //1. hook物件Instrumentation。
           this.hookInstrumentationAndHandler();
           //2. 根據不同的Android版本分別從ActivityManagerNative中Hook物件IActivityManager。
           if (Build.VERSION.SDK_INT >= 26) {
               this.hookAMSForO();
           } else {
               this.hookSystemServices();
           }
       }
   }
}
複製程式碼

可以發現VirtualAPK在初始化的時候主要hook了兩個物件,如下所示:

  1. hook物件Instrumentation。
  2. 根據不同的Android版本分別從ActivityManagerNative中Hook物件IActivityManager。

首先是Instrumentation物件。為什麼要hook這個物件呢??這是因為Instrumentation物件在啟動Activity會有一套校驗過程,其中一條就是檢查Activity有沒有在Manifest檔案中進行註冊,如下所示:

public class Instrumentation {
      public static void checkStartActivityResult(int res, Object intent) {
           if (res >= ActivityManager.START_SUCCESS) {
               return;
           }
           
           switch (res) {
               case ActivityManager.START_INTENT_NOT_RESOLVED:
               case ActivityManager.START_CLASS_NOT_FOUND:
                   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?");
                   throw new ActivityNotFoundException(
                           "No Activity found to handle " + intent);
               case ActivityManager.START_PERMISSION_DENIED:
                   throw new SecurityException("Not allowed to start activity "
                           + intent);
               case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
                   throw new AndroidRuntimeException(
                           "FORWARD_RESULT_FLAG used while also requesting a result");
               case ActivityManager.START_NOT_ACTIVITY:
                   throw new IllegalArgumentException(
                           "PendingIntent is not an activity");
               case ActivityManager.START_NOT_VOICE_COMPATIBLE:
                   throw new SecurityException(
                           "Starting under voice control not allowed for: " + intent);
               case ActivityManager.START_NOT_CURRENT_USER_ACTIVITY:
                   // Fail silently for this case so we don't break current apps.
                   // TODO(b/22929608): Instead of failing silently or throwing an exception,
                   // we should properly position the activity in the stack (i.e. behind all current
                   // user activity/task) and not change the positioning of stacks.
                   Log.e(TAG,
                           "Not allowed to start background user activity that shouldn't be displayed"
                           + " for all users. Failing silently...");
                   break;
               default:
                   throw new AndroidRuntimeException("Unknown error code "
                           + res + " when starting " + intent);
           }
       } 
}
複製程式碼

這些異常資訊多半我們都很熟悉,其中have you declared this activity in your AndroidManifest.xml,就是沒有註冊Activity所報的異常,hook物件Instrumentation,然後替換掉裡面相應 的方法,來達到繞過檢查的目的,我們來看看hook的流程,如下所示:

public class PluginManager {
    
       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);
               }
   
               //自定義的VAInstrumentation重寫了newActivity()等邏輯。Instrumentation物件也被儲存了下載
               final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
               //獲取ctivityThread例項
               Object activityThread = ReflectUtil.getActivityThread(this.mContext);
               //用自定義的VAInstrumentation重物件替換掉ActivityThread裡的Instrumentation物件
               ReflectUtil.setInstrumentation(activityThread, instrumentation);
               ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
               this.mInstrumentation = instrumentation;
           } catch (Exception e) {
               e.printStackTrace();
           }
       } 
}
複製程式碼

在文章03Android元件框架:Android檢視容器Activity中,我們提到 過Instrumentation物件用來監控應用與系統的互動行為,Activity的建立也是在Instrumentation物件裡完成的,之所以要hook這個物件就是為了修改Activity建立邏輯。

這裡用自定義的VAInstrumentation重物件替換掉ActivityThread裡的Instrumentation物件,這樣當系統啟動Activity呼叫Instrumentation的newActivity()方法的時候就會呼叫自定義的VAInstrumentation 裡面的newActivity()方法。

public class PluginManager {
           
       // Android API 26及其以上
       private void hookAMSForO() {
           try {
               Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManager.class, null, "IActivityManagerSingleton");
               IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());
               ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);
           } catch (Exception e) {
               e.printStackTrace();
           }
       }
       
       // Android API 26以下
       private void hookSystemServices() {
             try {
                 Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null, "gDefault");
                 IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());
     
                 //從ActivityManagerNative中Hook物件IActivityManager
                 ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);
     
                 if (defaultSingleton.get() == activityManagerProxy) {
                     this.mActivityManager = activityManagerProxy;
                 }
             } catch (Exception e) {
                 e.printStackTrace();
             }
       }  
}
複製程式碼

除了Instrumentation物件,它還根據不同的Android版本分別從ActivityManagerNative中Hook物件IActivityManager,那麼這個IActivityManager物件是什麼呢??

我們之前在文章02Android元件框架:Android元件管理者ActivityManager中 曾經聊到過它是ActivityManagerService的代理物件,通過它可以和ActivityManagerService進行IPC通訊,並請求它完成一些元件管理上的工作。我們平時呼叫的startActivity()、startService()、bindService()等元件呼叫的方法 最終都是呼叫ActivityManagerService裡的方法來完成的。

以上便是VIrtualAPK的初始化流程,我們接著來看VIrtualAPK是如何去載入一個APK檔案的。?

二 VirtualAPK的的載入流程

VirtualAPK對於載入的APK檔案沒有額外的約束,只需要新增VirtualAPK的外掛進行編譯,如下所示:

apply plugin: 'com.didi.virtualapk.plugin'

virtualApk {
    packageId = 0x6f             // The package id of Resources.
    targetHost='source/host/app' // The path of application module in host project.
    applyHostMapping = true      // [Optional] Default value is true. 
}
複製程式碼

然後就可以呼叫PluginManager直接載入編譯完成的APK,被載入的APK在PluginManager裡是一個LoadedPlugin物件,VirtualAPK通過這些LoadedPlugin物件來管理APK,這些APK也可以 想在手機裡直接安裝的App一樣執行。

String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/Test.apk");
File plugin = new File(pluginPath);
PluginManager.getInstance(base).loadPlugin(plugin);
複製程式碼

APK的載入流程如下圖所示:

Android開源框架原始碼鑑賞:VirtualAPK

我們可以看到上面呼叫loadPlugin()方法去載入一個APK,我們來看一下它的實現。

public class PluginManager {
    
     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());
          }
  
          // 1. 載入APK檔案
          LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
          if (null != plugin) {
              this.mPlugins.put(plugin.getPackageName(), plugin);
              // try to invoke plugin's application
              // 2. 嘗試呼叫APK
              plugin.invokeApplication();
          } else {
              throw  new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
          }
      }  
}
複製程式碼

這裡呼叫了LoadedPlugin的create()方法去構建一個LoadedPlugin物件,所以的初始化操作都是在LoadedPlugin的構造方法裡完成的,如下所示:

public final class LoadedPlugin {
    
    LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws PackageParser.PackageParserException {
            this.mPluginManager = pluginManager;
            this.mHostContext = context;
            this.mLocation = apk.getAbsolutePath();
            // 1. 呼叫PackageParser解析APK,獲取PackageParser.Package物件。
            this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
            this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
            // 2. 構建PackageInfo物件。
            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];
            // 3. 構建PluginPackageManager物件。
            this.mPackageManager = new PluginPackageManager();
            this.mPluginContext = new PluginContext(this);
            this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
            // 4. 構建Resouces物件。
            this.mResources = createResources(context, apk);
            // 5. 構建ClassLoader物件。
            this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
    
            // 6. 拷貝so庫。
            tryToCopyNativeLib(apk);
    
            // 7. 快取Instrumentation物件。
            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()]);
    
            // 8. 快取APK裡的Activity資訊。
            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()]);
            
            // 9. 快取APK裡的Service資訊。
            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()]);
    
            // 10. 快取APK裡的Content Provider資訊。
            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()]);
    
            // 11. 將靜態的廣播轉為動態的。
            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()]);
        }
}
複製程式碼

整個LoadedPlugin物件構建的過程就是去解析APK裡的元件資訊,並快取起來,具體說來:

  1. 呼叫PackageParser解析APK,獲取PackageParser.Package物件。
  2. 構建PackageInfo物件。
  3. 構建PluginPackageManager物件。
  4. 構建Resouces物件。
  5. 構建ClassLoader物件。
  6. 拷貝so庫。
  7. 快取Instrumentation物件。
  8. 快取APK裡的Activity資訊。
  9. 快取APK裡的Service資訊。
  10. 快取APK裡的Content Provider資訊。
  11. 將靜態的廣播轉為動態的。

我們接著來看沒有在宿主App的Manifest裡註冊的四大元件時如何被啟動起來的。?

三 VirtualAPK啟動元件的流程

3.1 Activity

前面我們說過在初始化VirtualAPK的過程中使用自定義的VAInstrumentation在ActivityThread中替換掉了原生的Instrumentation物件,來達到hook到Activity啟動流程的目的,繞開Instrumentation 啟動Activity的校驗流程。

那麼VirtualAPK是如何繞過系統校驗的呢??

Virtual是採用佔坑的方式來繞過校驗的,它在庫裡的Manifest檔案裡定義了佔坑的檔案,如下所示:

 <application>
        <!-- Stub Activities -->
        <activity android:name=".A$1" android:launchMode="standard"/>
        <activity android:name=".A$2" android:launchMode="standard"
            android:theme="@android:style/Theme.Translucent" />

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

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

        <!-- Stub Activities -->
        <activity android:name=".D$1" android:launchMode="singleInstance"/>
        <activity android:name=".D$2" android:launchMode="singleInstance"/>
        <activity android:name=".D$3" android:launchMode="singleInstance"/>
        <activity android:name=".D$4" android:launchMode="singleInstance"/>
        <activity android:name=".D$5" android:launchMode="singleInstance"/>
        <activity android:name=".D$6" android:launchMode="singleInstance"/>
        <activity android:name=".D$7" android:launchMode="singleInstance"/>
        <activity android:name=".D$8" android:launchMode="singleInstance"/>
 </application>
複製程式碼

A、B、C、D分別代表standard、singleTop、singleTask和singleInstance四種啟動模式。

VirtualAPK製造一些假的Activity替身在Manifest檔案提前進行註冊佔坑,在啟動真正的Activity時候,再將Activity填到坑裡,以完成啟動Activity。我們來看看具體的是實現流程:

  1. execStartActivity()
  2. realExecStartActivity()
  3. newActivity()
  4. callActivityOnCreate()

以上四個方法都是啟動Activity的過程中必經的四個方法。

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
    
     public ActivityResult execStartActivity(
             Context who, IBinder contextThread, IBinder token, Activity target,
             Intent intent, int requestCode, Bundle options) {
         //1. 將隱式Intent轉換為顯式Intent,Virtual是通過intent.setClassName(this, "com.guoxiaoxing.plugin.MainActivity");這種
         //方式來啟動Activity的,這裡將包名封裝成真正的ComponentName物件。
         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()));
             //2. 用註冊過的StubActivity替換掉真實的Activity以便繞過校驗。
             this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
         }
 
         //3. 生成了佔坑的StubActivity的Intent。呼叫realExecStartActivity()方法繼續執行Activity的啟動,藉此繞過校驗。
         ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                     intent, requestCode, options);
 
         return result;
     }
     
     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};
             
             // 用佔坑的StubActivity的Intent去啟動Activity,藉此繞過校驗。
             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;
     }
}
複製程式碼

該方法主要做了三件事情,如下所示:

  1. 將隱式Intent轉換為顯式Intent,Virtual是通過intent.setClassName(this, "com.guoxiaoxing.plugin.MainActivity");這種 方式來啟動Activity的,這裡將包名封裝成真正的ComponentName物件。
  2. 用註冊過的StubActivity替換掉真實的Activity以便繞過檢測。
  3. 生成了佔坑的StubActivity的Intent。呼叫realExecStartActivity()方法繼續執行Activity的啟動,藉此繞過校驗。

其中重點就在於註冊過的StubActivity替換掉真實的Activity以便繞過檢測,我們來看看它的實現,如下所示:

public class ComponentsHandler {
    
      public void markIntentIfNeeded(Intent intent) {
            if (intent.getComponent() == null) {
                return;
            }
    
            // 包名
            String targetPackageName = intent.getComponent().getPackageName();
            // 類名
            String targetClassName = intent.getComponent().getClassName();
            // 搜尋對應啟動模式的佔坑StubActivity
            if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
                // 做一個外掛的標記
                intent.putExtra(Constants.KEY_IS_PLUGIN, true);
                // 存包名
                intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
                // 存類名,之所以儲存這些資訊是為了以後獲取真正的Activity的Intent資訊去啟動真正的Activity。
                intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
                // 查詢StubActivity
                dispatchStubActivity(intent);
            }
        }
    
        private void dispatchStubActivity(Intent intent) {
            ComponentName component = intent.getComponent();
            String targetClassName = intent.getComponent().getClassName();
            // 獲取intent對應的LoadedPlugin物件
            LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
            // 根據ComponentName資訊獲取對應的ActivityInfo
            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);
            // 獲取對應的StubActivity
            String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
            Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
            // 設定StubActivity的全路徑名
            intent.setClassName(mContext, stubActivity);
        }

}
複製程式碼

我們來看看具體是如何查詢StubActivity的,如下所示:

class StubActivityInfo {
    
    // 標準模式Activity的最大個數
    public static final int MAX_COUNT_STANDARD = 1;
    // 棧頂複用模式Activity的最大個數
    public static final int MAX_COUNT_SINGLETOP = 8;
    // 棧內複用模式Activity的最大個數
    public static final int MAX_COUNT_SINGLETASK = 8;
    // 單例模式Activity的最大個數
    public static final int MAX_COUNT_SINGLEINSTANCE = 8;

    //那些佔坑的Activity的全路徑名
    public static final String corePackage = "com.didi.virtualapk.core";
    public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

    public final int usedStandardStubActivity = 1;
    public int usedSingleTopStubActivity = 0;
    public int usedSingleTaskStubActivity = 0;
    public int usedSingleInstanceStubActivity = 0;

    private HashMap<String, String> mCachedStubActivity = new HashMap<>();
    
    public String getStubActivity(String className, int launchMode, Theme theme) {
            // 1. 先從快取中查詢StuActivity。
            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);
            }
            
            // 標準啟動模式:com.didi.virtualapk.core.A$1
            stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
            switch (launchMode) {
                case ActivityInfo.LAUNCH_MULTIPLE: {
                   // 標準啟動模式:com.didi.virtualapk.core.$1,每次自增1
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                    if (windowIsTranslucent) {
                        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                    }
                    break;
                }
                case ActivityInfo.LAUNCH_SINGLE_TOP: {
                    // 棧頂複用模式:com.didi.virtualapk.core.$,每次自增1,範圍從1 - 8.
                    usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                    stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                    break;
                }
                case ActivityInfo.LAUNCH_SINGLE_TASK: {
                     // 棧內模式:com.didi.virtualapk.core.C$,每次自增1,範圍從1 - 8.
                    usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                    stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                    break;
                }
                case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
                    // 單例模式模式:com.didi.virtualapk.core.D$,每次自增1,範圍從1 - 8.
                    usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                    stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                    break;
                }
    
                default:break;
            }
    
            // 將查詢到的Activity放到快取中
            mCachedStubActivity.put(className, stubActivity);
            return stubActivity;
        }
}
複製程式碼

可以看著這裡邊完成了佔坑StubActivity的查詢,如下所示:

  • 標準啟動模式:com.didi.virtualapk.core.$1,每次自增1。
  • 棧頂複用模式:com.didi.virtualapk.core.$,每次自增1,範圍從1 - 8.
  • 棧內複用模式:com.didi.virtualapk.core.C$,每次自增1,範圍從1 - 8.
  • 單例模式模式:com.didi.virtualapk.core.D$,每次自增1,範圍從1 - 8.

既然這裡為了染過檢驗把要啟動的Activity變成了佔坑的StubActivity。那麼真正啟動Activity的時候就要再次變回來,我們接著分析。

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
        @Override
        public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
            try {
                cl.loadClass(className);
            } catch (ClassNotFoundException e) {
                // 1. 根據Intent查詢對應的LoadedPlugin。
                LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                // 2. 從Intent中提取真正的targetClassName。
                String targetClassName = PluginUtil.getTargetActivity(intent);
    
                Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));
    
                if (targetClassName != null) {
                    // 3. 這個mBase是我們上面儲存的原生的Instrumentation物件,呼叫它的newActivity()方法去完成Activity的構建,這
                    // 相當於是一個動態代理模式。getClassLoader()是自己構建的一個DexClassLoader類,專門用來載入APK裡面的類。
                    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);
        }
}
複製程式碼

VAInstrumentation覆寫了Instrumentation的newActivity方法,這個方法主要做了三件事情:

  1. 根據Intent查詢對應的LoadedPlugin。
  2. 從Intent中提取真正的targetClassName。
  3. 這個mBase是我們上面儲存的原生的Instrumentation物件,呼叫它的newActivity()方法去完成Activity的構建,這 相當於是一個動態代理模式。getClassLoader()是自己構建的一個DexClassLoader類,專門用來載入APK裡面的類。

通過上面的分析,整體的思路就非常清晰了,如下所示:

提前在Manifest檔案裡註冊多個佔坑的StubActivity,校驗階段,將Intent裡的className替換成StubActivity,並儲存原有的Activity資訊,藉此通過校驗。啟動階段,再 從Intent中取出真正的Activity資訊,呼叫Instrumentation的newActivity()方法繼續執行Activity的啟動。

整體的思路還是挺機制的?,當然佔坑思想很早就有Android的同學提出來了,這也是實現外掛化的思路的一種。介紹完了Activity的啟動流程,我們接著來看Service的啟動流程。?

3.2 Service

Service的啟動採用動態代理AMS,攔截Service的相關操作請求,然後轉給ActivityManagerProxy處理,我們來看一看。

public class ActivityManagerProxy implements InvocationHandler {
    
       private Object startService(Object proxy, Method method, Object[] args) throws Throwable {
            // 獲取IApplicationThread物件。
            IApplicationThread appThread = (IApplicationThread) args[0];
            // 獲取跳轉Intent。
            Intent target = (Intent) args[1];
            // 檢查Service的資訊
            ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
            if (null == resolveInfo || null == resolveInfo.serviceInfo) {
                // is host service
                return method.invoke(this.mActivityManager, args);
            }
            
            return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
        }
        
        private ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
            Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
            return mPluginManager.getHostContext().startService(wrapperIntent);
        }
    
        private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
            // fill in service with ComponentName
            target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
            String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();
    
            // 判斷交給LocalService處理還是交給RemoteService處理,LocalService和RemoteService分別對應是否
            // 在新程式中啟動Service。
            boolean local = PluginUtil.isLocalService(serviceInfo);
            Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
            Intent intent = new Intent();
            intent.setClass(mPluginManager.getHostContext(), delegate);
            intent.putExtra(RemoteService.EXTRA_TARGET, target);
            // 儲存一下這個command,分別對應不同的操作。
            intent.putExtra(RemoteService.EXTRA_COMMAND, command);
            intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
            if (extras != null) {
                intent.putExtras(extras);
            }
    
            return intent;
        }
}
複製程式碼

所以本質上講,不管是啟動、繫結還是關閉Intent,最終都是呼叫LocalService或者RemoteService裡的onStartCommand()方法進行操作請求的分發。

LocalService和RemoteService都已經在VirtualAPK的Manifest檔案裡進行了註冊,如下所示:

<application>
    <!-- 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>
</application>
複製程式碼

我們接著來看看它們倆的具體實現。

3.2.1 LocalService

public class LocalService extends Service {
    private static final String TAG = "LocalService";

    // 外掛APK裡的目標Service
    public static final String EXTRA_TARGET = "target";
    public static final String EXTRA_COMMAND = "command";
    public static final String EXTRA_PLUGIN_LOCATION = "plugin_location";

    public static final int EXTRA_COMMAND_START_SERVICE = 1;
    public static final int EXTRA_COMMAND_STOP_SERVICE = 2;
    public static final int EXTRA_COMMAND_BIND_SERVICE = 3;
    public static final int EXTRA_COMMAND_UNBIND_SERVICE = 4;

    private PluginManager mPluginManager;

    @Override
    public IBinder onBind(Intent intent) {
        return new Binder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 獲取PluginManager單例
        mPluginManager = PluginManager.getInstance(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (null == intent || !intent.hasExtra(EXTRA_TARGET) || !intent.hasExtra(EXTRA_COMMAND)) {
            return START_STICKY;
        }


        Intent target = intent.getParcelableExtra(EXTRA_TARGET);
        // 獲取command資訊
        int command = intent.getIntExtra(EXTRA_COMMAND, 0);
        if (null == target || command <= 0) {
            return START_STICKY;
        }

        // 獲取元件資訊
        ComponentName component = target.getComponent();
        // 根據元件資訊獲取對應的LoadedPlugin
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
        // ClassNotFoundException when unmarshalling in Android 5.1
        target.setExtrasClassLoader(plugin.getClassLoader());
        switch (command) {
            // 啟動Service
            case EXTRA_COMMAND_START_SERVICE: {
                // 獲取ActivityThread物件。
                ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
                // 獲取IApplicationThread物件。
                IApplicationThread appThread = mainThread.getApplicationThread();
                Service service;

                if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                    // 嘗試從ComponentsHandler裡獲取Service、
                    service = this.mPluginManager.getComponentsHandler().getService(component);
                } else {
                    try {
                        // 呼叫DexClassLoader載入Service類。
                        service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                        Application app = plugin.getApplication();
                        IBinder token = appThread.asBinder();
                        Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                        IActivityManager am = mPluginManager.getActivityManager();
                        // 呼叫attch()方法,繫結應用上下文。
                        attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                        // 回撥Service的onCreate()方法。
                        service.onCreate();
                        // 插入Service
                        this.mPluginManager.getComponentsHandler().rememberService(component, service);
                    } catch (Throwable t) {
                        return START_STICKY;
                    }
                }
                
                // 回撥Service的onStartCommand()方法。
                service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
                break;
            }
            
            // 繫結服務
            case EXTRA_COMMAND_BIND_SERVICE: {
                ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
                IApplicationThread appThread = mainThread.getApplicationThread();
                Service service = null;

                if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                    // 嘗試從ComponentsHandler裡獲取Service、
                    service = this.mPluginManager.getComponentsHandler().getService(component);
                } else {
                    try {
                        // 呼叫DexClassLoader載入Service類。
                        service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                        Application app = plugin.getApplication();
                        IBinder token = appThread.asBinder();
                        Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                        IActivityManager am = mPluginManager.getActivityManager();
                        // 呼叫attch()方法,繫結應用上下文。
                        attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                        // 回撥Service的onCreate()方法。
                        service.onCreate();
                        // 插入Service
                        this.mPluginManager.getComponentsHandler().rememberService(component, service);
                    } catch (Throwable t) {
                        t.printStackTrace();
                    }
                }
                try {
                    // 回撥Service的onBind()方法。
                    IBinder binder = service.onBind(target);
                    IBinder serviceConnection = PluginUtil.getBinder(intent.getExtras(), "sc");
                    IServiceConnection iServiceConnection = IServiceConnection.Stub.asInterface(serviceConnection);
                    if (Build.VERSION.SDK_INT >= 26) {
                        ReflectUtil.invokeNoException(IServiceConnection.class, iServiceConnection, "connected",
                                new Class[]{ComponentName.class, IBinder.class, boolean.class},
                                new Object[]{component, binder, false});
                    } else {
                        iServiceConnection.connected(component, binder);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            }
            // 停止服務
            case EXTRA_COMMAND_STOP_SERVICE: {
                // 從ComponentsHandler移除Service的記錄
                Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
                if (null != service) {
                    try {
                        // 回撥Service的onDestroy()方法
                        service.onDestroy();
                    } catch (Exception e) {
                        Log.e(TAG, "Unable to stop service " + service + ": " + e.toString());
                    }
                } else {
                    Log.i(TAG, component + " not found");
                }
                break;
            }
            case EXTRA_COMMAND_UNBIND_SERVICE: {
                 // 從ComponentsHandler移除Service的記錄
                Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
                if (null != service) {
                    try {
                        // 回撥Service的onUnbind()方法
                        service.onUnbind(target);
                        // 回撥Service的onDestroy()方法
                        service.onDestroy();
                    } catch (Exception e) {
                        Log.e(TAG, "Unable to unbind service " + service + ": " + e.toString());
                    }
                } else {
                    Log.i(TAG, component + " not found");
                }
                break;
            }
        }

        return START_STICKY;
    }

}
複製程式碼

你可以發現整個類的實現,就相當於重寫了Service啟動的一部分流程,主要包括上下文繫結和一些生命週期方法的回撥,這一塊的具體內容可以參照我們之前寫的文章05Android元件框架:Android後臺服務Service 除此之外,它還使用了ComponentsHandler來管理Service,畢竟我們只在Manifest中註冊了一個LocalService,ComponentsHandler主要用來插入和刪除Service以及管理ServiceConnection。這樣 就即便只註冊了一個LocalService,也可以啟動外掛APK裡的多個Service了。

我們接著來看看RemoteService,?

3.2.2 RemoteService

public class RemoteService extends LocalService {

    @Override
    public IBinder onBind(Intent intent) {
        // onBind()方法返回空,說明不能被繫結。
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null) {
            return super.onStartCommand(intent, flags, startId);
        }

        Intent target = intent.getParcelableExtra(EXTRA_TARGET);
        if (target != null) {
            String pluginLocation = intent.getStringExtra(EXTRA_PLUGIN_LOCATION);
            ComponentName component = target.getComponent();
            LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(component);
            if (plugin == null && pluginLocation != null) {
                try {
                    // 比LocalService多了一個從檔案載入APK外掛的操作
                    PluginManager.getInstance(this).loadPlugin(new File(pluginLocation));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        return super.onStartCommand(intent, flags, startId);
    }

}

複製程式碼

RemoteService繼承與於LocalService,它們倆的區別就在於onBind()和onStartCommand()方法的實現上,如下所示:

  • RemoteService的onBind()方法返回空,說明不能被繫結。
  • RemoteService的onStartCommand()方法比LocalService多了一個從檔案載入APK外掛的操作,也就是說它可以載入別的APK(別的程式)的Service。

3.3 Broadcast Receiver

Broadcast Receiver就比較簡單了,直接將靜態廣播轉為動態廣播,免去註冊的過程。

// 將靜態的廣播轉為動態的。
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();
    }
}
複製程式碼

3.4 Content Provider

VirtualAPK通過動態代理IContentProvider,攔截ContentProvider的相關操作請求,然後轉給PluginContentResolver來處理。IContentProvider物件的hook實際上是在PluginManager裡 完成的,如下所示:

public class PluginManager {
     private void hookIContentProviderAsNeeded() {
         
            // 拿到佔坑的Content Provider,然後主動去呼叫它的call()方法,call()方法
            // 會呼叫RemoteContentProvider的getContentProvider構建一個RemoteContentProvider。
            Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
            mContext.getContentResolver().call(uri, "wakeup", null, null);
            try {
                Field authority = null;
                Field mProvider = null;
                
                // 獲取ActivityThread物件
                ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);
                // 獲取ContentProvider Map
                Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");
                Iterator iter = mProviderMap.entrySet().iterator();
                // 變數查詢相應的ContentProvider      
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    Object key = entry.getKey();
                    Object val = entry.getValue();
                    String auth;
                    if (key instanceof String) {
                        auth = (String) key;
                    } else {
                        if (authority == null) {
                            authority = key.getClass().getDeclaredField("authority");
                            authority.setAccessible(true);
                        }
                        auth = (String) authority.get(key);
                    }
                    if (auth.equals(PluginContentResolver.getAuthority(mContext))) {
                        if (mProvider == null) {
                            mProvider = val.getClass().getDeclaredField("mProvider");
                            mProvider.setAccessible(true);
                        }
                        IContentProvider rawProvider = (IContentProvider) mProvider.get(val);
                        // 獲取對應的IContentProvider
                        IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                        mIContentProvider = proxy;
                        Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                        break;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
}
複製程式碼

ContentProvider也在Manifest檔案裡進行了佔坑註冊,如下所示:


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

獲取到IContentProvider物件後,就可以對它進行動態代理,攔截它裡面的操作,例如:query、insert、update、delete等操,在這些操作裡吧使用者呼叫的URI快取佔坑Provider的URI,再 把原來的URI作為引數拼接在佔坑Provider後面即可。這個替換和拼接的過程是由PluginContentResolver的wrapperUri()方法來完成的,如下所示:

public class PluginContentResolver extends ContentResolver {
        @Keep
        public static Uri wrapperUri(LoadedPlugin loadedPlugin, Uri pluginUri) {
            String pkg = loadedPlugin.getPackageName();
            String pluginUriString = Uri.encode(pluginUri.toString());
            StringBuilder builder = new StringBuilder(PluginContentResolver.getUri(loadedPlugin.getHostContext()));
            //先加入佔坑Provider的URI
            builder.append("/?plugin=" + loadedPlugin.getLocation());
            // 再將目標URI和packageName拼接上去
            builder.append("&pkg=" + pkg);
            builder.append("&uri=" + pluginUriString);
            Uri wrapperUri = Uri.parse(builder.toString());
            return wrapperUri;
        }
}
複製程式碼

可以發現上面佔坑的Provider是RemoteContentProvider,它繼承ContentProvider,相當於是它的代理類,如下所示:

public class RemoteContentProvider extends ContentProvider {
    
       private ContentProvider getContentProvider(final Uri uri) {
            final PluginManager pluginManager = PluginManager.getInstance(getContext());
            Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
            final String auth = pluginUri.getAuthority();
            // 1. 嘗試從快取中獲取ContentProvider。
            ContentProvider cachedProvider = sCachedProviders.get(auth);
            if (cachedProvider != null) {
                return cachedProvider;
            }
    
            synchronized (sCachedProviders) {
                // 2. 獲取LoadedPlugin物件。
                LoadedPlugin plugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
                if (plugin == null) {
                    try {
                        pluginManager.loadPlugin(new File(uri.getQueryParameter(KEY_PLUGIN)));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
    
                // 3. 從LoadedPlugin物件裡獲取Provider相關資訊ProviderInfo。
                final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0);
                if (providerInfo != null) {
                    RunUtil.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                LoadedPlugin loadedPlugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
                                // 4. 利用反射建立ContentProvider物件。
                                ContentProvider contentProvider = (ContentProvider) Class.forName(providerInfo.name).newInstance();
                                contentProvider.attachInfo(loadedPlugin.getPluginContext(), providerInfo);
                                // 5. 將ContentProvider物件存入快取中。
                                sCachedProviders.put(auth, contentProvider);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }, true);
                    return sCachedProviders.get(auth);
                }
            }
    
            return null;
        }
}
複製程式碼

整個構建ContentProvider物件的流程如下所示:

  1. 嘗試從快取中獲取ContentProvider。
  2. 獲取LoadedPlugin物件。
  3. 從LoadedPlugin物件裡獲取Provider相關資訊ProviderInfo。
  4. 利用反射建立ContentProvider物件。
  5. 將ContentProvider物件存入快取中。

我們再接著來看看RemoteContentProvider裡面的增刪改查操作,如下所示:

public class RemoteContentProvider extends ContentProvider {
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // 1. 通過傳入的URI生成一個新的Provider。
        ContentProvider provider = getContentProvider(uri);
        // 2. 拿到目標URI。
        Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
        if (provider != null) {
            // 3. 執行最終的查詢操作。
            return provider.query(pluginUri, projection, selection, selectionArgs, sortOrder);
        }

        return null;
    }
}
複製程式碼

query()方法的邏輯也十分簡單,如下所示:

  1. 通過傳入的URI生成一個新的Provider。
  2. 拿到目標URI。
  3. 執行最終的查詢操作。

好了,四大元件的啟動流程都分析完了,我們再來總結一下:

  • Activity:在宿主apk中提前佔坑,然後通過“欺上瞞下”的方式繞過校驗,啟動外掛APK裡的Activity;
  • Service:通過代理Service的方式去分發,VirtualAPK使用了兩個代理Service,即LocalService和RemoteService。
  • BroadcastReceiver:將靜態廣播轉為動態廣播。
  • ContentProvider:通過一個代理Provider進行操作的分發。

以上便是整個VrtualAPK框架的原理分析。

相關文章