DroidPlugin之获取插件APK信息

最近在研究DroidPlugin源码及其实现机制,读这种大神级的代码甚是费脑。我基本是逐行阅读代码,为求甚解,花了不少时间和心思。这里,我也做个笔记供大家参考,这样就可以少走弯路。

Run it

为了解其工作DroidPlugin的工作原理,我们第一步应该是把Demo运行起来,其项目地址是:DroidPlugin。作者的Demo比较复杂,毕竟他要测试到所有的功能。所以这里,建议各位自己重写一个简单的Demo,然后运行起来。这样起码你知道如何使用DroidPlugin了。
在Demo中,与我们打交道的类是PluginManager,用该类中以下方法就可以完成最基本的插件调用。

//判断插件服务是否处于连接状态
public boolean isConnected() {
    return mHostContext != null && mPluginManager != null;
}

//将插件的ServiceConnection添加到缓存中
public void addServiceConnection(ServiceConnection sc) {
    sServiceConnection.add(new WeakReference<ServiceConnection>(sc));
}

//将插件的ServiceConnection从缓存中移除
public void removeServiceConnection(ServiceConnection sc) {
    Iterator<WeakReference<ServiceConnection>> iterator = sServiceConnection.iterator();
    while (iterator.hasNext()) {
        WeakReference<ServiceConnection> wsc = iterator.next();
        if (wsc.get() == sc) {
            iterator.remove();
        }
    }
}

//安装插件
public int installPackage(String filepath, int flags) throws RemoteException {
    try {
        if (mPluginManager != null) {
            int result = mPluginManager.installPackage(filepath, flags);
            Log.w(TAG, String.format("%s install result %d", filepath, result));
            return result;
        } else {
            Log.w(TAG, "Plugin Package Manager Service not be connect");
        }
    } catch (RemoteException e) {
        throw e;
    } catch (Exception e) {
        Log.e(TAG, "forceStopPackage", e);
    }
    return -1;
}
//删除插件
public void deletePackage(String packageName, int flags) throws RemoteException {
    try {
        if (mPluginManager != null) {
            mPluginManager.deletePackage(packageName, flags);
        } else {
            Log.w(TAG, "Plugin Package Manager Service not be connect");
        }
    } catch (RemoteException e) {
        throw e;
    } catch (Exception e) {
        Log.e(TAG, "deletePackage", e);
    }
}

Analyse it

首先来看看PluginManager相关的类图。


PluginManager相关的类图

从类图中我们可以看到,整个插件管理器是通过Binder机制来完成运作的。绝大部分插件管理相关的方法都在IPluginManagerImpl类中实现。

获取插件Apk的基本信息

Apk包信息用PackageInfo来表示,通过PackageInfo我们可以获取包名、版本号、版本代码以及四大组件相关信息等。通过以下类图,我们可以知道包信息相关的内容。


包相关信息类图.jpg

我们知道,要获取一个Apk的包相关信息,可以通过PackageManager来获取,但是这仅限于已经安装的apk,对于一个未安装的apk该如何获取。
在PackageManager中有一个名为getPackageArchiveInfo的方法,代码如下。

 /**
     * Retrieve overall information about an application package defined
     * in a package archive file
     *
     * @param archiveFilePath The path to the archive file
     * @param flags Additional option flags. Use any combination of
     * @return Returns the information about the package. Returns
     * null if the package could not be successfully parsed.
     *
     * @see #GET_ACTIVITIES
     * @see #GET_GIDS
     * @see #GET_CONFIGURATIONS
     * @see #GET_INSTRUMENTATION
     * @see #GET_PERMISSIONS
     * @see #GET_PROVIDERS
     * @see #GET_RECEIVERS
     * @see #GET_SERVICES
     * @see #GET_SIGNATURES
     *
     */
    public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) {
        final PackageParser parser = new PackageParser();
        final File apkFile = new File(archiveFilePath);
        try {
            PackageParser.Package pkg = parser.parseMonolithicPackage(apkFile, 0);
            if ((flags & GET_SIGNATURES) != 0) {
                parser.collectCertificates(pkg, 0);
                parser.collectManifestDigest(pkg);
            }
            PackageUserState state = new PackageUserState();
            return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0, null, state);
        } catch (PackageParserException e) {
            return null;
        }
    }

