(android-12)app开机启动失败

工作中遇到这么个问题,com.kos.launcher是自写app,它须要开机启动。在运行着android-12的firefly-rk3588s-pc上安装这apk,安装成功,之后运行也成功。但是,重启设备后,会随机出现没有开机启动。那次开机启动失败后,运行com.kos.launcher,然后再次重启,可能哪次就开机启动成功了。一旦有一次成功,后面开机启动好像就都成功了。

问题原因是运行com.kos.launcher后,对应com.kos.launcher的PackageUserState.stopped是被置为false,而且保存在mSettings。但重启后,开机阶段PackageManagerService读取mSettings,不知什么原因,PackageUserState.stopped变回true。于是导致IntentResolver.java中的buildResolveList认为com.kos.launcher不符合要求,不把它放入能接收android.intent.action.BOOT_COMPLETED广播的处理者集合。用户看到现象就是com.kos.launcher开机启动失败。

 

一、app的开机启动代码

开机启动用的这么个思路:Android手机开机后,会发送android.intent.action.BOOT_COMPLETED广播,app监听这个广播,收到时启动自个主Activity。

public class BootCompletedReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
            Intent intent2 = new Intent(context, app.class);
            intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent2);
        }
    }
}

以下是和BootCompletedReceiver配对的AndroidManifest.xml示例。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.kos.launcher">
    ...
    <application ...>
        ...  
        <receiver android:name=".BootCompletedReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

低android版本不清楚,至少从android-5.1起,一直到android-12,都有在发这个广播,app能用这种方法实现开机启动。当然,正如本文开头写的,在firefly-rk3588s-pc主板遇到随机启动失败。

 

二、查找原因

网上已有文章介绍android广播,像“Android S静态广播注册流程(广播2)”,“Android 11 广播的注册、发送和接收流程分析”。

2.1 com.kos.launcher是否被放入了能接收android.intent.action.BOOT_COMPLETED广播的处理者集合

当生成一个针对于action=android.intent.action.BOOT_COMPLETED的广播任务(BroadcastRecord)后,会向BroadcastHandler类型的mHandler发送了一个BROADCAST_INTENT_MSG消息,然后在handleMessage中进行处理。

<aosp>/frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java
------
    private final class BroadcastHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case BROADCAST_INTENT_MSG: {
                    if (DEBUG_BROADCAST) Slog.v(
                            TAG_BROADCAST, "Received BROADCAST_INTENT_MSG ["
                            + mQueueName + "]");
                    processNextBroadcast(true);
                } break;
                ...
            }
        }
    }

之前产生广播需求的可能来自不同进程,为方便同步,把从processNextBroadcast开始的广播处理逻辑放在了专门的BroadcastHandler线程。这里直接调用了BroadcastQueue的processNextBroadcast方法,它就是调用processNextBroadcastLocked。processNextBroadcastLocked逻辑可看相关文单,这里说几个结论。

  1. android.intent.action.BOOT_COMPLETED属于有序广播。也就是说,它不是存放在存储无序广播的mParallelBroadcasts。
  2. 数据结构BroadcastRecord用于表示某种action的一个广播任务。这里的action指的就是android.intent.action.BOOT_COMPLETED,android.intent.action.USER_STARTING,等。针对某种action自然会有很多app,像须要开机启动的android.intent.action.BOOT_COMPLETED就会有数十个,这些app是放在BroadcastRecord的“List receivers”字段
  3. 处理有序广播时,先用mDispatcher.getNextBroadcastLocked(now)从等待队得到一个等待处理的广播任务BroadcastRecord,存储在变量r。然后从r.receivers列表的索引“r.nextReceiver++”处取出一个“单元”。如果com.kos.launcher开机启动正常的话,那它应该是当中一个“单元”,“单元”类型是ResolveInfo,不是BroadcastFilter。
  4. 在处理有序广播上,一次processNextBroadcastLocked只能处理一个r.receivers[recIdx]。

回到此文目的,在出问题时,检查action=android.intent.action.BOOT_COMPLETED的BroadcastRecord,结果发现它的receivers中没有com.kos.launcher。既然如此,向前查为什么com.kos.launcher没有出现在receivers。

2.2 buildResolveList为什么认为com.kos.launcher不符合要求

broadcastIntentLocked负责产生BroadcastRecord。步骤是这样的:生成一个receivers,然后以这个receivers为参数构造一个BroadcastRecord,存储在变量r,并把r放入BroadcastQueue。

