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 介绍中 我们知道这个过程比较耗时,记得使用的时候在线程中进行。

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