通过getPackageArchiveInfo方法,我们可以读取未安装插件的AndroidManifest.xml文件来获取packageName、versionName、versionCode、ActivityInfo、ServiceInfo、InstrumentationInfo等信息。

插件安装

插件的安装是通过IPluginManagerImpl的installPackage方法来完成的。

public int installPackage(String filepath, int flags) throws RemoteException {
    String apkfile = null;
    try {
        PackageManager pm = mContext.getPackageManager();
        /**
             * 调用getPackageArchiveInfo方法获取插件包的信息
             * */
        PackageInfo info = pm.getPackageArchiveInfo(filepath, 0);
        if (info == null) {
            return PackageManagerCompat.INSTALL_FAILED_INVALID_APK;
        }
        /**
             * 获取插件将要放置的路径,/data/data/com.HOST.PACKAGE/Plugin/PLUGIN.PKG/apk/apk-1.apk下
             * */
        apkfile = PluginDirHelper.getPluginApkFile(mContext, info.packageName);
        /**
             * 这里是是判断是否允许替换相同的插件包,这种写法应该是模仿系统的写法。
             * 可以通过PackageManager的getPackageArchiveInfo方法源码来看。
             * 通过标志位与系统预设类变量进行与运算
             * */
        if ((flags & PackageManagerCompat.INSTALL_REPLACE_EXISTING) != 0) {
            /**
                 * 强制停止插件进程,所有的插件都是运行在独立的进程
                 * */
            forceStopPackage(info.packageName);
            /**
                 * 删除先前插件的缓存,ApplicationInfo的dataDir路径下的所有file
                 * */
            if (mPluginCache.containsKey(info.packageName)) {
                deleteApplicationCacheFiles(info.packageName, null);
            }
            /**
                 * 删除原有插件apk
                 * */
            new File(apkfile).delete();
            /**
                 * 将插件apk复制到apkfile路径
                 * */
            Utils.copyFile(filepath, apkfile);
            /**
                 * PluginPackageParser可以理解为插件的PackageManager,作者基本上把所有组件相关的信息都解析出来了,牛逼!
                 * 所以作者写了PluginPackageParser这个类来获取四大组件、签名、权限等相关信息。
                 * */
            PluginPackageParser parser = new PluginPackageParser(mContext, new File(apkfile));
            /**
                 * 获取证书信息
                 * */
            parser.collectCertificates(0);
            /**
                 * 获取插件的权限信息和签名信息
                 * */
            PackageInfo pkgInfo = parser.getPackageInfo(PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES);
            if (pkgInfo != null && pkgInfo.requestedPermissions != null && pkgInfo.requestedPermissions.length > 0) {
                for (String requestedPermission: pkgInfo.requestedPermissions) {
                    boolean b = false;
                    /**
                         * 如果插件申明的权限,宿主没有的话,则安装插件失败
                         * **/
                    try {
                        b = pm.getPermissionInfo(requestedPermission, 0) != null;
                    } catch(NameNotFoundException e) {}
                    if (!mHostRequestedPermission.contains(requestedPermission) && b) {
                        Log.e(TAG, "No Permission %s", requestedPermission);
                        new File(apkfile).delete();
                        return PluginManager.INSTALL_FAILED_NO_REQUESTEDPERMISSION;
                    }
                }
            }
            /**
                 * 保存签名信息到DISK
                 * */
            saveSignatures(pkgInfo);
            //                if (pkgInfo.reqFeatures != null && pkgInfo.reqFeatures.length > 0) {
            //                    for (FeatureInfo reqFeature : pkgInfo.reqFeatures) {
            //                        Log.e(TAG, "reqFeature name=%s,flags=%s,glesVersion=%s", reqFeature.name, reqFeature.flags, reqFeature.getGlEsVersion());
            //                    }
            //                }
            /**
                 * 把插件的lib下so文件拷贝到/data/data/.../lib 下
                 * */
            copyNativeLibs(mContext, apkfile, parser.getApplicationInfo(0));
            //优化dex
            dexOpt(mContext, apkfile, parser);
            //缓存已经解析出来插件的PluginPackageParser信息(类似PackageInfo)
            mPluginCache.put(parser.getPackageName(), parser);
            //回调方法,通知已经完成
            mActivityManagerService.onPkgInstalled(mPluginCache, parser, parser.getPackageName());
            //发送安装完成的广播
            sendInstalledBroadcast(info.packageName);
            return PackageManagerCompat.INSTALL_SUCCEEDED;
        }
        //以下分析与上述通覆盖安装同理
        else {
        //代码省略........
           }
}

通过以上分析,我们可以知道,安装插件主要做了解析插件apk的PackageInfo,获取证书信息,读取so文件,加载dex等。前面我们有提到过使用getPackageArchiveInfo可以获取插件的PackageInfo,但是在installPackage方法中有PluginPackageParser类实例创建的过程,用PluginPackageParser来完成包相关信息的解析。
PluginPackageParser主要是通过反射来获取PackageInfo,而且还要做版本适配,很复杂。那为什么作者不直接getPackageArchiveInfo方法获取的PackageInfo?
上面有介绍到PackageInfo类相关的类图,其实整个PackageInfo所包含的信息是相当多的。我们通过getPackageArchiveInfo获取插件的PackageInfo仅仅是读取AndroidManifest.xml文件上的信息,有些信息是无法获取,比如ApplicationInfo中的processName、publicSourceDir、sourceDir,PackageInfo中的gids等,这些信息都是需要安装或运行后才能确定的。
下面,首先来看看PluginPackageParser相关的类图


PluginPackageParser相关类图

整个解析过程,看似很复杂,但是只要掌握了核心思想就很好理解了。

PluginPackageParser分析

我们继续分析上面提到过的getPackageArchiveInfo方法。

public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) {
        //PackageParser用于解析在disk上的apk安装包
        final PackageParser parser = new PackageParser();
        final File apkFile = new File(archiveFilePath);
        try {
            //获取Package对象
            PackageParser.Package pkg = parser.parseMonolithicPackage(apkFile, 0);
            if ((flags & GET_SIGNATURES) != 0) {
                //获取证书信息
                parser.collectCertificates(pkg, 0);
                parser.collectManifestDigest(pkg);
            }
           //每个用户关于package的状态信息,例如package是否安装,是否隐藏,是否停止等
            PackageUserState state = new PackageUserState();
           //调用generatePackageInfo方法返回PackageInfo
            return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0, null, state);
        } catch (PackageParserException e) {
            return null;
        }
    }

    /**
     *从以上代码分析中,我们可以看到PackageParser是完成包解析的核心步骤,接着分析
     *PackageParser类中的generatePackageInfo方法,因为generatePackageInfo方法在PackageParser中重载了, 
     *我们直接看该方法最终实现
     **/
