Android 插件化框架 Replugin 源碼解讀(四)插件安裝與更新、預加載

       插件安裝的過程只將APK移動(或複製)到“插件路徑”下,不釋放優化後的Dex和Native庫,不會加載插件。可以在 插件路徑data/data/包名/app_p_a (這裏是apk插件,不同的插件會放到不同的文件夾下,app_p_n "p-n"插件路徑) 下找到這個jar 文件

一 . 插件的安裝和更新

     

 //com.qihoo360.replugin.RePlugin

 /**
     * 安裝或升級此插件 <p>
     * 注意: <p>
     * 1、這裏只將APK移動(或複製)到“插件路徑”下,不釋放優化後的Dex和Native庫,不會加載插件 <p>
     * 2、支持“純APK”和“p-n”(舊版,即將廢棄)插件 <p>
     * 3、此方法是【同步】的,耗時較少 <p>
     * 4、不會觸發插件“啓動”邏輯,因此只要插件“當前沒有被使用”,再次調用此方法則新插件立即生效
     *
     * @param path 插件安裝的地址。必須是“絕對路徑”。通常可以用context.getFilesDir()來做
     * @return 安裝成功的插件信息,外界可直接讀取
     * @since 2.0.0 (1.x版本爲installDelayed)
     */
    public static PluginInfo install(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new IllegalArgumentException();
        }

        // 判斷文件合法性
        File file = new File(path);
        if (!file.exists()) {
            return null;
        } else if (!file.isFile()) {
            return null;
        }

        // 省略了p-n插件的處理邏輯,直接看純apk 的情況
        ......
        return MP.pluginDownloaded(path);
    }

1. 外部調用 install 方法 參數是apk的下載路徑,先檢驗這個apk 的路徑是否正確。然後調用 MP.pluginDownloaded 執行接下去的操作

 //com.qihoo360.loader2.MP

 public static final PluginInfo pluginDownloaded(String path) {
        

        try {
            if (path != null) {
                File f = new File(path);
                String fileName = f.getName();
                String fileDir = f.getParent();

                。。。。。。
            }

            //獲取常駐進程服務去完成安裝
            PluginInfo info = PluginProcessMain.getPluginHost().pluginDownloaded(path);
            
            if (info != null) {
                RePlugin.getConfig().getEventCallbacks().onInstallPluginSucceed(info);
            }
            return info;
        } catch (Throwable e) {
           
        } finally {
            // 去鎖
            if (lock != null) {
                lock.unlock();
            }
        }
        return null;
    }

2 . 這裏PluginProcessMain.getPluginHost() 返回的是IPluginHost ,這個在服務端初始化的時候介紹過是一個Binder 對象,實際的處理是在PmHostSvc中完成的,因此PluginProcessMain.getPluginHost().pluginDownloaded(path);調用的是PmHostSvc中pluginDownloaded方法。


//com.qihoo360.loader2.PmHostSvc

 @Override
    public PluginInfo pluginDownloaded(String path) throws RemoteException {
        

        // 通過路徑來判斷是採用新方案,還是舊的P-N(即將廢棄,有多種)方案
        PluginInfo pi;
        String fn = new File(path).getName();
        if (fn.startsWith("p-n-") || fn.startsWith("v-plugin-") || fn.startsWith("plugin-s-") || fn.startsWith("p-m-")) {
            pi = pluginDownloadedForPn(path);
        } else {
            //apk方案直接走這裏
            pi = mManager.getService().install(path);
        }

        if (pi != null) {
            // 通常到這裏,表示“安裝已成功”,這時不管處於什麼狀態,都應該通知外界更新插件內存表
            syncInstalledPluginInfo2All(pi);

        }
        return pi;
    }

3.純apk方案會調用 mManager.getService().install(path) ,這個mManager是PluginManagerServer類型,它是在PmHostSvc的構造方法中被創建,是用來管理插件的安裝、卸載、更新、獲取等功能 

public IPluginManagerServer getService() {

   return mStub;
}

mManager.getService 返回的是IPluginManagerServer 這也是一個Binder對象,他的install 方法 直接返回外部PluginManagerServer的installLocked方法。

public PluginInfo install(String path) throws RemoteException {
    synchronized (LOCKER) {
        return PluginManagerServer.this.installLocked(path);
    }
}

