工作中遇到这么个问题,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逻辑可看相关文单,这里说几个结论。
- android.intent.action.BOOT_COMPLETED属于有序广播。也就是说,它不是存放在存储无序广播的mParallelBroadcasts。
- 数据结构BroadcastRecord用于表示某种action的一个广播任务。这里的action指的就是android.intent.action.BOOT_COMPLETED,android.intent.action.USER_STARTING,等。针对某种action自然会有很多app,像须要开机启动的android.intent.action.BOOT_COMPLETED就会有数十个,这些app是放在BroadcastRecord的“List receivers”字段
- 处理有序广播时,先用mDispatcher.getNextBroadcastLocked(now)从等待队得到一个等待处理的广播任务BroadcastRecord,存储在变量r。然后从r.receivers列表的索引“r.nextReceiver++”处取出一个“单元”。如果com.kos.launcher开机启动正常的话,那它应该是当中一个“单元”,“单元”类型是ResolveInfo,不是BroadcastFilter。
- 在处理有序广播上,一次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)。