RePlugin源码阅读---插件安装

403 阅读6分钟

Replugin源码阅读---框架初始化

1.RePlugin.install
/**
 * 安装或升级此插件 <p>
 * 注意: <p>
 * 1、这里只将APK移动(或复制)到“插件路径”下,不释放优化后的Dex和Native库,不会加载插件 <p>
 * 2、支持“纯APK”和“p-n”(旧版,即将废弃)插件 <p>
 * 3、此方法是【同步】的,耗时较少 <p>
 * 4、不会触发插件“启动”逻辑,因此只要插件“当前没有被使用”,再次调用此方法则新插件立即生效
 *
 * @param path 插件安装的地址。必须是“绝对路径”。通常可以用context.getFilesDir()来做
 * @return 安装成功的插件信息,外界可直接读取
 */
public static PluginInfo install(String path) {
    // 1.判断文件合法性
    File file = new File(path);
    if (!file.exists()) {
        return null;
    } else if (!file.isFile()) {
        return null;
    }
    // 2.省略p-n判断
	...
    // 3.安装插件
    return MP.pluginDownloaded(path);
}
2.MP.pluginDownloaded插件安装
/**
 * path: 插件的安装地址, 必须是"绝对路径", 通常可以用context.getFilesDir()来做
 * return: 安装成功的插件信息, 外界可直接读取
 */
public static final PluginInfo pluginDownloaded(String path) {
    /**
     * 问题描述:
     * 对于正在生效的插件,如果当前时机pluginHost没有存活,那么这里会先启动pluginHost,然后再调用它的PluginHost进程的pluginDownloaded接口
     * 这里的问题是:pluginHost进程在启动过程会通过扫描文件的方式将当前即将生效的插件识别到,
     * 而在进程ready后,再去调用pluginDownloaded接口的时候会认为它不是新插件,从而不会通过NEW_PLUGIN广播来周知所有进程新插件生效了
     * 因此,当前进程也不会同步新插件生效的逻辑。
     * so,问题就来了,当前进程新下载的插件由于pluginHost的逻辑无法正常生效。
     * 当然该问题只针对p-n格式的插件,而纯APK格式的插件不再使用进程启动的时候通过扫描文件目录的方式来来识别所有准备好的插件
	 * 解决办法:
     * 对于处于该流程的插件文件(p-n插件)加上lock文件,以表示当前插件正在生效,不需要plugHost进程再次扫描生效了,也就不存在新插件在新进程中成为了老插件
     */
    ProcessLocker lock = null;
    try {
        if (path != null) {
            File f = new File(path);
            String fileName = f.getName();
            String fileDir = f.getParent();
        }
        PluginInfo info = PluginProcessMain.getPluginHost().pluginDownloaded(path);
        return info;
    } catch (Throwable e) {
    } finally {
    }
    return null;
}
3.PluginProcessMain.getPluginHost
/**
 * sPluginHostLocal 常驻进程使用,非常驻进程为null 
 * sPluginHostRemote 非常驻进程使用,常驻进程为null,用于非常驻进程连接常驻进程  
 */
public static final IPluginHost getPluginHost() {
	// 1.sPluginHostLocal != null表示当前进程为常驻进程
    if (sPluginHostLocal != null) {
        return sPluginHostLocal;
    }
    // 2.非常驻进程, sPluginHostRemote表示进程有可能挂掉了
    if (sPluginHostRemote == null) {
        // 3.再次唤起常驻进程
        connectToHostSvc();
    }
    // 3.返回常驻进程的远程代理类
    return sPluginHostRemote;
}

  在分析Replugin初始化时, 如果当前进程为常驻进程, sPluginHostLocal = PmHostSvc, 如果为非常驻进程, sPluginHostLocal = null, 且sPluginHostRemote = PmHostSvc.

4.PmHostSvc.pluginDownloaded
@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-")) {
        ...
    } else {
    	// 插件安装
        pi = mManager.getService().install(path);
    }
    if (pi != null) {
        // 通常到这里,表示“安装已成功”,这时不管处于什么状态,都应该通知外界更新插件内存表
        syncInstalledPluginInfo2All(pi);
    }
    return pi;
}
5.Stub.install
方法调用链:
Stub.install()
---> PluginManagerServer.installLocked()

/**
 * path: 插件的安装地址, 必须是"绝对路径", 通常可以用context.getFilesDir()来做
 * return: 安装成功的插件信息, 外界可直接读取
 */
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) {
        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) {
            return null;
        } else if (checkResult == 0){
            instPli.setIsPendingCover(true);
        }
    }
    // 4. 将合法的APK改名后,移动(或复制,见RePluginConfig.isMoveFileWhenInstalling)到新位置
    // 注意:不能和p-n的最终释放位置相同,因为管理方式不一样
    if (!copyOrMoveApk(path, instPli)) {
        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;
}
6.PmHostSvc.syncInstalledPluginInfo2All
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,这样最外面拿到的就是安装成功的新插件信息,符合开发者的预期
    // 如果在上一步中是走更新逻辑这里获取到的parent是上一个插件信息对象
    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);
}

  如果这个插件正在运行则会记录这个要更新插件的信息到元插件信息对象上, 直到所有"正在使用插件"的进程结束并重启后才会生效.

7.PmBase.newPluginFound更新插件内存表
final void newPluginFound(PluginInfo info, boolean persistNeedRestart) {
    // 更新最新插件表
    PluginTable.updatePlugin(info);
    // 更新可加载插件表
    insertNewPlugin(info);
    // 清空插件的状态(解禁)
    PluginStatusController.setStatus(info.getName(), info.getVersion(), PluginStatusController.STATUS_OK);
    if (IPC.isPersistentProcess()) {
        persistNeedRestart = mNeedRestart;
    }
    // 通知本进程:通知给外部使用者
    Intent intent = new Intent(RePluginConstants.ACTION_NEW_PLUGIN);
    intent.putExtra(RePluginConstants.KEY_PLUGIN_INFO, (Parcelable) info);
    intent.putExtra(RePluginConstants.KEY_PERSIST_NEED_RESTART, persistNeedRestart);
    intent.putExtra(RePluginConstants.KEY_SELF_NEED_RESTART, mNeedRestart);
    LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
}
8.PmBase.insertNewPlugin
final void insertNewPlugin(PluginInfo info) {
    synchronized (LOCKER) {
        // 检查插件是否已经被禁用
        if (RePlugin.getConfig().getCallbacks().isPluginBlocked(info)) {
            return;
        }
        Plugin p = mPlugins.get(info.getName());
        // 如果是内置插件,新插件extract成功,则直接替换
        // TODO 考虑加锁?
        if (p != null && p.mInfo.getType() == PluginInfo.TYPE_BUILTIN && info.getType() == PluginInfo.TYPE_PN_INSTALLED) {
            // next
        } else if (p != null && p.isInitialized()) {
            // 检查该插件是否已加载
            // 设置是否需要重启标志
            mNeedRestart = true;
            return;
        }
        // 此处直接使用该插件,没有考虑是否只采用最新版
        Plugin plugin = Plugin.build(info);
        plugin.attach(mContext, mClassLoader, mLocal);
        // 同时加入PackageName和Alias(如有)
        putPluginObject(info, plugin);
    }
}

  Plugin与mContext、mClassLoader绑定, mClassLoader为宿主的ClassLoader, mContext为Application