<aosp>/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
------
    @GuardedBy("this")
    final int broadcastIntentLocked(...) {
        ......
        // Figure out who all will receive this broadcast.
        List receivers = null;
        List<BroadcastFilter> registeredReceivers = null;
        // Need to resolve the intent to interested receivers...
        if ((intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY)
                 == 0) {
            receivers = collectReceiverComponents(
                    intent, resolvedType, callingUid, users, broadcastAllowList);
        }
        ......
    }

针对action=android.intent.action.BOOT_COMPLETED,生成receivers用的是上面的collectReceiverComponents。

<aosp>/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
------
    private List<ResolveInfo> collectReceiverComponents(Intent intent, String resolvedType,
            int callingUid, int[] users, int[] broadcastAllowList) {

        List<ResolveInfo> receivers = null;
        try {
            HashSet<ComponentName> singleUserReceivers = null;
            boolean scannedFirstReceivers = false;
            // 对单用户场景,users数组长度是1,并且user值是0。
            for (int user : users) {
                ......
                List<ResolveInfo> newReceivers = AppGlobals.getPackageManager()
                        .queryIntentReceivers(intent, resolvedType, pmFlags, user).getList();
                if (user != UserHandle.USER_SYSTEM && newReceivers != null) {
                    // 这个if的作用是当user是非USER_SYSTEM用户时,删除那些只能是USER_SYSTEM用户才能发出的广播。
                    // 对单用户场景,user值是0,因而不会进这个入口。
                    ......
                }
                if (newReceivers != null && newReceivers.size() == 0) {
                    // 如果newReceivers长度是0,置null。方便后面用“receivers == null”统一判断是不是空。
                    newReceivers = null;
                }
                if (receivers == null) {
                    // action=android.intent.action.BOOT_COMPLETED,有要开机启动的app,那newReceivers应该非空,进这个入口。
                    receivers = newReceivers;
                } else if (newReceivers != null) {
                    ......
                }
            }
        } catch (RemoteException ex) {
            // pm is in same process, this will never happen.
        }
        ...
        return receivers;
    }

针对action=android.intent.action.BOOT_COMPLETED,collectReceiverComponents是通过queryIntentReceiversInternal得到receivers。后者就一个操作:调用queryIntentReceiversInternal。

<aosp>/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
------
    private @NonNull List<ResolveInfo> queryIntentReceiversInternal(Intent intent,
            String resolvedType, int flags, int userId, boolean allowDynamicSplits) {
        。。。
        synchronized (mLock) {
            String pkgName = intent.getPackage();
            if (pkgName == null) {
                final List<ResolveInfo> result =
                        mComponentResolver.queryReceivers(intent, resolvedType, flags, userId);
                ...
            }
            ...
        }
    }

queryIntentReceiversInternal通过mComponentResolver.queryReceivers得到receivers。mComponentResolver类型是ComponentResolver。ComponentResolver.queryReceivers --> ReceiverIntentResolver.queryIntent --> ActivityIntentResolver.queryIntent --> MimeGroupsAwareIntentResolver.queryIntent --> IntentResolver-->queryIntent。经过一系列调用,会进入IntentResolver-->queryIntent。

<aosp>/frameworks/base/services/core/java/com/android/server/IntentResolver.java
------
    public List<R> queryIntent(Intent intent, String resolvedType, boolean defaultOnly,
            int userId) {
        String scheme = intent.getScheme();

        ArrayList<R> finalList = new ArrayList<R>();

        ......
        if (resolvedType == null && scheme == null && intent.getAction() != null) {
            firstTypeCut = mActionToFilter.get(intent.getAction());
            if (debug) Slog.v(TAG, "Action list: " + Arrays.toString(firstTypeCut));
        }

        FastImmutableArraySet<String> categories = getFastIntentCategories(intent);
        if (firstTypeCut != null) {
            buildResolveList(intent, categories, debug, defaultOnly, resolvedType,
                    scheme, firstTypeCut, finalList, userId);
        }
        ......
        return finalList;
    }

先是得到mActionToFilter,以action=android.intent.action.BOOT_COMPLETED为参数,生成firstTypeCut。再以firstTypeCut参数,调用buildResolveList,得到queryIntentReceiversInternal需要的List<ResolveInfo>,也就是broadcastIntentLocked需要的r.receivers。

出现的firstTypeCut,它是什么,是开机PackageManagerService.scanDirLI扫描出的各个app中那些个receiver、activity、service的集合?——基本是可这么认为。回到本文问题,查看firstTypeCut数值,它是包含com.kos.launcher这个app。于是接下要进入buildResolveList,寻找它在生成r.receivers时,为什么扔掉了com.kos.launcher。

<aosp>/frameworks/base/services/core/java/com/android/server/IntentResolver.java
------
    private void buildResolveList(...) {
        ......

        final int N = src != null ? src.length : 0;
        boolean hasNonDefaults = false;
        int i;
        F filter;
        for (i=0; i<N && (filter=src[i]) != null; i++) {
            int match;
            if (debug) Slog.v(TAG, "Matching against filter " + filter);

            if (excludingStopped && isFilterStopped(filter, userId)) {
                // 查下来,com.kos.launcher进入了这个入口,导致被扔掉
                if (debug) {
                    Slog.v(TAG, "  Filter's target is stopped; skipping");
                }
                continue;
            }
            ......
        }
        ......
    }

针对com.kos.launcher,isFilterStopped(filter, userId)判断出来是true,于是被buildResolveList扔掉。以下是isFilterStopped的逐步深入:ActivityIntentResolver.isFilterStopped ==> ComponentResolver.isFilterStopped ==> [PackageSetting]PackageSettingBase.getStopped(userId) ==> PackageUserState.stopped。也就是说,isFilterStopped函数的返回值就是PackageUserState中的stopped字段值。

每个apk都有一个PackageUserState,isFilterStopped来自PackageUserState的stopped字段。顾名思义,PackageUserState存储着用户使用该apk时产生的状态。所有apk的PackageUserState则统一存储在PackageManagerService.mSettings。具体到stopped,初始值是true,当运行apk时,它会被PackageManagerService.setPackageStoppedState设置为false,该函数同时会把notLaunched置为false。

综上所述,问题原因是运行com.kos.launcher后,对应com.kos.launcher的PackageUserState.stopped是被置为false,而且保存在mSettings。但重启后,开机阶段PackageManagerService读取mSettings,不知什么原因,PackageUserState.stopped变回true。于是导致IntentResolver.java中的buildResolveList认为com.kos.launcher不符合要求,不把它放入能接收android.intent.action.BOOT_COMPLETED广播的处理者集合。用户看到现象就是com.kos.launcher开机启动失败。

 

三、临时解决办法

没找到原因,只能找个临时办法。要修改android源码,思路是PackageManagerService.scanDirLI扫描到com.kos.launcher时,如果发现PackageUserState.stopped是true,那强制改到false。

<aosp>/frameworks/base/services/core/java/com/android/server/pm/ackageManagerService.java
------
    private AndroidPackage addForInitLI(ParsedPackage parsedPackage,
            @ParseFlags int parseFlags, @ScanFlags int scanFlags, long currentTime,
            @Nullable UserHandle user)
                    throws PackageManagerException {
        ......

        synchronized (mLock) {
            ......

            if (scanSystemPartition) {
                ......
            }
            // 在“synchronized (mLock)”末尾增加下面这个if判断
            if (installedPkgSetting != null && installedPkgSetting.name != null && installedPkgSetting.name.equals("com.kos.launcher")) {
                int userId = user.myUserId();
                boolean stopped = installedPkgSetting.getStopped(userId);
                // 一旦stopped被错误“恢复”true时,notLaunched往往也成了的true。上次启动时运行过,notLaunched应该是false。
                Log.d(TAG, "{leagor}[addForInitLI](1)name=" + installedPkgSetting.name + " stopped=" + stopped + " notLaunched=" + installedPkgSetting.getNotLaunched(userId));
                if (stopped) {
                    // apk是com.kos.launcher,并且PackageUserState.stopped是true
                    mSettings.setPackageStoppedStateLPw(this, parsedPackage.getPackageName(), false, userId);
                    // installedPkgSetting是引用,经过上面修改,此时的stopped和notLaunched都应该是false。
                    Log.d(TAG, "{leagor}[addForInitLI](2)name=" + installedPkgSetting.name + " stopped=" + installedPkgSetting.getStopped(userId) + " notLaunched=" + installedPkgSetting.getNotLaunched(userId));
                }
            }
        }
        ......
    }

经过修改,即使上一次只安装、没运行过com.kos.launcher,此次都会开机启动。除修改PackageUserState.stopped,还有notLaunched,上次开机,运行过的话,notLaunched应该是false。只是想改stopped,或许可改为调用installedPkgSetting.setStopped(false, userId)。

全部评论: 0

    写评论: