插件安裝的過程只將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 介紹中 我們知道這個過程比較耗時,記得使用的時候在線程中進行。