源碼茶舍之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

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