ContentProvider插件化解決方案

  • 2019 年 10 月 8 日
  • 筆記

1.當要傳輸的數據量大小不超過1M的時候,使用Binder;數據量超過1M時,Binder就搞不定了,需要ContentProvider

2.ContentProvider就是一個資料庫引擎,向外界提供了CRUD的API

ContentProvider插件化

將靜態Provider手動安裝到宿主app中,把它們放在宿主的ContentProvider列表中,就可以使用了

/**   * 由於應用程式使用的ClassLoader為PathClassLoader   * 最終繼承自 BaseDexClassLoader   * 查看源碼得知,這個BaseDexClassLoader載入程式碼根據一個叫做   * dexElements的數組進行, 因此我們把包含程式碼的dex文件插入這個數組   * 系統的classLoader就能幫助我們找到這個類   *   * 這個類用來進行對於BaseDexClassLoader的Hook   * 類名太長, 不要吐槽.   * @author weishu   * @date 16/3/28   */  //***第一步:宿主app和插件app的dex合併到一起  public final class BaseDexClassLoaderHookHelper {        public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)              throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {          // 獲取 BaseDexClassLoader : pathList          Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");            // 獲取 PathList: Element[] dexElements          Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");            // Element 類型          Class<?> elementClass = dexElements.getClass().getComponentType();            // 創建一個數組, 用來替換原始的數組          Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);            // 構造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 這個構造函數          Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};          Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};          Object o = RefInvoke.createObject(elementClass, p1, v1);            Object[] toAddElementArray = new Object[] { o };          // 把原始的elements複製進去          System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);          // 插件的那個element複製進去          System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);            // 替換          RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);      }  }
public class ProviderHelper {        /**       * 解析Apk文件中的 <provider>, 並存儲起來       * 主要是調用PackageParser類的generateProviderInfo方法       *       * @param apkFile 插件對應的apk文件       * @throws Exception 解析出錯或者反射調用出錯, 均會拋出異常       */      public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {            //獲取PackageParser對象實例          Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");          Object packageParser = packageParserClass.newInstance();            // 首先調用parsePackage獲取到apk對象對應的Package對象          Class[] p1 = {File.class, int.class};          Object[] v1 = {apkFile, PackageManager.GET_PROVIDERS};          Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage",p1, v1);            // 讀取Package對象裡面的services欄位          // 接下來要做的就是根據這個List<Provider> 獲取到Provider對應的ProviderInfo          List providers = (List) RefInvoke.getFieldObject(packageObj, "providers");            // 調用generateProviderInfo 方法, 把PackageParser.Provider轉換成ProviderInfo            //準備generateProviderInfo方法所需要的參數          Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider");          Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");          Object defaultUserState = packageUserStateClass.newInstance();          int userId = (Integer) RefInvoke.invokeStaticMethod("android.os.UserHandle", "getCallingUserId");          Class[] p2 = {packageParser$ProviderClass, int.class, packageUserStateClass, int.class};            List<ProviderInfo> ret = new ArrayList<>();          // 解析出intent對應的Provider組件          for (Object provider : providers) {              Object[] v2 = {provider, 0, defaultUserState, userId};              //***第二步:把得到的Package對象轉換為我們需要的ProviderInfo類型對象***              ProviderInfo info = (ProviderInfo) RefInvoke.invokeInstanceMethod(packageParser, "generateProviderInfo",p2, v2);              ret.add(info);          }            return ret;      }        /**       * 在進程內部安裝provider, 也就是調用 ActivityThread.installContentProviders方法       *       * @param context you know       * @param apkFile       * @throws Exception       */      public static void installProviders(Context context, File apkFile) throws Exception {          List<ProviderInfo> providerInfos = parseProviders(apkFile);          //***第三步:把插件ContentProvider的packageName設置為當前apk的packageName          for (ProviderInfo providerInfo : providerInfos) {              providerInfo.applicationInfo.packageName = context.getPackageName();          }          //***第四步:把這些插件ContentProvider安裝到宿主App中          Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");            Class[] p1 = {Context.class, List.class};          Object[] v1 = {context, providerInfos};            RefInvoke.invokeInstanceMethod(currentActivityThread, "installContentProviders", p1, v1);      }  }

Hook的時機很重要,越早越好,不然外部app調用插件的ContentProvider就要等很久了

/**   * 一定需要Application,並且在attachBaseContext裡面Hook   * 因為provider的初始化非常早,比Application的onCreate還要早   * 在別的地方hook都晚了。   *   * @author weishu   * @date 16/3/29   */  public class UPFApplication extends Application {        @Override      protected void attachBaseContext(Context base) {          super.attachBaseContext(base);            try {              File apkFile = getFileStreamPath("plugin2.apk");              if (!apkFile.exists()) {                  Utils.extractAssets(base, "plugin2.apk");              }                File odexFile = getFileStreamPath("plugin2.odex");                // Hook ClassLoader, 讓插件中的類能夠被成功載入              BaseDexClassLoaderHookHelper.patchClassLoader(getClassLoader(), apkFile, odexFile);                //安裝插件中的Providers              ProviderHelper.installProviders(base, getFileStreamPath("plugin2.apk"));          } catch (Exception e) {              throw new RuntimeException("hook failed", e);          }      }  }

ContentProvider轉發機制

在當前app中定義一個StubContentProvider作為中轉,讓外界app調用當前app的StubContentProvider,再調用插件里的ContentProvider

/**   * 為了使得插件的ContentProvder提供給外部使用,我們需要一個StubProvider做中轉;   * 如果外部程式需要使用插件系統中插件的ContentProvider,不能直接查詢原來的那個uri   * 我們對uri做一些手腳,使得插件系統能識別這個uri;   *   * 這裡的處理方式如下:   *   * 原始查詢插件的URI應該為:   * content://host_auth/plugin_auth/path/query   * 例子 content://baobao222/jianqiang   *   * 如果需要查詢插件,替換為:   *   * content://plugin_auth/path/query   * 例子 content://jianqiang   *   * 也就是,我們把插件ContentProvider的資訊放在URI的path中保存起來;   * 然後在StubProvider中做分發。   *   * @param raw 外部查詢我們使用的URI   * @return 插件真正的URI   */  private Uri getRealUri(Uri raw) {      String rawAuth = raw.getAuthority();      if (!AUTHORITY.equals(rawAuth)) {          Log.w(TAG, "rawAuth:" + rawAuth);      }        String uriString = raw.toString();      uriString = uriString.replaceAll(rawAuth + '/', "");      Uri newUri = Uri.parse(uriString);      Log.i(TAG, "realUri:" + newUri);      return newUri;  }

這是ContentProvider獨有的URI機制,而且是簡單的字元串,所以很適合這種轉發機制

–摘自《android插件化開發指南》