public static PackageInfo generatePackageInfo(PackageParser.Package p,
            int gids[], int flags, long firstInstallTime, long lastUpdateTime,
            Set<String> grantedPermissions, PackageUserState state, int userId) {

        if (!checkUseInstalledOrHidden(flags, state)) {
            return null;
        }
        //创建PackageInfo,将Package中的信息赋给PackageInfo
        PackageInfo pi = new PackageInfo();
        pi.packageName = p.packageName;
        pi.splitNames = p.splitNames;
        pi.versionCode = p.mVersionCode;
        pi.baseRevisionCode = p.baseRevisionCode;
        pi.splitRevisionCodes = p.splitRevisionCodes;
        pi.versionName = p.mVersionName;
        pi.sharedUserId = p.mSharedUserId;
        pi.sharedUserLabel = p.mSharedUserLabel;
        pi.applicationInfo = generateApplicationInfo(p, flags, state, userId);
        pi.installLocation = p.installLocation;
        pi.coreApp = p.coreApp;
        //......省略中间代码,
        //生成ActivityInfo是通过调用generateActivityInfo方法,其他组件或包相关信息在PackageParser都有对应的生产方法。
        if ((flags&PackageManager.GET_ACTIVITIES) != 0) {
            int N = p.activities.size();
            if (N > 0) {
                if ((flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) {
                    pi.activities = new ActivityInfo[N];
                } else {
                    int num = 0;
                    for (int i=0; i<N; i++) {
                        if (p.activities.get(i).info.enabled) num++;
                    }
                    pi.activities = new ActivityInfo[num];
                }
                for (int i=0, j=0; i<N; i++) {
                    final Activity activity = p.activities.get(i);
                    if (activity.info.enabled
                        || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) {
                        pi.activities[j++] = generateActivityInfo(p.activities.get(i), flags,
                                state, userId);
                    }
                }
            }
        }
        //省略部分代码.......
        }
        return pi;
    }

