Android插件化的探索

簡(jiǎn)介

對(duì)于App而言,所謂的插件化邮旷,個(gè)人的理解就是把一個(gè)完整的App拆分成宿主和插件兩大部分懦尝,我們?cè)谒拗鱝pp運(yùn)行時(shí)可以動(dòng)態(tài)的載入或者替換插件的部分,插件不僅是對(duì)宿主功能的擴(kuò)展而且還能減小宿主的負(fù)擔(dān)浩考,所謂的宿主就是運(yùn)行的app夹孔,插件即宿主運(yùn)行時(shí)加載的apk文件,這樣宿主和插件結(jié)合的方案技術(shù)大概就是插件化了吧析孽。

image

為什么要插件化搭伤?

  • 解耦,獨(dú)立各大模塊的業(yè)務(wù)成為插件袜瞬,互不干擾怜俐,即用即插,方便開(kāi)發(fā)與維護(hù)邓尤。當(dāng)業(yè)務(wù)龐大拍鲤、繁瑣之后贴谎,是否存在牽一發(fā)而動(dòng)全身的感覺(jué),是否存在邏輯過(guò)于復(fù)雜季稳、耦合度較高擅这、難以掌控整個(gè)項(xiàng)目。
  • 加快編譯景鼠。每次修改后無(wú)需重新編輯整個(gè)工程項(xiàng)目仲翎,可以單獨(dú)編譯某個(gè)插件工程,對(duì)于龐大的項(xiàng)目而言莲蜘,速度就是至上的武功谭确。
  • 動(dòng)態(tài)更新。無(wú)需重新下載與安裝app票渠,可以單獨(dú)下載某個(gè)插件apk逐哈,直接加載,從動(dòng)態(tài)更新问顷、包體積和流量上感覺(jué)是個(gè)不錯(cuò)的選擇昂秃。
  • 模塊定制。需要什么模塊下載什么模塊杜窄,無(wú)需讓app變得龐大肠骆,所需所得。
image

插件化原理簡(jiǎn)單描述

關(guān)于插件化主要解決的大概就是類加載塞耕、資源加載蚀腿、組件的加載這些核心問(wèn)題了吧,所謂的原理也就是圍繞這些問(wèn)題進(jìn)行的探討扫外。

image

Android的類加載

android中的類加載系統(tǒng)的ClassLoader可以大致劃分為BaseDexClassLoader莉钙,SecureClassLoader。作為插件化我們只簡(jiǎn)單分析一下PathClassLoader與DexClassLoader筛谚,畢竟類加載的內(nèi)容也很多磁玉,要寫的東西也很多??,先看下android類加載繼承關(guān)系圖:

image
  • PathClassLoader 提供一個(gè)簡(jiǎn)單的類加載器實(shí)現(xiàn)驾讲,該實(shí)現(xiàn)對(duì)本地文件系統(tǒng)中的文件和目錄列表進(jìn)行操作蚊伞,但不嘗試從網(wǎng)絡(luò)加載類。Android將該類用于其系統(tǒng)類加載器和應(yīng)用程序類加載器(簡(jiǎn)單講可加載已安裝的apk)吮铭。下面是官方的描述:

Provides a simple ClassLoader implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).

  • DexClassLoader 它從.jar和.apk文件中加載包含類.dex條目的類时迫。這可以用于執(zhí)行未作為應(yīng)用程序的一部分安裝的代碼(簡(jiǎn)單講可加載未安裝的apk,熱修復(fù)與動(dòng)態(tài)更新可能就是靠它了)在API級(jí)別26之前谓晌,這個(gè)類加載器需要一個(gè)應(yīng)用程序?qū)S玫目蓪懩夸泚?lái)緩存優(yōu)化的類别垮。使用Context.getCodeCacheDir()創(chuàng)建這樣一個(gè)目錄:以下為官方的描述:

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
Prior to API level 26, this class loader requires an application-private, writable directory to cache optimized classes. Use Context.getCodeCacheDir() to create such a directory:

以上關(guān)于android的類加載只是輕描淡寫了一下,說(shuō)了半天扎谎,關(guān)于插件化當(dāng)然用到了DexClassLoader碳想,我們來(lái)看一下DexClassLoader的實(shí)現(xiàn)吧。

public class DexClassLoader extends BaseDexClassLoader {
   public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
  }
}
image

英語(yǔ)不是很好毁靶,下面我簡(jiǎn)單翻譯一下??

