ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Android开机自启动与后台保活实战:拉起指定APK的完整方案

Android开机自启动与后台保活实战:拉起指定APK的完整方案 简介这份安卓源码DEMO面向需要在后台保持应用运行、并实现开机自启指定APK的开发者尤其适合工具型或服务型应用场景。压缩包共54个文件以java源码、xml配置、class编译文件为主内含apk安装包与jar依赖库整体仅1.31MB结构清晰便于直接导入工程分析。已有307人学习下载适合有一定安卓基础、希望深入理解Service与BroadcastReceiver用法的学习者。资源完整演示了注册ACTION_BOOT_COMPLETED广播、配置RECEIVE_BOOT_COMPLETED权限、自定义Service启动模式及前台服务等关键步骤并涵盖Android O及以上后台限制下的处理思路。通过阅读和调试该Demo可快速掌握开机自启流程与服务生命周期管理为开发保活类应用提供可复用的参考模板。1. 开机自启动与后台保活是一个四组件的协同问题很多做工具类APP、车机、电子班牌或工控设备的人都遇到过同一个需求设备上电后不需要点图标应用自己就起来了而且另一个指定的业务APK也要被拉起来。这份“安卓Android源码——后台保持运行开机后自动启动设定好的APK的DEMO”就是解决这个场景的示例代码。它把开机广播、后台Service、Intent拉起、权限配置串成了一条完整链路。拆开看会发现它并不神奇但有四个关键点必须一起配合缺一个都会导致自启动失败或Service被杀。而且Android 8.0之后系统对后台Service的限制让这种老写法变得脆弱。这篇文我按这个DEMO的源码逻辑走一遍顺便把它改造成能在Android 7到13上稳定跑的工程方案。2. 源码骨架拆解Manifest、广播接收器与Service的协作方式2.1 从AndroidManifest.xml看权限和组件声明打开DEMO的AndroidManifest.xml首先看到的是权限声明。要接收开机广播必须声明android.permission.RECEIVE_BOOT_COMPLETED否则系统直接把广播丢弃这一点经常被忽略。除此之外如果Service要常驻还要考虑FOREGROUND_SERVICE权限以及启动外部APK时在Android 11 必须处理的包可见性声明。下面是一个整理后的Manifest关键片段uses-permission android:nameandroid.permission.RECEIVE_BOOT_COMPLETED / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.QUERY_ALL_PACKAGES / application android:icondrawable/ic_launcher android:labelstring/app_name receiver android:name.BootReceiver android:enabledtrue android:exportedtrue intent-filter action android:nameandroid.intent.action.BOOT_COMPLETED / /intent-filter /receiver service android:name.CoreService android:enabledtrue android:exportedfalse / /application这里exported属性要留意。Receiver接收系统广播必须允许系统进程访问所以exportedtrueService只给本应用调用exportedfalse更安全。在Android 12API 31上显式声明exported是必须的否则编译期报错并导致安装失败。表格列出组件与权限的对应关系组件声明要点作用BootReceiverexportedtrueintent-filter包含BOOT_COMPLETED系统启动完成后接收广播CoreServiceexportedfalseonStartCommand返回START_STICKY在后台保持运行并执行拉起APK逻辑权限RECEIVE_BOOT_COMPLETED接收开机广播的唯一前提权限FOREGROUND_SERVICEAndroid 8.0 使用前台服务必需2.2 开机广播的接收与处理逻辑DEMO里的BootReceiver继承了BroadcastReceiver核心代码很直接public class BootReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { // 开机完成后启动常驻服务 Intent service new Intent(context, CoreService.class); context.startService(service); } } }注意从Android 8.0开始隐式广播不再支持在Manifest中静态注册的Receiver但BOOT_COMPLETED是个例外它仍然可以通过Manifest注册接收。这是为什么这份老DEMO还能继续用的原因之一。不过这里有一个前置条件应用必须至少被手动启动过一次系统才会把开机广播交给它。原因要追溯到Android 3.1引入的停止状态Force stop未启动过的应用处于stopped状态所有外部广播都不会触发其Receiver。实际工程项目里我通常会让应用的主Activity在首启时把一个引导开关打开或者使用adb shell am start -n 包名/.MainActivity启动一次。在车机和工控场景里这一步可以在出厂刷机后通过脚本完成也可以直接给出“安装后必须打开一次”的要求。2.3 Service如何在后台“保持运行”CoreService是DEMO里的核心它的onStartCommand里往往写着几行关键代码Override public int onStartCommand(Intent intent, int flags, int startId) { // 返回START_STICKY让系统在低内存杀进程后尝试重建 return START_STICKY; }只写这一行还不够。系统杀掉服务后重建时会传入null的IntentonStartCommand需要判空否则拉起逻辑会收到一个空Intent直接崩掉。真正要“保持运行”需要考虑两点一是提高进程优先级二是当系统资源紧张时如何自恢复。前台服务是最高优先级中的常见手段把Service变成前台服务需要调用startForeground。下面这段代码是我在CoreService中使用前台服务的标准写法Override public void onCreate() { super.onCreate(); if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( keep_alive, Keep Alive, NotificationManager.IMPORTANCE_LOW); NotificationManager manager getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } Notification notification new NotificationCompat.Builder(this, keep_alive) .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(守护服务运行中) .setContentText(正在保持目标APK可拉起) .build(); startForeground(1, notification); }这段代码在Android 8.0以后是必须的因为8.0系统对后台Service启动时间有严格限制在后台运行的Service不能长时间执行任务但通过startForeground先转成前台服务就把进程从“缓存”级别提升到“感知”级别。注意如果使用startForegroundService()方法启动Service必须在5秒内调用startForeground()否则会抛出ForegroundServiceDidNotStartInTimeException。代价是通知栏始终有一个常驻通知部分产品设计上会把它做成低敏感小图标但不能诱导用户关闭通知否则在Android 13上无法正常显示前台服务通知。3. 拉起指定APK的两种实现路径与选型3.1 通过包名拉起已安装应用DEMO的文件名其实已经把核心功能点出来了RunOtherAPK。要实现“启动另一个APK”最常见也是最安全的方式是通过目标应用的包名来拉起而不是直接操作APK文件因为包名在系统里是全局唯一的直接拿到启动入口Intent就能调用。public boolean launchApp(Context context, String packageName) { PackageManager pm context.getPackageManager(); try { Intent intent pm.getLaunchIntentForPackage(packageName); if (intent ! null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); return true; } } catch (Exception e) { Log.e(RunOtherAPK, launch failed: e.getMessage()); } return false; }getLaunchIntentForPackage返回的是该应用在Manifest中声明的launcher Activity对应的Intent通常就是用户点击桌面图标时发起的那个Intent。对于没有主Activity的应用比如纯后台服务应用这个方法会返回null此时可以通过pm.getPackageInfo(packageName, 0)判断包是否存在再用ACTION_MAIN和CATEGORY_LAUNCHER的组合去查。另外从Android 11开始应用之间默认是包不可见的直接查其他包名时必须在Manifest里加入queries声明或QUERY_ALL_PACKAGES权限否则会返回空。这部分我在2.1的Manifest里已经加上了权宜之计如果要上架应用市场官方规定必须声明具体用途。3.2 通过安装APK文件并启动另一种场景是目标APK不是预置应用而是在系统启动后才需要从某目录中读取并静默安装后启动。这种情况比拉起已安装要复杂得多。原版DEMO里存放了一个APK文件RunOtherAPK.apk这也意味着它确实可能在演示“安装并拉起”这个动作。从实现上讲直接弹出系统安装界面很简单但要注意Android 7.0的FileUriExposedException必须用FileProvider优雅地分享文件public void installApk(Context context, File apkFile) { Uri apkUri; if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { apkUri FileProvider.getUriForFile( context, context.getPackageName() .fileprovider, apkFile); } else { apkUri Uri.fromFile(apkFile); } Intent intent new Intent(Intent.ACTION_VIEW); intent.setDataAndType(apkUri, application/vnd.android.package-archive); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); context.startActivity(intent); }这段代码会弹出系统安装确认界面需要用户点“安装”才能继续。如果设备已root或应用是系统签名可以直接执行pm install -r实现真正静默安装。个人做工具型Demo时我更推荐先让用户确认一次因为静默安装的兼容性非常差不同ROM对pm install的权限策略不一样很容易在交付时被客户打回来。3.3 把“设定好的APK”做成可配置项如果每次启动都拉起同一个包名直接在代码里写常量就够了但实际项目里经常遇到同一套源码给不同客户交付、每个客户需要拉起不同应用的情况。这时候把包名放到一个配置中心就显得很舒服。原DEMO大概率是把包名写在了一个静态变量里但我们改进一下把目标包名存到SharedPreferencespublic static String getTargetPackage(Context context) { SharedPreferences sp context.getSharedPreferences(run_other_apk, MODE_PRIVATE); String pkg sp.getString(target_package, null); if (pkg null) { // 默认包名 pkg com.example.target; } return pkg; }拿到配置后Service里做一个循环拉起处理系统刚开机时目标应用可能还没注册完成的情况private void startTargetAppWithRetry() { String targetPackage getTargetPackage(getApplicationContext()); for (int i 0; i 5; i) { if (launchApp(getApplicationContext(), targetPackage)) { break; } try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }注意Thread.sleep不能放在主线程这里最好放到ExecutorService或HandlerThread。我在实际项目里甚至会先轮询PackageManager判断目标包是否已安装再执行拉起否则直接sleep一轮效率太低。4. 老DEMO在Android 8.0的兼容性改造从保活到合理复用4.1 后台限制对Service的冲击原版DEMO在Android 7.1及以下可以跑得很稳但放到Android 8.0以后如果直接startService一个非前台Service系统在应用处于后台时会直接抛出IllegalStateException即使服务在前台系统也会在Doze模式下暂停它的网络和计算资源。更重要的是国内厂商ROM普遍把“自启动”权限默认关闭即使Manifest写了对系统仍然会拦截开机广播。这导致“后台保持运行”这个原始诉求在原生Android上越来越难。但注意正确理解是系统不再允许无限制的常驻但仍然允许通过前台服务、WorkManager、AlarmManager等实现“有意义地持续运行”。对车机/工控这类设备常见做法是把应用放到电池优化白名单里并保持充电状态。在改造时我的建议是保留原来的BootReceiver把CoreService改成前台服务并引入一个前台服务通知图标。同时在onStartCommand里要处理Intent为null的情况这是系统重建服务时最容易踩的坑。如果服务被系统杀掉START_STICKY会尝试重建但如果重建时设备还是处于低内存状态这次重建也会失败。因此我在Service启动后会调用startForeground提升进程优先级提高存活概率。4.2 用WorkManager替代常驻Service的现代化方案如果业务本质上只是“开机后做一次轻量检测然后拉起APK”那就不需要长期存活Service。Android Jetpack的WorkManager在这里更合适它内部封装了JobScheduler和AlarmManager能自动处理Doze模式下的延迟执行还不会像前台服务那样一直被用户盯着通知栏。先定义Workerpublic class LaunchWorker extends Worker { public LaunchWorker(NonNull Context context, NonNull WorkerParameters params) { super(context, params); } NonNull Override public Result doWork() { // 这里执行拉起APK的逻辑 boolean launched RunOtherApkHelper.launch(getApplicationContext(), com.example.target); return launched ? Result.success() : Result.retry(); } }然后在BootReceiver的onReceive中调度一个一次性任务public void onReceive(Context context, Intent intent) { if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { OneTimeWorkRequest request new OneTimeWorkRequest.Builder(LaunchWorker.class) .setInitialDelay(10, TimeUnit.SECONDS) .build(); WorkManager.getInstance(context).enqueue(request); } }延迟10秒是为了等系统把已安装应用都索引完避免目标应用还没处于可启动状态。WorkManager不保证精确时间但能保证“任务最终会被执行”即使设备重启已持久化的任务也会在下次条件满足时恢复。这种模式尤其适合“开机后只需要拉一次”的场景。4.3 厂商ROM的“自启动管理”才是最大变量做过国内Android开发的几乎都遇到过华为、小米、OPPO、vivo默认不允许第三方应用自启动即使给了BOOT_COMPLETED权限开机广播也可能被系统拦截。所以这份DEMO要真正商用必须引导用户把应用加入“自启动管理”和“后台弹出界面”白名单。下面这个表格是我在各厂商设置里经常要用到的路径细节会随系统版本变化厂商设置路径大致关键开关小米设置-应用设置-应用管理-自启动自启动 省电策略无限制华为手机管家-应用启动管理关闭“自动管理”手动允许自启动 关联启动OPPO/vivo设置-电池-后台耗电管理允许后台高耗电 自启动三星设置-应用程序-电池-后台使用限制取消“正在优化电池使用量”很多开发者以为写了Receiver和Service就万事大吉结果设备一重启就毫无反应日志里连Receiver都没进入这就是被ROM的自启动策略拦掉了。在交付设备前把这些开关批量确认一遍比调一天代码都有效。如果是系统级应用可以把APK放到/system/app下并使用系统签名那样可以绕过大部分第三方的自启动限制。5. 验证技巧用adb和日志把整个自启动链路看通透5.1 模拟开机广播不再反复重启设备每次改完代码不需要真的重启设备来验证Receiveradb就能模拟一次开机广播。注意系统可能不允许向普通应用发送受保护广播但am broadcast命令可以指定包名收窄范围adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -p com.example.runotherapk --receiver-foreground命令中-p指定包名避免广播被其他应用误接收。如果Receiver已经启动Service再来看日志adb logcat -s BootReceiver CoreService RunOtherAPK我一般会在Receiver和Service的关键入口各打一条Log.d这样就能确认广播真的收到了、服务真的起了、拉起动作是否执行。如果没有日志输出先检查Manifest的权限和exported声明再到ROM设置里看自启动开关。5.2 确认Service存活状态与进程优先级服务起来之后用dumpsys activity services过滤自己的组件名adb shell dumpsys activity services com.example.runotherapk输出中的appProcessRecord这一行能看到进程是否处于fg或fgs状态fg是前台fgs是前台服务。如果什么信息都没有说明服务没起来。再用ps -A | grep runotherapk确认进程号连续执行两次中间间隔几秒钟如果进程号变了说明服务被系统杀了后又重建这正好验证START_STICKY是否生效。5.3 验证目标APK是否真的被拉起拉起动作很容易因为包名不存在或目标未启动完成而失败。在Service的代码里加了返回值判断后可以用命令行直接测试目标包的启动Intentadb shell am start -W -n com.example.target/.MainActivity-W参数会等待Activity启动完成并打印耗时如果目标包名不对这里会直接提示Error type 3或Error: Activity not started。结合这个验证就能准确判断是Service没执行到启动代码还是目标APK本身不可拉起。最后补一个高频踩坑点在Service中启动Activity一定要加上FLAG_ACTIVITY_NEW_TASK因为Service上下文没有Activity对应的返回栈不加这个flag会直接抛出AndroidRuntimeException。这个坑我在做车辆设备定制时遇到过不止一次而且它往往在Demo里被忽略。把验证脚本跑通后这一套自启动链路才能真正交付到现场。本文还有配套的精品资源点击获取
返回列表