简介:这份安卓源码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:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name"> <receiver android:name=".BootReceiver" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> <service android:name=".CoreService" android:enabled="true" android:exported="false" /> </application>这里exported属性要留意。Receiver接收系统广播,必须允许系统进程访问,所以exported=true;Service只给本应用调用,exported=false更安全。在Android 12(API 31)上,显式声明exported是必须的,否则编译期报错并导致安装失败。
表格列出组件与权限的对应关系:
| 组件 | 声明要点 | 作用 |
|---|---|---|
| BootReceiver | exported=true,intent-filter包含BOOT_COMPLETED | 系统启动完成后接收广播 |
| CoreService | exported=false,onStartCommand返回START_STICKY | 在后台保持运行,并执行拉起APK逻辑 |
| 权限 | RECEIVE_BOOT_COMPLETED | 接收开机广播的唯一前提 |
| 权限 | FOREGROUND_SERVICE | Android 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的Intent,onStartCommand需要判空,否则拉起逻辑会收到一个空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大概率是把包名写在了一个静态变量里,但我们改进一下,把目标包名存到SharedPreferences:
public 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模式下的延迟执行,还不会像前台服务那样一直被用户盯着通知栏。
先定义Worker:
public 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 模拟开机广播,不再反复重启设备
每次改完代码,不需要真的重启设备来验证Receiver,adb就能模拟一次开机广播。注意系统可能不允许向普通应用发送受保护广播,但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输出中的app=ProcessRecord这一行能看到进程是否处于fg或fgs状态,fg是前台,fgs是前台服务。如果什么信息都没有,说明服务没起来。再用ps -A | grep runotherapk确认进程号,连续执行两次,中间间隔几秒钟,如果进程号变了,说明服务被系统杀了后又重建,这正好验证START_STICKY是否生效。
5.3 验证目标APK是否真的被拉起
拉起动作很容易因为包名不存在或目标未启动完成而失败。在Service的代码里加了返回值判断后,可以用命令行直接测试目标包的启动Intent:
adb 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里被忽略。把验证脚本跑通后,这一套自启动链路才能真正交付到现场。
本文还有配套的精品资源,点击获取