//com.qihoo360.replugin.packages.PluginManagerServer


 private PluginInfo installLocked(String path) {
        final boolean verifySignEnable = RePlugin.getConfig().getVerifySign();
        final int flags = verifySignEnable ? PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES : PackageManager.GET_META_DATA;

        // 1. 讀取APK內容
        PackageInfo pi = mContext.getPackageManager().getPackageArchiveInfo(path, flags);
        if (pi == null) {
           

            RePlugin.getConfig().getEventCallbacks().onInstallPluginFailed(path, RePluginEventCallbacks.InstallResult.READ_PKG_INFO_FAIL);
            return null;
        }

        // 2. 校驗插件簽名
        if (verifySignEnable) {
            if (!verifySignature(pi, path)) {
                return null;
            }
        }

        // 3. 解析出名字和三元組
        PluginInfo instPli = PluginInfo.parseFromPackageInfo(pi, path);
        
        instPli.setType(PluginInfo.TYPE_NOT_INSTALL);

        // 若要安裝的插件版本小於或等於當前版本,則安裝失敗
        // NOTE 絕大多數情況下,應該在調用RePlugin.install方法前,根據雲端回傳的信息來判斷,以防止下載舊插件,浪費流量
        // NOTE 這裏僅做雙保險,或通過特殊渠道安裝時會有用

        // 注意:這裏必須用“非Clone過的”PluginInfo,因爲要修改裏面的內容
        PluginInfo curPli = MP.getPlugin(instPli.getName(), false);
        if (curPli != null) {
            

            // 版本較老?直接返回
            final int checkResult = checkVersion(instPli, curPli);
            if (checkResult < 0) {
                RePlugin.getConfig().getEventCallbacks().onInstallPluginFailed(path, RePluginEventCallbacks.InstallResult.VERIFY_VER_FAIL);
                return null;
            } else if (checkResult == 0){
                instPli.setIsPendingCover(true);
            }
        }

        // 4. 將合法的APK改名後,移動(或複製,見RePluginConfig.isMoveFileWhenInstalling)到新位置
        // 注意:不能和p-n的最終釋放位置相同,因爲管理方式不一樣
        if (!copyOrMoveApk(path, instPli)) {
            RePlugin.getConfig().getEventCallbacks().onInstallPluginFailed(path, RePluginEventCallbacks.InstallResult.COPY_APK_FAIL);
            return null;
        }

        // 5. 從插件中釋放 So 文件
        PluginNativeLibsHelper.install(instPli.getPath(), instPli.getNativeLibsDir());

        // 6. 若已經安裝舊版本插件,則嘗試更新插件信息,否則直接加入到列表中
        if (curPli != null) {
            updateOrLater(curPli, instPli);
        } else {
            mList.add(instPli);
        }

        // 7. 保存插件信息到文件中,下次可直接使用
        mList.save(mContext);

        return instPli;
    }

4. 先根據apk路徑獲取PackageInfo,如果開啓了簽名校驗 則校驗簽名,然後通過PluginInfo.parseFromPackageInfo 解析插件包。

//com.qihoo360.replugin.model.PluginInfo

  /**
     * 通過插件APK的MetaData來初始化PluginInfo <p>
     * 注意:框架內部接口,外界請不要直接使用
     */
    public static PluginInfo parseFromPackageInfo(PackageInfo pi, String path) {
        ApplicationInfo ai = pi.applicationInfo;
        String pn = pi.packageName;
        String alias = null;
        int low = 0;
        int high = 0;
        int ver = 0;

        Bundle metaData = ai.metaData;

        // 優先讀取MetaData中的內容(如有),並覆蓋上面的默認值
        if (metaData != null) {
            // 獲取插件別名(如有),如無則將"包名"當做插件名
            alias = metaData.getString("com.qihoo360.plugin.name");

            // 獲取最低/最高協議版本(默認爲應用的最小支持版本,以保證一定能在宿主中運行)
            low = metaData.getInt("com.qihoo360.plugin.version.low");
            high = metaData.getInt("com.qihoo360.plugin.version.high");

            // 獲取插件的版本號。優先從metaData中讀取,如無則使用插件的VersionCode
            ver = metaData.getInt("com.qihoo360.plugin.version.ver");
        }

        // 針對有問題的字段做除錯處理
        if (low <= 0) {
            low = Constant.ADAPTER_COMPATIBLE_VERSION;
        }
        if (high <= 0) {
            high = Constant.ADAPTER_COMPATIBLE_VERSION;
        }
        if (ver <= 0) {
            ver = pi.versionCode;
        }

        PluginInfo pli = new PluginInfo(pn, alias, low, high, ver, path, PluginInfo.TYPE_NOT_INSTALL);

        // 獲取插件的框架版本號
        pli.setFrameworkVersionByMeta(metaData);

        return pli;
    }

解析方法在 PluginInfo 類中完成。這個類用來描述插件的描述信息,以Json來封裝。我們可以看到解析的內容有插件的別名,最低最高版本,插件的版本號,這些內容我們都是在插件打包apk 的時候 定義在 <MetaData> 標籤中的內容,這個類中還有type字段描述插件類型。

然後從插件信息表中通過名字獲取當前的插件,如果已經有插件在。進行版本比對 checkVersion < 0 版本比老版本低,  = 0 同版本覆蓋安裝,然後將apk 改名字後移動到 插件路徑下。成功後把apk中的so 文件也移動到插件路徑下。如果已經安裝了就版本插件則嘗試更新插件,所以插件的安裝與更新思路是一樣的,只是判斷一下本地有沒有安裝了老的插件。最後把插件的信息保存在文件中,也就是插件路徑下 的那個 p.l 文件 裏面都是插件的json 信息。

//com.qihoo360.loader2.PmHostSvc
 

private void syncInstalledPluginInfo2All(PluginInfo pi) {
        // PS:若更新了“正在運行”的插件(屬於“下次重啓進程後更新”),則由於install返回的是“新的PluginInfo”,爲防止出現“錯誤更新”,需要使用原來的
        //
        // 舉例,有一個正在運行的插件A(其Info爲PluginInfoOld)升級到新版(其Info爲PluginInfoNew),則:
        // 1. mManager.getService().install(path) 的返回值爲:PluginInfoNew
        // 2. PluginInfoOld在常駐進程中的內容修改爲:PluginInfoOld.mPendingUpdate = PendingInfoNew
        // 3. 同步到各進程,這裏存在兩種可能:
        //    a) (有問題)同步的是PluginInfoNew,則所有進程的內存表都強制更新到新的Info上,因此【正在運行的】插件信息將丟失,會出現嚴重問題
        //    b) (沒問題)同步的是PluginInfoOld,只不過這個Old裏面有個mPendingUpdate指向PendingInfoNew,則不會有問題,舊的仍被使用,符合預期
        // 4. 最終install方法的返回值是PluginInfoNew,這樣最外面拿到的就是安裝成功的新插件信息,符合開發者的預期
        PluginInfo needToSyncPi;
        PluginInfo parent = pi.getParentInfo();
        if (parent != null) {
            needToSyncPi = parent;
        } else {
            needToSyncPi = pi;
        }

        // 在常駐進程內更新插件內存表
        mPluginMgr.newPluginFound(needToSyncPi, false);

        // 通知其它進程去更新
        Intent intent = new Intent(PmBase.ACTION_NEW_PLUGIN);
        intent.putExtra(RePluginConstants.KEY_PERSIST_NEED_RESTART, mNeedRestart);
        intent.putExtra("obj", (Parcelable) needToSyncPi);
        IPC.sendLocalBroadcast2AllSync(mContext, intent);

       
    }

5 . 在 2  中還有一個 syncInstalledPluginInfo2All 方法 這個方法主要是通過廣播去通知各個進程更新插件表的信息。每個進程都會註冊一個廣播,這個是在初始化的時候 onCreate  方法中執行的。

 

二. 插件的預加載

插件的安裝只是將apk文件 改名字後移動到插件路徑下(data/data/包名/app_p_a)這個文件夾下有jar 和 so 還有插件json文件。但是加載插件 ClassLoader 加載的是dex 文件,爲了提高打開插件的速度。可以提前先釋放dex 文件,人爲的去做這個操作。可以通過 preload 這個方法實現

//com.qihoo360.replugin.RePlugin

/**
     * 預加載此插件。此方法會立即釋放優化後的Dex和Native庫,但不會運行插件代碼。 <p>
     * 使用場景:在“安裝”完成後“提前釋放Dex”(時間算在“安裝過程”中)。這樣下次啓動插件時則速度飛快 <p>
     * 注意: <p>
     * 1、該方法非必須調用(見“使用場景”)。換言之,只要涉及到插件加載,就會自動完成preload操作,無需開發者關心 <p>
     * 2、Dex和Native庫會佔用大量的“內部存儲空間”。故除非插件是“確定要用的”,否則不必在安裝完成後立即調用此方法 <p>
     * 3、該方法爲【同步】調用,且耗時較久(尤其是dex2oat的過程),建議在線程中使用 <p>
     * 4、調用後將“啓動”此插件,若再次升級,則必須重啓進程後才生效
     *
     * @param pi 要加載的插件信息
     * @return 預加載是否成功
     * @see #install(String)
     * @since 2.0.0
     */
    public static boolean preload(PluginInfo pi) {
        if (pi == null) {
            return false;
        }

        // 藉助“UI進程”來快速釋放Dex(見PluginFastInstallProviderProxy的說明)
        return PluginFastInstallProviderProxy.install(RePluginInternal.getAppContext(), pi);
    }


  
//com.qihoo360.replugin.packages.PluginFastInstallProviderProxy



/**
     * 根據PluginInfo的信息來通知UI進程去“安裝”插件,包括釋放Dex等。
     *
     * @param context Context對象
     * @param pi PluginInfo對象
     * @return 安裝是否成功
     */
    public static boolean install(Context context, PluginInfo pi) {
        // 若Dex已經釋放,則無需處理,直接返回
        if (pi.isDexExtracted()) {
            
            return true;
        }

        ContentProviderClient cpc = getProvider(context);
        if (cpc == null) {
            return false;
        }
        //通過ContentProvider 的   update 方法去執行操作
        try {
            int r = cpc.update(PluginFastInstallProvider.CONTENT_URI,
                    PluginFastInstallProvider.makeInstallValues(pi),
                    PluginFastInstallProvider.SELECTION_INSTALL, null);
            
            return r > 0;
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        return false;
    }

1. 這裏首先獲取在UI進程中註冊的 ContentProvider 通過 ContentProvider 的update 方法完成操作,

//com.qihoo360.replugin.packages.PluginFastInstallProvider

public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
       

        if (TextUtils.isEmpty(selection)) {
            return 0;
        }
        switch (selection) {
            case SELECTION_INSTALL: {
                return install(values);
            }
        }
        return 0;
    }

    private int install(ContentValues cv) {
        if (cv == null) {
            return 0;
        }

        String pit = cv.getAsString(KEY_PLUGIN_INFO);
        if (TextUtils.isEmpty(pit)) {
            return 0;
        }
        // 插件信息 中的json字符串再轉回PluginInfo
        PluginInfo pi = PluginInfo.parseFromJsonText(pit);

        // 開始加載ClassLoader
        ClassLoader cl = PMF.getLocal().loadPluginClassLoader(pi);
        if (cl != null) {
            return 1;
        } else {
            return 0;
        }
    }

2. 在 update 方法中,先把PluginInfo  轉成 JSON 字符串 變成參數 然後 調用 install 方法 再把json 字符串轉成 PluginInfo 然後開始加載插件的dex

//com.qihoo360.loader2.PluginCommImpl

/**
     * 警告:低層接口
     * 調用此接口會“依據PluginInfo中指定的插件信息”,在當前進程加載插件(不啓動App)。通常用於“指定路徑來直接安裝”的情況
     * 注意:調用此接口將不會“通知插件更新”
     * Added by Jiongxuan Zhang
     * @param pi 插件信息
     * @return 插件的Resources
     */
    public ClassLoader loadPluginClassLoader(PluginInfo pi) {
        // 不從緩存中獲取,而是直接初始化ClassLoader
        Plugin p = mPluginMgr.loadPlugin(pi, this, Plugin.LOAD_DEX, false);
        if (p != null) {
            return p.mLoader.mClassLoader;
        }

        

        return null;
    }

 // 底層接口
    final Plugin loadPlugin(Plugin p, int loadType, boolean useCache) {
        if (p == null) {
            return null;
        }
        //這裏調用了load 方法。在插件加載中 我們知道 load 方法中有很多loadType 
        if (!p.load(loadType, useCache)) {
            if (LOGR) {
          
            return null;
        }
        return p;
    }

3 . dex 的釋放是通過load 方法實現的,之前我們說加載插件的時候 知道load 有很多loadType 加載整個插件type是LOAD_APP ,這裏loadPlugin 中 loadType就是 LOAD_DEX 表示加載dex, 加載成功的話 就會 在插件路徑下釋放dex 文件。

在preload 介紹中 我們知道這個過程比較耗時,記得使用的時候在線程中進行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章