dexPath: 字符串變量胧奔,包含類和資源的jar/apk文件列表,用File.pathSeparator分隔预吆,在Android上默認(rèn)為“:”龙填。
optimizedDirectory:不推薦使用此參數(shù),貌似是一個(gè)廢棄的參數(shù)拐叉,據(jù)說(shuō)是.dex文件的解壓路徑岩遗,自API級(jí)別26起不再生效,那么26之前是怎么用的呢凤瘦,查了一下是通過(guò) context.getCodeCacheDir()宿礁。
librarySearchPath: 包含native庫(kù)的目錄列表,C/C++庫(kù)存放的路徑,用File.pathSeparator分隔蔬芥;可能為null梆靖。
parent: 父類加載器ClassLoader.

再看一下調(diào)用的父類BaseDexClassLoader的構(gòu)造方法及核心方法

public class BaseDexClassLoader extends ClassLoader {
  private final DexPathList pathList;
  public BaseDexClassLoader(String dexPath, File optimizedDirectory,
           String libraryPath, ClassLoader parent) {
       super(parent);
       this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
   }
    @Override
   protected Class<?> findClass(String name) throws ClassNotFoundException {
       List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
       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;
   }
    @Override
   protected URL findResource(String name) {
       return pathList.findResource(name);
   }
    @Override
   protected Enumeration<URL> findResources(String name) {
       return pathList.findResources(name);
   }
    @Override
   public String findLibrary(String name) {
       return pathList.findLibrary(name);
   }
}

顯然看出DexPathList這個(gè)成員對(duì)象的重要性,初始化構(gòu)造方法的時(shí)候?qū)嵗疍exPathList對(duì)象笔诵,同時(shí)返吻,BaseDexClassLoader重寫了父類findClass()方法,通過(guò)該方法進(jìn)行類查找的時(shí)候乎婿,會(huì)委托給pathList對(duì)象的findClass()方法進(jìn)行相應(yīng)的類查找测僵,下面繼續(xù)查看DexPathList類的findClass方法:

final class DexPathList {
    private Element[] dexElements;
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        ...
        
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        ...
    }
    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      for (File file : files) {
          if (file.isDirectory()) {
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      return elements;
    }
    public Class findClass(String name, List<Throwable> 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;
    }
}

DexPathList構(gòu)造方法被調(diào)用的時(shí)候其實(shí)就是通過(guò)makeDexElements方法把dexPath進(jìn)行遍歷,依次加載每個(gè)dex文件谢翎,然后通過(guò)數(shù)組Element[]存放捍靠,而在DexPathList類的findClass調(diào)用的時(shí)候,通過(guò)遍歷Element[]的dex文件岳服,在通過(guò)DexFile類的loadClassBinaryName()來(lái)加載類剂公,如果不為空那么代表加載成功,并且返回class吊宋,否則返回null纲辽。
下面再來(lái)看一下基類的ClassLoader是如何實(shí)現(xiàn)的吧

public abstract class ClassLoader {
    private final ClassLoader parent;
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                if (c == null) {
                    c = findClass(name);
                }
            }
            return c;
    }
}

這明顯就是一個(gè)雙親委派模型,在類加載的時(shí)候璃搜,首先去查找這個(gè)類之前是否已經(jīng)被加載過(guò)拖吼,如果加載過(guò)直接返回,否則委托父類加載器去查找这吻,如果父類加載器找不到那么就去系統(tǒng)的BootstrapClass去查找吊档,到最后還是找不到的話,那么就自己親自上陣查找了唾糯。這樣就避免了重復(fù)加載怠硼,實(shí)現(xiàn)了更加安全鬼贱。
好了總結(jié)一下DexClassLoader的加載過(guò)程:loadClass->findClass->BaseDexClassLoader.findClass->DexPathList.findClass->loadDexFile->DexFile.loadClassBinaryName->DexFile.defineClass,大體上就這樣么個(gè)過(guò)程吧香璃。

資源加載

