Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 掌閱Android App插件補丁實踐(ZeusPlugin)

掌閱Android App插件補丁實踐(ZeusPlugin)

編輯:關於Android編程

遇到問題

65K方法數超限

隨著應用不斷迭代,業務線的擴展,應用越來越大,那麼很不幸,總有一天,當你編譯的時候,會遇到一個類似下面的錯誤:

Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

沒錯,這就是臭名昭著的65536方法數超限問題。當然,google也意識到這個問題,所以發布了MultiDex支持庫。喜大普奔,趕緊使用,問題解決?Too Young ! 使用過程中,你會發現MultiDex有不少坑:啟動時間過長、ANR/Crash。當然也有解決方法。但我只想說真的太….麻煩了,還能不能愉快地回家玩游戲了….

上線太慢,更新率太低

總所周知,Android APP發布流程較為漫長,一般需要經歷開發完成—上傳市場—審核—上線幾個階段,而且各個市場都各有各的政策和審核速度,每發一版都是一次煎熬呀。再者,Android APP的升級率跟Android系統升級率一樣,怎一個慢字了得。新版本要覆蓋80%左右,怎麼也需要兩周左右。

一上線就如臨大敵

以為應用上線就完事了?NO !相信大部分開發同學在應用上線的頭一周都是過得提心吊膽的,祈禱著不要出bug,用戶不要反饋問題。但往往事與願違,怎麼辦,趕緊出hotfix版本?

解決方案

就不賣關子了,是的,我們的解決方案是構建一套插件補丁的方案,期望可以無痛解決以上問題。插件化和補丁在目前看來是老生常談的東西了,市面上已經有一堆實現方案,如DroidPlugin、Small、Android-Plugin-Framework。掌閱研究插件化是從2014年中開始,研究補丁是從2016年初開始,相對來說,算是比較晚。直至目前,插件化方案已經達到相對成熟的階段,而補丁方案也已經上線。秉著開源的精神,我們的插件補丁方案最近已經在Github開源— ZeusPlugin。相對其他插件化和熱修復方案,ZeusPlugin最大特點是:簡單易懂,核心類只有6個,類總數只有13個,我們期望開發同學在使用這套方案的同時能理解所有的實現細節,在我們看來,這確實不是什麼晦澀難懂的東西。

原理

要實現插件補丁,其實無非就是要解決幾個問題:插件安裝、資源加載和類加載。這幾點,我們可以參考Android系統加載APK的實現原理。

Android系統加載APK

APK安裝過程

復制APK安裝包到data/app臨時目錄下,如vmdl648417937.tmp/base.apk;

解析應用程序的配置文件AndroidManifest.xml;

進行Dexopt並生成ODEX,如vmdl648417937.tmp/oat/arm/base.odex;

將臨時目錄(vmdl648417937.tmp)重命名為packageName + "-" + suffix,如com.test_1;

在PackageManagerService中將上述步驟生成的apk信息通過mPackages成員變量緩存起來;

mPackages是個ArrayMap,key為包名,value為PackageParser.Package(apk包信息)

在data/data目錄下創建對應的應用數據目錄。

啟動APK過程

點擊桌面App圖標,Launcher接收到點擊事件,獲取應用信息,通過Binder IPC向SystemService進程(即system_process)發起startActivity請求(ActivityManagerService(AMS)#startActivity); SystemServer(AMS) 向zygote進程請求啟動一個新進程(ActivityManagerService#startProcessLocked); Zygote進程fork出新的子進程(APP進程),在新進程中執行 ActivityThread 類的 main 方法; App進程創建ActivityThread實例,並通過Binder IPC向 SystemServer(AMS) 請求 attach 到 AMS; SystemServer(AMS) 進程在收到請求後,進行一系列准備工作後,再通過binder IPC向App進程發送bindApplication和scheduleLaunchActivity請求; App進程(ActivityThread)在收到bindApplication請求後,通過handler向主線程發送BIND_APPLICATION消息; 主線程在收到BIND_APPLICATION消息後,根據傳遞過來的ApplicationInfo創建一個對應的LoadApk對象(標志當前APK信息),然後創建ContextImpl對象(標志當前進程的環境),緊接著通過反射創建目標Application,並調用其attach方法,將ContextImpl對象設置為目標Application的上下文環境,最後調用Application的onCreate函數,做一些初始工作; App進程(ApplicationThread)在收到scheduleLaunchActivity請求後,通過handler向主線程發送LAUNCH_ACTIVITY消息; 主線程在收到LAUNCH_ACTIVITY消息後,通過反射機制創建目標Activity,並調用Activity的onCreate()方法。

以上分析都是基於Android 6.0的源碼,其他版本可能有少許差異,但不影響主流程,限於篇幅問題,在此不一一展開分析,只重點分析相關的關鍵幾個步驟。

為什麼提到Android系統加載APK的流程,因為分析完Android系統加載APK的流程,插件補丁方案也就基本能實現出來了,下面我展開說一下。

插件安裝

從APK安裝過程分析得知

配置文件AndroidManifest.xml是在應用安裝時就已經解析並記錄,所以插件的AndroidManifest.xml配置無法生效 每個APK安裝都是獨享空間的,不同APK、同一個APK的不同時間安裝都是完全獨立的。這樣做,個人覺得大大降低了系統的復雜度,而且清晰明了。在這點上, ZeusPlugin插件安裝策略幾乎就是仿照系統設計的。具體可以參考 ZeusPlugin源碼,在此不展開描述。

類加載

從上述啟動APK過程分析7、9可以得知,Application和Activity都是通過反射機制創建的,我們可以看看Application創建具體源碼實現:

ActivityThread#handleBindApplication

   private void handleBindApplication(AppBindData data) {
            ......
             //省略代碼
        .......
             //生成APK信息LoadedApk,即packageInfo
           data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
             //創建上下文環境
           final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
            ......
             //省略代碼
        .......
           try {
               // If the app is being launched for full backup or restore, bring it up in
               // a restricted environment with the base application class.
                //通過反射機制創建Application實例
               Application app = data.info.makeApplication(data.restrictedBackupMode, null);
               mInitialApplication = app;

            ......
                //省略代碼
             .......

               try {
                    //調用Application onCreate方法·
                   mInstrumentation.callApplicationOnCreate(app);
               } catch (Exception e) {
                   if (!mInstrumentation.onException(app, e)) {
                       throw new RuntimeException(
                           "Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
                         }
          }
      } finally {
          StrictMode.setThreadPolicy(savedPolicy);
      }
  }

我們再看看LoadedApk#makeApplication的實現

   public Application makeApplication(boolean forceDefaultAppClass,
              Instrumentation instrumentation) {
          if (mApplication != null) {
              return mApplication;
          }

          Application app = null;

          String appClass = mApplicationInfo.className;
          if (forceDefaultAppClass || (appClass == null)) {
              appClass = "android.app.Application";
          }

          try {
                //獲取ClassLoader
              java.lang.ClassLoader cl = getClassLoader();
              if (!mPackageName.equals("android")) {
                  initializeJavaContextClassLoader();
              }
              ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
              //使用獲取到的ClassLoader通過反射機制創建Application實例,其內部實現是通過     ClassLoader.loadClass(className)得到Application Class
              app = mActivityThread.mInstrumentation.newApplication(
                      cl, appClass, appContext);
              appContext.setOuterContext(app);
          } catch (Exception e) {
              if (!mActivityThread.mInstrumentation.onException(app, e)) {
                  throw new RuntimeException(
                      "Unable to instantiate application " + appClass
                      + ": " + e.toString(), e);
              }
          }
          mActivityThread.mAllApplications.add(app);
          mApplication = app;

         ......
          //省略代碼
       .......

          return app;
      }

從上述代碼可以得知,系統加載Application時候是先獲取一個特定ClassLoader,然後該ClassLoader通過反射機制創建Application實例。我們繼續看看getClassLoader()的實現

  public ClassLoader getClassLoader() {
          synchronized (this) {
              if (mClassLoader != null) {
                  return mClassLoader;
              }

              if (mIncludeCode && !mPackageName.equals("android")) {
                  ......
                 //省略代碼
              .......
               //創建ClassLoader
                  mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
                          mBaseClassLoader);

                  StrictMode.setThreadPolicy(oldPolicy);
              } else {
                  if (mBaseClassLoader == null) {
                      mClassLoader = ClassLoader.getSystemClassLoader();
                  } else {
                      mClassLoader = mBaseClassLoader;
                  }
              }
              return mClassLoader;
          }
      }

繼續跟蹤ApplicationLoaders類


      public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
      {

          ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

          synchronized (mLoaders) {
              if (parent == null) {
                  parent = baseParent;
              }

              /*
               * If we're one step up from the base class loader, find
               * something in our cache.  Otherwise, we create a whole
               * new ClassLoader for the zip archive.
               */
              if (parent == baseParent) {
                  ClassLoader loader = mLoaders.get(zip);
                  if (loader != null) {
                      return loader;
                  }

                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);

                  PathClassLoader pathClassloader =
                      new PathClassLoader(zip, libPath, parent);
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

                  mLoaders.put(zip, pathClassloader);
                  return pathClassloader;
              }

              Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
              PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
              Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
              return pathClassloader;
          }
      }

