源码茶舍之PackageManager获取注册Service数量问题

问题

今天有朋友遇到个问题,说bindService失败了,查了几步发现是由于PackageManager获取不到对应的Service组件导致的。具体示例代码如下:

val serviceInfos = packageManager.getPackageInfo("com.xxx.xxx", PackageManager.GET_SERVICES).services
Log.d("TEST", Arrays.toString(serviceInfos))

这里我们通过PackageManager获取到对应包名的PackageInfo,最终的serviceInfos是一个数组,包含该应用注册的所有Service组件
但不同时候打印出来的数组长度竟然不同,也就是说某些Service一会儿有一会儿没有,这是为什么呢?

溯源

要搞清楚上面的问题,我们就要追本溯源啦!在追踪的过程中我们时刻记得留意一切可能使services数组发生变化的逻辑

提示:以下Android系统源码均基于Android P。

先看看PackageInfo的源码中对services成员的注释描述:

/**
 * Array of all {@link android.R.styleable#AndroidManifestService
 * <service>} tags included under <application>,
 * or null if there were none.  This is only filled in if the flag
 * {@link PackageManager#GET_SERVICES} was set.
 */
public ServiceInfo[] services;

可以看出,这里只提到了该数组包含AndroidManifest.xml中注册的所有Service组件,并没有说明有何具体过滤限制。那我们就只能从services赋值的源头找寻了。

PackageManager只是一层API,我们需要看它对应的系统服务,那么就是PackageManagerService,getPackageInfo相关方法:

@Override
public PackageInfo getPackageInfo(String packageName, int flags, int userId) {
    return getPackageInfoInternal(packageName, PackageManager.VERSION_CODE_HIGHEST,
            flags, Binder.getCallingUid(), userId);
}

// 实际的内部方法,这里做了代码精简,只保留关键部分
private PackageInfo getPackageInfoInternal(String packageName, long versionCode,
        int flags, int filterCallingUid, int userId) {
    // ...

    // reader
    synchronized (mPackages) {
        // Normalize package name to handle renamed packages and static libs
        packageName = resolveInternalPackageNameLPr(packageName, versionCode);

        final boolean matchFactoryOnly = (flags & MATCH_FACTORY_ONLY) != 0;
        if (matchFactoryOnly) {
            final PackageSetting ps = mSettings.getDisabledSystemPkgLPr(packageName);
            if (ps != null) {
                // ...
                return generatePackageInfo(ps, flags, userId); // 生成PackageInfo实例
            }
        }

        PackageParser.Package p = mPackages.get(packageName);
        // ...
        if (!matchFactoryOnly && (flags & MATCH_KNOWN_PACKAGES) != 0) {
            final PackageSetting ps = mSettings.mPackages.get(packageName);
            // ...
            return generatePackageInfo(ps, flags, userId); // 生成PackageInfo实例
        }
    }
}

从getPackageInfoInternal方法的源码来看还只是一些权限校验和匹配,没有涉及到具体组件信息生成的逻辑,所以我们继续看generatePackageInfo方法:

private PackageInfo generatePackageInfo(PackageSetting ps, int flags, int userId) {
    // ...
    if (p != null) {
        // ...
        PackageInfo packageInfo = PackageParser.generatePackageInfo(p, gids, flags,
                ps.firstInstallTime, ps.lastUpdateTime, permissions, state, userId);
        // ...
        return packageInfo;
// ...

同样地,我们只保留关键代码,可以看到生成PackageInfo的过程实际上是由PackageParser来处理。而且,到这里flags都还没解析判断呢,系统怎么知道我需要获取的是什么组件呢是吧?没错,最终逻辑基本都在PackageParser的相关方法里了:

public static PackageInfo generatePackageInfo(PackageParser.Package p,
        int gids[], int flags, long firstInstallTime, long lastUpdateTime,
        Set<String> grantedPermissions, PackageUserState state, int userId) {
    // ...
    PackageInfo pi = new PackageInfo();
    pi.packageName = p.packageName;
    // ...
    if ((flags & PackageManager.GET_SERVICES) != 0) {
        // 这里的N就等于Manifest文件中实际声明的Service的数量
        final int N = p.services.size();
        if (N > 0) {
            int num = 0;
            final ServiceInfo[] res = new ServiceInfo[N];
            for (int i = 0; i < N; i++) {
                final Service s = p.services.get(i);
                // 关键就在这个判断,决定了哪些Service组件会被过滤掉
                if (state.isMatch(s.info, flags)) {
                    res[num++] = generateServiceInfo(s, flags, state, userId);
                }
            }
            // 由于返回的数组长度并不一定等于N,所以还需要专门trim一下数组
            pi.services = ArrayUtils.trimToSize(res, num);
        }
    }
    // ...
    return pi;
}

总算是找到老巢了,可以看到,最终返回的是pi对象,和传进来的p是不一样的。相关逻辑也很简单,从我的源码注释里可得知ServiceInfo数组之所以会发生变化,就是因为那个 isMatch 方法,如果它返回了false,那么这个Service组件不会返回给外部了。
继续深入,找到这个PackageUserState的isMatch方法:

/**
 * Test if the given component is considered installed, enabled and a match
 * for the given flags.
 *
 * <p>
 * Expects at least one of {@link PackageManager#MATCH_DIRECT_BOOT_AWARE} and
 * {@link PackageManager#MATCH_DIRECT_BOOT_UNAWARE} are specified in {@code flags}.
 * </p>
 */
public boolean isMatch(ComponentInfo componentInfo, int flags) {
    final boolean isSystemApp = componentInfo.applicationInfo.isSystemApp();
    final boolean matchUninstalled = (flags & PackageManager.MATCH_KNOWN_PACKAGES) != 0;
    if (!isAvailable(flags)
            && !(isSystemApp && matchUninstalled)) return false;
    if (!isEnabled(componentInfo, flags)) return false; // 重点关注

    if ((flags & MATCH_SYSTEM_ONLY) != 0) {
        if (!isSystemApp) {
            return false;
        }
    }

    final boolean matchesUnaware = ((flags & MATCH_DIRECT_BOOT_UNAWARE) != 0)
            && !componentInfo.directBootAware;
    final boolean matchesAware = ((flags & MATCH_DIRECT_BOOT_AWARE) != 0)
            && componentInfo.directBootAware;
    return matchesUnaware || matchesAware; // 重点关注
}

哟,瞧瞧,这限制真的不少啊。对于三方非系统应用来说,我们暂时只用关心两个return分支。

解决

从上述的isMatch源码来分析问题排查办法。

第一个即isEnabled的检查,这个我们可以对应Service组件中的 android:enabled 属性,也就是说当你的组件被禁用时,那么对应Service的ServiceInfo就不会返回给外部了,这个很好理解,组件不可用时,外部肯定不能获取其信息,所以你要去bindService之类的操作肯定是抛异常的。当然,此属性默认值是true,但我们不排除业务逻辑中有动态设置false的可能,这个具体参考PackageManager的setComponentEnabledSetting方法,此处不赘述。

第二个即组件direct-boot(直接启动模式)的相关设置,这是从7.1之后出现的特性,对应 android:directBootAware 属性,该属性默认是false,即不支持该模式,那么很可能你的应用在设备加密锁屏后获取不到所需要的Service组件。可将directBootAware属性设为true后再尝试是否能解决本文问题,若涉及到Context的,还需要额外操作,具体参考谷歌官方文档中对直接启动模式的详细介绍和适配方式:https://developer.android.com/training/articles/direct-boot.html

DirectX修复工具(DirectX Repair)是一款系统级工具软件,简便易用。本程序为绿色版,无需安装,可直接运行。 本程序的主要功能是检测当前系统的DirectX状态,如果发现异常则进行修复。程序主要针对0xc000007b问题设计,可以完美修复该问题。本程序中包含了最新版的DirectX redist(Jun2010),并且全部DX文件都有Microsoft的数字签名,安全放心。 本程序为了应对一般电脑用户的使用,采用了易用的一键式设计,只要点击主界面上的“检测并修复”按钮,程序就会自动完成校验、检测、下载、修复以及注册的全部功能,无需用户的介入,大大降低了使用难度。在常规修复过程中,程序还会自动检测DirectX加速状态,在异常时给予用户相应提示。 本程序适用于多个操作系统,如Windows XP(需先安装.NET 2.0,详情请参阅“致Windows XP用户.txt”文件)、Windows Vista、Windows 7、Windows 8、Windows 8.1、Windows 8.1 Update、Windows 10,同时兼容32位操作系统和64位操作系统。本程序会根据系统的不同,自动调整任务模式,无需用户进行设置。 本程序的V4.0版分为标准版、增强版以及在线修复版。所有版本都支持修复DirectX的功能,而增强版则额外支持修复c++的功能。在线修复版功能与标准版相同,但其所需的数据包需要在修复时自动下载。各个版本间,主程序完全相同,只是其配套使用的数据包不同。因此,标准版和在线修复版可以通过补全扩展包的形式成为增强版。本程序自V3.5版起,自带扩展功能。只要在主界面的“工具”菜单下打开“选项”对话框,找到“扩展”标签,点击其中的“开始扩展”按钮即可。扩展过程需要Internet连接,扩展成功后新的数据包可自动生效。扩展用时根据网络速度不同而不同,最快仅需数秒,最慢需要数分钟,烦请耐心等待。如扩展失败,可点击“扩展”界面左上角小锁图标切换为加密连接,即可很大程度上避免因防火墙或其他原因导致的连接失败。 本程序自V2.0版起采用全新的底层程序架构,使用了异步多线程编程技术,使得检测、下载、修复单独进行,互不干扰,快速如飞。新程序更改了自我校验方式,因此使用新版本的程序时不会再出现自我校验失败的错误;但并非取消自我校验,因此程序安全性与前版本相同,并未降低。 程序有更新系统c++功能。由于绝大多数软件运行时需要c++的支持,并且c++的异常也会导致0xc000007b错误,因此程序在检测修复的同时,也会根据需要更新系统中的c++组件。自V3.2版本开始使用了全新的c++扩展包,可以大幅提高工业软件修复成功的概率。修复c++的功能仅限于增强版,标准版及在线修复版在系统c++异常时(非丢失时)会提示用户使用增强版进行修复。除常规修复外,新版程序还支持C++强力修复功能。当常规修复无效时,可以到本程序的选项界面内开启强力修复功能,可大幅提高修复成功率。请注意,请仅在常规修复无效时再使用此功能。 程序有两种窗口样式。正常模式即默认样式,适合绝大多数用户使用。另有一种简约模式,此时窗口将只显示最基本的内容,修复会自动进行,修复完成10秒钟后会自动退出。该窗口样式可以使修复工作变得更加简单快速,同时方便其他软件、游戏将本程序内嵌,即可进行无需人工参与的快速修复。开启简约模式的方法是:打开程序所在目录下的“Settings.ini”文件(如果没有可以自己创建),将其中的“FormStyle”一项的值改为“Simple”并保存即可。 新版程序支持命令行运行模式。在命令行中调用本程序,可以在路径后直接添加命令进行相应的设置。常见的命令有7类,分别是设置语言的命令、设置窗口模式的命令,设置安全级别的命令、开启强力修复的命令、设置c++修复模式的命令、控制Direct加速的命令、显示版权信息的命令。具体命令名称可以通过“/help”或“/?”进行查询。 程序有高级筛选功能,开启该功能后用户可以自主选择要修复的文件,避免了其他不必要的修复工作。同时,也支持通过文件进行辅助筛选,只要在程序目录下建立“Filter.dat”文件,其中的每一行写一个需要修复文件的序号即可。该功能仅针对高级用户使用,并且必须在正常窗口模式下才有效(简约模式时无效)。 本程序有自动记录日志功能,可以记录每一次检测修复结果,方便在出现问题时,及时分析和查找原因,以便找到解决办法。 程序的“选项”对话框中包含了7项高级功能。点击"常规”选项卡可以调整程序的基本运行情况,包括日志记录、安全级别控制、调试模式开启等。只有开启调试模式后才能在C
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页