Android系統(tǒng)加載資源都是通過(guò)Resource資源對(duì)象來(lái)進(jìn)行加載的这难,因此只需要添加資源(即apk文件)所在路徑到AssetManager中,即可實(shí)現(xiàn)對(duì)插件資源的訪問(wèn)葡秒。

 /**
     * Create a new AssetManager containing only the basic system assets.
     * Applications will not generally use this method, instead retrieving the
     * appropriate asset manager with {@link Resources#getAssets}.    Not for
     * use by applications.
     * @hide
     */
    public AssetManager() {
        final ApkAssets[] assets;
        synchronized (sSync) {
            createSystemAssetsInZygoteLocked();
            assets = sSystemApkAssets;
        }

        mObject = nativeCreate();
        if (DEBUG_REFS) {
            mNumRefs = 0;
            incRefsLocked(hashCode());
        }

        // Always set the framework resources.
        setApkAssets(assets, false /*invalidateCaches*/);
    }

不難發(fā)現(xiàn)AssetManager的構(gòu)造方法是@hide隱藏的api姻乓,所以不能直接使用,這里肯定是需要通過(guò)反射啦眯牧,不過(guò)有人說(shuō)Android P不是對(duì)系統(tǒng)的隱藏Api做出了限制蹋岩,因此插件化估計(jì)要涼涼,但是我想說(shuō)現(xiàn)在一些主流的插件化技術(shù)基本都已經(jīng)適配了Android9.0了学少,所以無(wú)需擔(dān)心剪个。下面先簡(jiǎn)單貼出Android資源的加載流程。關(guān)于插件化的資源加載可以參考下滴滴VirtualApk資源的加載思想 (傳送門

class ContextImpl extends Context {
//...
    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration) {
    //....
    Resources resources = packageInfo.getResources(mainThread);
    //....
    }
//...
}

這里不去關(guān)注packageInfo是如何生成的旱易,直接跟蹤到下面去.

public final class LoadedApk {
  private final String mResDir;
  public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
            CompatibilityInfo compatInfo, ClassLoader baseLoader,
            boolean securityViolation, boolean includeCode, boolean registerPackage) {
        final int myUid = Process.myUid();
        aInfo = adjustNativeLibraryPaths(aInfo);
        mActivityThread = activityThread;
        mApplicationInfo = aInfo;
        mPackageName = aInfo.packageName;
        mAppDir = aInfo.sourceDir;
        mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
        // 注意一下這個(gè)sourceDir禁偎,這個(gè)是我們宿主的APK包在手機(jī)中的路徑,宿主的資源通過(guò)此地址加載阀坏。
        // 該值的生成涉及到PMS,暫時(shí)不進(jìn)行分析如暖。
        // Full path to the base APK for this application.
       //....
    }
//....
   public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }
//....
}

進(jìn)入到ActivityThread.getTopLevelResources()的邏輯中

public final class ActivityThread {  
  Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {  
  //我們暫時(shí)只關(guān)注下面這一段代碼
 
       AssetManager assets = new AssetManager();  
        if (assets.addAssetPath(resDir) == 0) {  //此處將上面的mResDir,也就是宿主的APK在手機(jī)中的路徑當(dāng)做資源包添加到AssetManager里忌堂,則Resources對(duì)象可以通過(guò)AssetManager查找資源盒至,此處見(jiàn)(老羅博客:Android應(yīng)用程序資源的查找過(guò)程分析)
            return null;  
        }  
        // 創(chuàng)建Resources對(duì)象,此處依賴AssetManager類來(lái)實(shí)現(xiàn)資源查找功能士修。
       r = new Resources(assets, metrics, getConfiguration(), compInfo);  
  
 }  
}

從上面的代碼中我們知道了我們常用的Resources是如何生成的了枷遂,那么理論上插件也就按照如此方式生成一個(gè)Resources對(duì)象給自己用就可以了。

組件的加載

這個(gè)其實(shí)不能一概而論棋嘲,因?yàn)锳ndroid擁有四大組件酒唉,分別為Activity、Service沸移、ContentProvider痪伦、BoradCastRecevier,每個(gè)組件的屬性及生命周期也不一樣雹锣,所以關(guān)于插件中加載的組件就需要分別研究每個(gè)組件是如何加載的了网沾。

簡(jiǎn)單拿Activity組件來(lái)說(shuō),現(xiàn)在一些主流的方式基本上都是通過(guò)“坑位”的思想蕊爵,這個(gè)詞最早據(jù)說(shuō)也是來(lái)源于360辉哥,總的來(lái)說(shuō),先占坑,因?yàn)槲覀兯拗鱝pp的Manifest中是不會(huì)去申請(qǐng)插件中的Activity的醋旦,那我就先占一個(gè)坑恒水,欺騙系統(tǒng),然后替換成插件中的Activity饲齐。這里可能需要多個(gè)坑位寇窑,因?yàn)橐恍┵Y源屬性都是可以動(dòng)態(tài)配置的。比如launchMode箩张、process、configChanges窗市、theme等等先慷。
這里還需要了解一下Activity的啟動(dòng)流程,這里我們可以簡(jiǎn)單看一下咨察。

 @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

可以看出论熙,我們平時(shí)startActivity其實(shí)都是通過(guò)調(diào)用startActivityForResult(),我們接下來(lái)繼續(xù)看

 public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                mStartedActivity = true;
            }
            cancelInputsAndStartExitTransition(options);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

我們可以看到是通過(guò)系統(tǒng)的Instrumentation這個(gè)類execStartActivity()來(lái)執(zhí)行啟動(dòng)Activity的摄狱,我們繼續(xù)可以看到下面的這個(gè)方法:

  public ActivityResult execStartActivity(
  脓诡、、媒役、祝谚、、
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }
    
 /**
     * @hide
     */
    public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }
    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };

ActivityManager.getService()拿到IActivityManager對(duì)象酣衷,然后就去調(diào)用startActivity()了交惯,而IActivityManager只是一個(gè)抽象接口,下面看看它的實(shí)現(xiàn)類

public abstract class ActivityManagerNative extends Binder implements IActivityManager

public final class ActivityManagerService extends ActivityManagerNative
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback
        
class ActivityManagerProxy implements IActivityManager

可以看到它的兩個(gè)實(shí)現(xiàn)類ActivityManagerProxy與ActivityManagerService穿仪,簡(jiǎn)稱AMP與AMS席爽,AMP只是AMS的本地代理對(duì)象,其startActivity方法會(huì)調(diào)用到AMS的startActivity方法啊片。而且要注意只锻,這個(gè)startActivity方法會(huì)把ApplicationThread對(duì)象傳遞到AMS所在進(jìn)程,當(dāng)然AMS拿到的實(shí)際上是ApplicationThread的代理對(duì)象ApplicationThreadProxy紫谷,AMS就要通過(guò)這個(gè)代理對(duì)象與我們的App進(jìn)程進(jìn)行通信齐饮。
既然Activity是否存在的校驗(yàn)是發(fā)生在AMS端,那么我們?cè)谂cAMS交互前碴里,提前將Activity的ComponentName進(jìn)行替換為占坑的名字沈矿,選擇hook Instrumentation或者ActivityManagerProxy應(yīng)該都是可以的,然后Activity經(jīng)過(guò)復(fù)雜的啟動(dòng)流程后最終會(huì)執(zhí)行Instrumentation的newActivity()咬腋,這里我們可以進(jìn)行還原成插件的Activity羹膳。

 public Activity newActivity(Class<?> clazz, Context context, 
            IBinder token, Application application, Intent intent, ActivityInfo info, 
            CharSequence title, Activity parent, String id,
            Object lastNonConfigurationInstance) throws InstantiationException, 
            IllegalAccessException {
        Activity activity = (Activity)clazz.newInstance();
        ActivityThread aThread = null;
        // Activity.attach expects a non-null Application Object.
        if (application == null) {
            application = new Application();
        }
        activity.attach(context, aThread, this, token, 0 /* ident */, application, intent,
                info, title, parent, id,
                (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
                new Configuration(), null /* referrer */, null /* voiceInteractor */,
                null /* window */, null /* activityConfigCallback */);
        return activity;
    }

關(guān)于插件化四大組件的加載原理過(guò)于復(fù)雜,我只簡(jiǎn)單的描述了一下插件化的思想根竿,如果想看具體的思想流程陵像,也可以查看滴滴VirtualApk的組件加載原理就珠,插件化思想都有共通之處(傳送門

image

關(guān)于插件化方案的選取

如果你在做插件化,或者想去研究插件化醒颖,上面看不懂沒(méi)有關(guān)系妻怎,反正市場(chǎng)上已經(jīng)擁有非常多的成熟方案,下面是從萬(wàn)千的方案中挑取較好的幾個(gè)方案泞歉,以免走更多的彎路逼侦,畢竟我也是從茫茫的插件化方案中走了一遭。

  • VirtualApk 滴滴插件化方案腰耙,功能非常強(qiáng)大榛丢,而且兼容性強(qiáng),目前已經(jīng)適配Android 9.0挺庞,如果項(xiàng)目插件和宿主存在依賴的話是個(gè)不錯(cuò)的選擇晰赞。
  • DroidPlugin 360的一款插件化方案,最大的特色就是插件獨(dú)立选侨,不依賴宿主掖鱼,當(dāng)然就無(wú)耦合啦
  • RePlugin 360另外一款插件化方案,它與DroidPlugin代表2個(gè)不同方向援制,各個(gè)功能模塊能獨(dú)立升級(jí)戏挡,又能需要和宿主、插件之間有一定交互和耦合隘谣。
  • Shadow 騰訊最近剛開(kāi)源的插件化方案增拥,最大特色零反射,核心庫(kù)采取Kotlin實(shí)現(xiàn)寻歧,個(gè)人覺(jué)得以后是個(gè)不錯(cuò)的選擇掌栅,但是因?yàn)閯傞_(kāi)源,還未受到大眾的檢測(cè)码泛。
  • VirtualApp 羅盒科技的一款運(yùn)行于Android系統(tǒng)的沙盒產(chǎn)品猾封,可以理解為輕量級(jí)的“Android虛擬機(jī)”,非常的牛噪珊,廣泛應(yīng)用于插件化開(kāi)發(fā)晌缘、無(wú)感知熱更新、云控自動(dòng)化痢站、多開(kāi)磷箕、手游租號(hào)、手游手柄免激活阵难、區(qū)塊鏈岳枷、移動(dòng)辦公安全、軍隊(duì)政府保密、手機(jī)模擬信息空繁、腳本自動(dòng)化殿衰、自動(dòng)化測(cè)試等技術(shù)領(lǐng)域,最大的特色app雙開(kāi)及多開(kāi)盛泡,沙盒能力闷祥,內(nèi)外隔離。不過(guò)2017已經(jīng)商業(yè)化了傲诵。
image

滴滴插件化嘗鮮

VirtualAPK的工作過(guò)程

VirtualAPK對(duì)插件沒(méi)有額外的約束凯砍,原生的apk即可作為插件。插件工程編譯生成apk后拴竹,即可通過(guò)宿主App加載果覆,每個(gè)插件apk被加載后,都會(huì)在宿主中創(chuàng)建一個(gè)單獨(dú)的LoadedPlugin對(duì)象殖熟。如下圖所示,通過(guò)這些LoadedPlugin對(duì)象斑响,VirtualAPK就可以管理插件并賦予插件新的意義菱属,使其可以像手機(jī)中安裝過(guò)的App一樣運(yùn)行。


image

如何使用

第一步: 宿主Project的build.gradle添加

dependencies {
    classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}

第二步:宿主的Moudle的build.gradle添加

apply plugin: 'com.didi.virtualapk.host'
implementation 'com.didi.virtualapk:core:0.9.8'

第三步:宿主app的Applicaiton中添加初始化:

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    PluginManager.getInstance(base).init();
}

第四步:增加混淆:

-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }

-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }

第五步:宿主的使用:

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

// Given "com.didi.virtualapk.demo" is the package name of plugin APK, 
// and there is an activity called `MainActivity`.
Intent intent = new Intent();
intent.setClassName("com.didi.virtualapk.demo", "com.didi.virtualapk.demo.MainActivity");
startActivity(intent);

第六步:插件的Project的build.gradle配置:

dependencies {
    classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}

第七步: 插件app的build.gradle配置:

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. 
}

第八步:關(guān)于編譯運(yùn)行命令

宿主:gradlew clean assembleRelease
插件:gradlew clean assemblePlugin

原理

  • 合并宿主和插件的ClassLoader 需要注意的是舰罚,插件中的類不可以和宿主重復(fù)
  • 合并插件和宿主的資源 重設(shè)插件資源的packageId纽门,將插件資源和宿主資源合并
  • 去除插件包對(duì)宿主的引用 構(gòu)建時(shí)通過(guò)Gradle插件去除插件對(duì)宿主的代碼以及資源的引用

四大組件的實(shí)現(xiàn)原理

  • Activity 采用宿主manifest中占坑的方式來(lái)繞過(guò)系統(tǒng)校驗(yàn),然后再加載真正的activity营罢;
  • Service 動(dòng)態(tài)代理AMS赏陵,攔截service相關(guān)的請(qǐng)求,將其中轉(zhuǎn)給Service Runtime去處理饲漾,Service Runtime會(huì)接管系統(tǒng)的所有操作蝙搔;
  • Receiver 將插件中靜態(tài)注冊(cè)的receiver重新注冊(cè)一遍;
  • ContentProvider 動(dòng)態(tài)代理IContentProvider考传,攔截provider相關(guān)的請(qǐng)求吃型,將其中轉(zhuǎn)給Provider Runtime去處理,Provider Runtime會(huì)接管系統(tǒng)的所有操作僚楞。
    如下是VirtualAPK的整體架構(gòu)圖勤晚,更詳細(xì)的內(nèi)容請(qǐng)大家閱讀源碼。
    image

偶編譯運(yùn)行遇見(jiàn)的問(wèn)題

  • 插件和宿主既可在同工程也可不在同工程泉褐,他們是通過(guò)targetHost來(lái)關(guān)聯(lián)的赐写,所以非常靈活,無(wú)需擔(dān)心結(jié)構(gòu)(正常來(lái)說(shuō)插件和宿主都是不同的工程)
  • 與阿里的熱修復(fù)框架產(chǎn)生了不兼容膜赃,大概跟初始化有關(guān)系(暫時(shí)剔除)
  • 與JobIntentService產(chǎn)生了不兼容挺邀,會(huì)報(bào)No such service componentInfo(用IntentService替代)
  • 宿主跳轉(zhuǎn)插件,發(fā)現(xiàn)資源界面還是宿主的(資源id不能和宿主的資源重名)
  • 報(bào)宿主需要依賴所有com.android.support包(插件和宿主都要同時(shí)依賴com.android.support包且版本都要一樣)
  • 報(bào)IllegalStateException:You need to use a Theme.AppCompat theme,構(gòu)建插件時(shí)悠夯,請(qǐng)使用(gradlew clean assemblePlugin)
  • 發(fā)現(xiàn)插件的主題未起作用(請(qǐng)確保宿主和插件使用同一主題)
  • 報(bào)The directory of host application doesn't exist!(targetHost路勁配置錯(cuò)誤)
  • 發(fā)現(xiàn)騰訊X5瀏覽器加載失敗默認(rèn)采取的是系統(tǒng)的WebView(檢查so文件癌淮,確保宿主和插件的cpu核心保持一致)

最后

關(guān)于android的插件化簡(jiǎn)單研究大概就是醬樣子了,初次嘗鮮感覺(jué)還是蠻不錯(cuò)的沦补,但是最大的苦惱應(yīng)該是業(yè)務(wù)插件該如何拆分了乳蓄,基礎(chǔ)組件如何拆分,如何從復(fù)雜的荊棘業(yè)務(wù)中殺出一條血路夕膀,想要“萬(wàn)花叢中過(guò)虚倒,片葉不沾身”,騷年产舞,我相信你可以的魂奥。

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市易猫,隨后出現(xiàn)的幾起案子耻煤,更是在濱河造成了極大的恐慌,老刑警劉巖准颓,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哈蝇,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡攘已,警方通過(guò)查閱死者的電腦和手機(jī)炮赦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)样勃,“玉大人吠勘,你說(shuō)我怎么就攤上這事∠靠簦” “怎么了剧防?”我有些...
    開(kāi)封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辫樱。 經(jīng)常有香客問(wèn)我诵姜,道長(zhǎng),這世上最難降的妖魔是什么搏熄? 我笑而不...
    開(kāi)封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任棚唆,我火速辦了婚禮,結(jié)果婚禮上心例,老公的妹妹穿的比我還像新娘宵凌。我一直安慰自己,他們只是感情好止后,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布瞎惫。 她就那樣靜靜地躺著溜腐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瓜喇。 梳的紋絲不亂的頭發(fā)上挺益,一...
    開(kāi)封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音乘寒,去河邊找鬼望众。 笑死,一個(gè)胖子當(dāng)著我的面吹牛伞辛,可吹牛的內(nèi)容都是我干的烂翰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蚤氏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼甘耿!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起竿滨,我...
    開(kāi)封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤佳恬,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后于游,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體殿怜,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年曙砂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骏掀。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鸠澈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出截驮,到底是詐尸還是另有隱情笑陈,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布葵袭,位于F島的核電站涵妥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏坡锡。R本人自食惡果不足惜蓬网,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鹉勒。 院中可真熱鬧帆锋,春花似錦、人聲如沸禽额。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至实辑,卻和暖如春捺氢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剪撬。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工摄乒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人婿奔。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓缺狠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親萍摊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挤茄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容