插件化開發—動態載入技術載入已安裝和未安裝的apk

首先引入一個概念,動態載入技術是什麼?爲何要引入動態載入?它有什麼優勢呢?首先要明確這幾個問題。咱們先從java

應用程序入手,你們都知道在Android App中。一個應用程序dex文件的方法數最大不能超過65536個。不然,你的applinux

將出異常了,那麼假設越大的項目那確定超過了,像美團、支付寶等都是使用動態載入技術。支付寶在去年的一個技微信

術分享類會議上就推崇讓應用程序插件化,而美團也發佈了他們的解決方式:Dex本身主動拆包和動態載入技術。app

因此使ide

用動態載入技術解決此類問題。學習

而它的長處可以讓應用程序實現插件化、插拔式結構,對後期維護做用那不用說了。ui

一、什麼是動態載入技術?

動態載入技術就是使用類載入器載入對應的apk、dex、jar(必須含有dex文件)。再經過反射得到該apk、dex、jar內部的資源(class、圖片、color等等)進而供宿主app使用。this

二、關於動態載入使用的類載入器

使用動態載入技術時,通常需要用到這兩個類載入器:
  • PathClassLoader - 僅僅能載入已經安裝的apk,即/data/app文件夾下的apk。
  • DexClassLoader  - 能載入手機中未安裝的apk、jar、dex。僅僅要能在找到相應的路徑。
這兩個載入器分別相應使用的場景各不一樣。因此接下來。分別解說它們各自載入一樣的插件apk的使用。

三、使用PathClassLoader載入已安裝的apk插件,獲取對應的資源供宿主app使用

如下經過一個demo來介紹PathClassLoader的使用:
一、首先咱們需要知道一個manifest中的屬性:SharedUserId。

該屬性是用來幹嗎的呢?簡單的說,應用從一開始安裝在Android系統上時。系統都會給它分配一個linux user id,之
後該應用在從此都將執行在獨立的一個進程中。其餘應用程序不能訪問它的資源,那麼假設兩個應用的sharedUserId一樣,那麼它們將共同執行在一樣的 linux進程中 ,從而便可以數據共享、資源訪問了。

因此咱們在宿主app和插件app的manifest上都定義一個一樣的sharedUserId。spa


二、那麼咱們將插件apk安裝在手機上後。宿主app怎麼知道手機內該插件是不是咱們應用程序的插件呢?
咱們以前是否是定義過插件apk也是使用一樣的sharedUserId,那麼,我就可以這樣思考了,是否是可以獲得手機內所有已安裝apk的sharedUserId呢,而後經過推斷sharedUserId是否和宿主app的一樣。假設是。那麼該app就是咱們的插件app了。確實是這種思路的,那麼有了思路最大的問題就是怎麼獲取一個應用程序內的sharedUserId了。咱們可以經過PackageInfo.sharedUserId來獲取。請看代碼:
/**
     * 查找手機內所有的插件
     * @return 返回一個插件List
     */
    private List<PluginBean> findAllPlugin() {
        List<PluginBean> plugins = new ArrayList<>();
        PackageManager pm = getPackageManager();
        //經過包管理器查找所有已安裝的apk文件
        List<PackageInfo> packageInfos = pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
        for (PackageInfo info : packageInfos) {
            //獲得當前apk的包名
            String pkgName = info.packageName;
            //獲得當前apk的sharedUserId
            String shareUesrId = info.sharedUserId;
            //推斷這個apk是不是咱們應用程序的插件
            if (shareUesrId != null && shareUesrId.equals("com.sunzxyong.myapp") && !pkgName.equals(this.getPackageName())) {
                String label = pm.getApplicationLabel(info.applicationInfo).toString();//獲得插件apk的名稱
                PluginBean bean = new PluginBean(label,pkgName);
                plugins.add(bean);
            }
        }
        return plugins;
    }
經過這段代碼,咱們就可以輕鬆的獲取手機內存在的所有插件。當中PluginBean是定義的一個實體類而已,就不貼它的代碼了。

三、假設找到了插件。就把可用的插件顯示出來了。假設沒有找到,那麼就可提示用戶先去下載插件什麼的。

                List<HashMap<String, String>> datas = new ArrayList<>();
                List<PluginBean> plugins = findAllPlugin();
                if (plugins != null && !plugins.isEmpty()) {
                    for (PluginBean bean : plugins) {
                        HashMap<String, String> map = new HashMap<>();
                        map.put("label", bean.getLabel());
                        datas.add(map);
                    }
                } else {
                    Toast.makeText(this, "沒有找到插件,請先下載。", Toast.LENGTH_SHORT).show();
                }
                showEnableAllPluginPopup(datas);