ApplicationLoaders是一個靜態緩存工具類,其內部維護了一個key為dexPath,value為PathClassLoader的ArrayMap,可以看到,應用程序使用的ClassLoader都是同一個PathClassLoader類的實例

我們繼續扒一扒PathClassLoader的源碼,發現其實現都在父類BaseDexClassLoader中,我們直接找到其findClass方法

  protected Class findClass(String name) throws ClassNotFoundException {
          List suppressedExceptions = new ArrayList();
          Class c = pathList.findClass(name, suppressedExceptions);
          if (c == null) {
              ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
              for (Throwable t : suppressedExceptions) {
                  cnfe.addSuppressed(t);
              }
              throw cnfe;
          }
          return c;
      }

可以看到,查找Class的任務通其內部一個DexPathList類對象實現的,它的findClass方法如下:

   public Class findClass(String name, List suppressed) {
          for (Element element : dexElements) {
              DexFile dex = element.dexFile;

              if (dex != null) {
                  Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                  if (clazz != null) {
                      return clazz;
                  }
              }
          }
          if (dexElementsSuppressedExceptions != null) {
              suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
          }
          return null;
      }

至此,真相大白,原來,APK類加載是通過遍歷dexElements這個數組來查找Class,而dexElements就是APK dexPath裡面的文件。

從上述分析可以得知要實現插件的類加載有兩種方式:

把插件的信息通過反射放進這個數組裡面

替換系統的ClassLoader

考慮到類的隔離性以及框架拓展性,ZeusPlugin目前使用的方案是第二種,根據類加載器的雙親委派模型,我們可以實現一套插件補丁類加載方案,如下圖:

類加載.jpg<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxibG9ja3F1b3RlPg0KCTxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPs7Sw8fNqLn9t7TJ5NDeuMTPtc2ztcRDbGFzc0xvYWRlcs6qWmV1c0NsYXNzTG9hZGVyo6zG5MTasPy6rLbguPZaZXVzUGx1Z2luQ2xhc3NMb2FkZXIgw7/Su7j2suW8/rbU06bSu7j2WmV1c1BsdWdpbkNsYXNzTG9hZGVyo6y1sdLGs/2y5bz+yrHU8sm+s/3Su7j2WmV1c1BsdWdpbkNsYXNzTG9hZGVyo6y809TY0ru49rLlvP7U8sztvNPSu7j2WmV1c1BsdWdpbkNsYXNzTG9hZGVyo6wgWmV1c0NsYXNzTG9hZGVytcRwYXJlbnTOqtStyrxBUEu1xENsYXNzTG9hZGVyKFBhdGhDbGFzc0xvYWRlcimjrLb41K3KvEFQS7XEQ2xhc3NMb2FkZXK1xHBhcmVudChQYXRoQ2xhc3NMb2FkZXIpzqpaZXVzSG90Zml4Q2xhc3NMb2FkZXIsIFpldXNIb3RmaXhDbGFzc0xvYWRlcrXEcGFyZW50zqrPtc2ztcRDbGFzc0xvYWRlcihCb290Q2xhc3NMb2FkZXIpoaMgPC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2Jsb2NrcXVvdGU+DQo8aDQgaWQ9"資源加載">資源加載

關於資源加載,我們回到handleBindApplication方法

  private void handleBindApplication(AppBindData data) {
            ......
            //省略代碼
        .......
            //生成APK信息LoadedApk,即packageInfo
          data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
             //創建上下文環境
          final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
        ......
            //省略代碼
        .......
    }

這裡創建了上下文環境,即ContextImpl,再看看createAppContext方法真正實現:

  private ContextImpl(ContextImpl container, ActivityThread mainThread,
              LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
              Display display, Configuration overrideConfiguration, int createDisplayWithId) {
          ......
            //省略代碼
        .......
        //真正創建Resources的地方
          Resources resources = packageInfo.getResources(mainThread);
          if (resources != null) {
              if (displayId != Display.DEFAULT_DISPLAY
                      || overrideConfiguration != null
                      || (compatInfo != null && compatInfo.applicationScale
                              != resources.getCompatibilityInfo().applicationScale)) {
                  resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                          packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                          packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                          overrideConfiguration, compatInfo);
              }
          }
          mResources = resources;
        ......
            //省略代碼
        .......

      }