通过以上简要分析,我们可以知道在PackageParser类中有很多可以获取包信息的方法,比如获取PackageInfo的generatePackageInfo,获取ServiceInfo的generateServiceInfo等,这些方法的名字绝大部分都是以generate开头的。
DroidPlugin作者通过反射的方法来获取PackageInfo、ActivityInfo等信息,因为PackageParser是hide类,所以每个系统版本获取包相关信息的实现可能不一样,所以作者做了版本兼容,这需要读完各个系统版本PackageParser类源码,辛苦作者了!
作者自己也定义了一个PackageParser类,用于完成与系统PackageParser类类似的功能。在PluginPackageParser的构造方法中通过调用自定义PackageParser类的newPluginParser方法来获取包相关信息。

public PluginPackageParser(Context hostContext, File pluginFile) throws Exception {
        mHostContext = hostContext;
        mPluginFile = pluginFile;
        //通过反射系统PackageParser相关方法来完成包相关信息解析
        mParser = PackageParser.newPluginParser(hostContext);
        //解析插件包
        mParser.parsePackage(pluginFile, 0);
        mPackageName = mParser.getPackageName();
        mHostPackageInfo = mHostContext.getPackageManager().getPackageInfo(mHostContext.getPackageName(), 0);
        //以下操作是将解析到的ActivityInfo进行缓存操作。
        //下面代码出现ComponentName,我想作者是为了模仿系统PackageManager类中相关的api
        //例如PackageManager中获取ActivityInfo的方法,getActivityInfo(ComponentName,int)
        List datas = mParser.getActivities();
        for (Object data : datas) {
            ComponentName componentName = new ComponentName(mPackageName, mParser.readNameFromComponent(data));
            synchronized (mActivityObjCache) {
                mActivityObjCache.put(componentName, data);
            }
            synchronized (mActivityInfoCache) {
                ActivityInfo value = mParser.generateActivityInfo(data, 0);
               //这个方法很重要,前面有提到过未安装的apk某些属性是无法获取到的,
               //该方法完成了ApplicationInfo中部分字段的赋值,比如processName、dataDir等。
               //类似的还有fixPackageInfo方法。
                fixApplicationInfo(value.applicationInfo);
                if (TextUtils.isEmpty(value.processName)) {
                    value.processName = value.packageName;
                }
                mActivityInfoCache.put(componentName, value);
            }

            List<IntentFilter> filters = mParser.readIntentFilterFromComponent(data);
            synchronized (mActivityIntentFilterCache) {
                mActivityIntentFilterCache.remove(componentName);
                mActivityIntentFilterCache.put(componentName, new ArrayList<IntentFilter>(filters));
            }
        }
    //......省略代码
    }

至此,parser package的过程大致讲完,如果遗漏的地方,欢迎指出。建议大家多读下DroidPlugin关于包解析的源码,作者某些思路相当精彩。

最后

不知道,大家有没有一个疑问,其实我们可以通过getPackageArchiveInfo方法来获取PackageInfo,然后通过类似fixApplicationInfo来完成包解析。但是作者并没有这样做,而是全部通过反射来完成。
我觉得应该有以下考量。
1.全部使用反射获取包信息,逻辑更清楚,而且更保障。
2.有些信息getPackageArchiveInfo获取不到,例如PermissionGroupInfo、PermissionInfo、IntentFilterInfo等。
3.作者将PluginParserManager完成类似PackageManager功能,比如getActivityInfo(ComponentName,int)方法,必须通过反射获取到PackageParser.Activity类实例。
如有疑问,欢迎交流!非常感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容