四、假設找到後,那麼咱們選擇相應的插件時,在宿主app中就載入插件內相應的資源,這個纔是PathClassLoader的重點。咱們首先看看怎麼實現的吧:
/**
     * 載入已安裝的apk
     * @param packageName 應用的包名
     * @param pluginContext 插件app的上下文
     * @return 相應資源的id
     */
    private int dynamicLoadApk(String packageName, Context pluginContext) throws Exception {
        //第一個參數爲包括dex的apk或者jar的路徑,第二個參數爲父載入器
        PathClassLoader pathClassLoader = new PathClassLoader(pluginContext.getPackageResourcePath(),ClassLoader.getSystemClassLoader());
//        Class<?> clazz = pathClassLoader.loadClass(packageName + ".R$mipmap");//經過使用自身的載入器反射出mipmap類進而使用該類的功能
        //參數:一、類的全名,二、是否初始化類,三、載入時使用的類載入器
        Class<?> clazz = Class.forName(packageName + ".R$mipmap", true, pathClassLoader);
        //使用上述兩種方式都可以,這裏咱們獲得R類中的內部類mipmap,經過它獲得相應的圖片id,進而給咱們使用
        Field field = clazz.getDeclaredField("one");
        int resourceId = field.getInt(R.mipmap.class);
        return resourceId;
    }


這種方法就是載入包名爲packageName的插件。而後得到插件內名爲one.png的圖片的資源id,進而供宿主app使用該圖片。現在咱們一步一步來解說一下:
  • 首先就是new出一個PathClassLoader對象。它的構造方法爲:
    public PathClassLoader(String dexPath, ClassLoader parent)
    中當中第一個參數是經過插件的上下文來獲取插件apk的路徑,事實上獲取到的就是/data/app/apkthemeplugin.apk。那麼插件的上下文怎麼獲取呢?在宿主app中咱們僅僅有本app的上下文啊,答案就是爲插件app建立一個上下文:
     //獲取相應插件中的上下文,經過它可獲得插件的Resource
                Context plugnContext = this.createPackageContext(packageName, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);
    經過插件的包名來建立上下文,只是這樣的方法僅僅適合獲取已安裝的app上下文。或者不需要經過反射直接經過插件上下文getResource().getxxx(R.*.*);也行,而這裏用的是反射方法。
    第二個參數是父載入器,都是ClassLoader.getSystemClassLoader()。

  • 好了,插件app的類載入器咱們建立出來了,接下來就是經過反射獲取相應類的資源了,這裏我是獲取R類中的內部類mipmap類,而後經過反射獲得mipmap類中名爲one的字段的值。,而後經過
    plugnContext.getResources().getDrawable(resouceId)
    就可以獲取相應id的Drawable獲得該圖片資源進而宿主app的可用它設置背景等。
    固然也可以獲取到其餘的資源或者獲取Acitivity類等,這裏僅僅是作一個演示樣例。
  • 備:關於R類。在AS中的文件夾爲:/build/generated/source/r/debug/<- packageName ->。它的內部類有:腦洞大的可以儘量的利用這些資源吧!!

    .net

如下演示下該demo效果,在沒有插件狀況下會提示請先下載插件,有插件時候就選擇相應的插件而供宿主app使用,本demo是換背景的功能演示。我來看宿主app中mipmap目錄下並無one.png這張圖片,截圖爲證:

在沒有安裝插件狀況下:

安裝插件後:

可以看到。宿主app使用了插件中的圖片資源。


這時,有的人就會想,這個插件需要下載下來還需要安裝到手機中去。這不就是又安裝了一個apk啊。僅僅是沒顯示出來而已,這種方式不太友好,那麼,可不可以僅僅下載下來,不用安裝,也能供宿主app使用呢?像微信上可以執行沒有安裝的飛機大戰這種,這固然可以的。這就需要用到另一個載入器DexClassLoader。

四、DexClassLoader載入未安裝的apk,提供資源供宿主app使用

關於動態載入未安裝的apk,我先描寫敘述下思路:首先咱們獲得事先知道咱們的插件apk存放在哪一個文件夾下。而後分別獲得插件apk的信息(名稱、包名等)。而後顯示可用的插件,最後動態載入apk得到資源。

依照上面這個思路。咱們需要解決幾個問題:
一、怎麼獲得未安裝的apk的信息
二、怎麼獲得插件的context或者Resource。因爲它是未安裝的不可能經過createPackageContext(...);方法來構建出一個context,因此這時僅僅有在Resource上下功夫。

現在咱們就一一來解答這些問題吧:
一、獲得未安裝的apk信息可以經過mPackageManager .getPackageArchiveInfo()方法得到,
public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags)
它的參數恰好是傳入一個FilePath。而後返回apk文件的PackageInfo信息:
/**
     * 獲取未安裝apk的信息
     * @param context
     * @param archiveFilePath apk文件的path
     * @return
     */
    private String[] getUninstallApkInfo(Context context, String archiveFilePath) {
        String[] info = new String[2];
        PackageManager pm = context.getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(archiveFilePath, PackageManager.GET_ACTIVITIES);
        if (pkgInfo != null) {
            ApplicationInfo appInfo = pkgInfo.applicationInfo;
            String versionName = pkgInfo.versionName;//版本
            Drawable icon = pm.getApplicationIcon(appInfo);//圖標
            String appName = pm.getApplicationLabel(appInfo).toString();//app名稱
            String pkgName = appInfo.packageName;//包名
            info[0] = appName;
            info[1] = pkgName;
        }
        return info;
    }

二、獲得相應未安裝apk的Resource對象。咱們需要經過反射來得到:
/**
     * @param apkName 
     * @return 獲得相應插件的Resource對象
     */
    private Resources getPluginResources(String apkName) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//反射調用方法addAssetPath(String path)
            //第二個參數是apk的路徑:Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin"+File.separator+"apkplugin.apk"
            addAssetPath.invoke(assetManager, apkDir+File.separator+apkName);//將未安裝的Apk文件的加入進AssetManager中,第二個參數爲apk文件的路徑帶apk名
            Resources superRes = this.getResources();
            Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                    superRes.getConfiguration());
            return mResources;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

經過獲得AssetManager中的內部的方法addAssetPath,將未安裝的apk路徑傳入從而加入進assetManager中。而後經過new Resource把assetManager傳入構造方法中。進而獲得未安裝apk相應的Resource對象。


好了!上面兩個問題攻克了。那麼接下來就是載入未安裝的apk得到它的內部資源。
/**
     * 載入apk得到內部資源
     * @param apkDir apk文件夾
     * @param apkName apk名字,帶.apk
     * @throws Exception
     */
    private void dynamicLoadApk(String apkDir, String apkName, String apkPackageName) throws Exception {
        File optimizedDirectoryFile = getDir("dex", Context.MODE_PRIVATE);//在應用安裝文件夾下建立一個名爲app_dex文件夾文件夾,假設已經存在則不建立
        Log.v("zxy", optimizedDirectoryFile.getPath().toString());// /data/data/com.example.dynamicloadapk/app_dex
        //參數:一、包括dex的apk文件或jar文件的路徑,二、apk、jar解壓縮生成dex存儲的文件夾。三、本地library庫文件夾,通常爲null,四、父ClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
        Class<?

> clazz = dexClassLoader.loadClass(apkPackageName + ".R$mipmap");//經過使用apk本身的類載入器,反射出R類中相應的內部類進而獲取咱們需要的資源id Field field = clazz.getDeclaredField("one");//獲得名爲one的這張圖片字段 int resId = field.getInt(R.id.class);//獲得圖片id Resources mResources = getPluginResources(apkName);//獲得插件apk中的Resource if (mResources != null) { //經過插件apk中的Resource獲得resId相應的資源 findViewById(R.id.background).setBackgroundDrawable(mResources.getDrawable(resId)); } }

當中經過new DexClassLoader()來建立未安裝apk的類載入器,咱們來看看它的參數:
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
  • dexPath - 就是apk文件的路徑
  • optimizedDirectory - apk解壓縮後的存放dex的文件夾,值得注意的是,在4.1之後該文件夾不一樣意在sd卡上,看官方文檔:
    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.
    
    This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getDir(String, int) to create such a directory:
    
       File dexOutputDir = context.getDir("dex", 0);
    
    Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection atta
    ,因此咱們用getDir()方法在應用內部建立一個dexOutputDir。
  • libraryPath - 本地的library,通常爲null
  • parent - 父載入器
接下來,就是經過反射的方法,獲取出需要的資源。

如下咱們來看看demo演示的效果。我是把三個apk插件先放在assets文件夾下。而後copy到sd上來模仿下載過程。而後載入出對應插件的資源:
先僅僅拷貝一個插件:
copyApkFile("apkthemeplugin-1.apk");
可以看到正常的獲取到了未安裝apk的資源。
再看看拷貝了三個插件:
        copyApkFile("apkthemeplugin-1.apk");
        copyApkFile("apkthemeplugin-2.apk");
        copyApkFile("apkthemeplugin-3.apk");
可以看到僅僅要一有插件下載,就能顯示出來並使用它。



固然插件化開發並不只僅是像僅僅有這樣的換膚那麼簡單的用途,這僅僅是個demo,學習這樣的插件化開發思想的。由此可以聯想,這樣的插件化的開發。是否是像QQ裏的表情包啊、背景皮膚啊,經過線上下載線下維護的方式。可以在線下載使用對應的皮膚,不使用時候就可以刪了。因此插件化開發是插件與宿主app進行解耦了。即便在沒有插件狀況下。也不會對宿主app有不論什麼影響。而有的話就供用戶選擇性使用了。


相關文章
相關標籤/搜索