Resources resources = packageInfo.getResources(mainThread);這段代碼就是真正創建Resources的地方,我們繼續跟進去會發現它最終調用的是ResourcesManager的getTopLevelResources方法

   Resources getTopLevelResources(String resDir, String[] splitResDirs,
              String[] overlayDirs, String[] libDirs, int displayId,
              Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
          final float scale = compatInfo.applicationScale;
          Configuration overrideConfigCopy = (overrideConfiguration != null)
                  ? new Configuration(overrideConfiguration) : null;
          ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
          Resources r;
          synchronized (this) {
              // Resources is app scale dependent.
              if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
           //判斷是否已經存在Resources
              WeakReference wr = mActiveResources.get(key);
              r = wr != null ? wr.get() : null;
              //if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
              if (r != null && r.getAssets().isUpToDate()) {
                  if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                          + ": appScale=" + r.getCompatibilityInfo().applicationScale
                          + " key=" + key + " overrideConfig=" + overrideConfiguration);
                  return r;
              }
          }

          //if (r != null) {
          //    Log.w(TAG, "Throwing away out-of-date resources!!!! "
          //            + r + " " + resDir);
          //}
        //創建資源管理器
          AssetManager assets = new AssetManager();
          // resDir can be null if the 'android' package is creating a new Resources object.
          // This is fine, since each AssetManager automatically loads the 'android' package
          // already.
          if (resDir != null) {
             //添加APK資源路徑
              if (assets.addAssetPath(resDir) == 0) {
                  return null;
              }
          }
        ......
            //省略代碼
        .......
          //創建Resources
          r = new Resources(assets, dm, config, compatInfo);
          if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                  + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale);

          synchronized (this) {
              WeakReference wr = mActiveResources.get(key);
              Resources existing = wr != null ? wr.get() : null;
              if (existing != null && existing.getAssets().isUpToDate()) {
                  // Someone else already created the resources while we were
                  // unlocked; go ahead and use theirs.
                  r.getAssets().close();
                  return existing;
              }

              // XXX need to remove entries when weak references go away
              mActiveResources.put(key, new WeakReference<>(r));
              if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size());
              return r;
          }
      }

至此,Resources就創建好了,這裡有一個關鍵的類AssetManager,它是應用程序的資源管理器,在它的構造函數裡會把framework/framework-res.apk也會添加到資源路徑中,這是C++調用,有興趣的話,可以參考一下老羅這篇文章。同時這也解釋了為什麼我們開發的應用可以訪問到系統的資源。

通過上述分析,我們可以得知,要實現插件資源加載,只需創建一個AssetManager,然後把把宿主資源路徑和插件apk路徑添加進去,創建我們自己的Resources,然後通過反射把PackageInfo的mResources替換成我們的Resources即可,具體代碼如下:

   AssetManager assetManager = AssetManager.class.newInstance();
              Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
              addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());
              if (mLoadedPluginList != null && mLoadedPluginList.size() != 0) {
                  //每個插件的packageID都不能一樣
                  for (String id : mLoadedPluginList.keySet()) {
                      addAssetPath.invoke(assetManager, PluginUtil.getAPKPath(id));
                  }
              }
              //這裡提前創建一個resource是因為Resources的構造函數會對AssetManager進行一些變量的初始化
              //還不能創建系統的Resources類,否則中興系統會出現崩潰問題
              PluginResources newResources = new PluginResources(assetManager,
                      mBaseContext.getResources().getDisplayMetrics(),
                      mBaseContext.getResources().getConfiguration());


              PluginUtil.setField(mBaseContext, "mResources", newResources);
              //這是最主要的需要替換的,如果不支持插件運行時更新,只留這一個就可以了
              PluginUtil.setField(mPackageInfo, "mResources", newResources);

現在,參考以上思路,我們已經基本可以實現一個插件補丁框架,其實站在巨人的肩膀(Android 系統源碼)上,是不是覺得實現一套插件補丁框架也沒那麼復雜呢?當然,真正項目中,還有很多細節需要處理,譬如說資源分區、代碼混淆等問題。但核心邏輯基本還是以上這些思路。具體實現可以參考 ZeusPlugin源碼

TODO

由於公司業務線、時間精力等原因, ZeusPlugin有一些特性和功能還沒實現,但很多也提上日程了,比如:

demo完善

gradle插件maven遠程依賴

支持補丁更換資源

……..

GitHub

https://github.com/iReaderAndroid/ZeusPlugin

github原文鏈接

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved