赞
踩
https://bwshen.blog.csdn.net/article/details/126435583
众所皆知,日活率是一款 App 的核心绩效指标,日活量不仅反应了应用的受欢迎程度,同时反应了产品的变现能力,进而直接影响盈利能力和企业估值,所以对于国内各个提供了 APP 业务的厂商来说,如何提高 APP 的日活量成为共同关心的利益问题。同时对于黑产恶意 APP 应用来说,为了自己能运行起来干坏事,需要寻求自启动且在后台保活的路径,因此 APP 自启动和保活手段也就成为了灰色产业孜孜不倦的目标……
于是便自然而然出现了各种 APP 都企图实现开机自启动、互相拉活的乱象。所幸国内手机厂商基本都在自己的 ROM 里实现了一套管理应用自启动的机制,来限制以上的“不良”行为。本文来学习、总结下目前 Android 应用自启动与保活的手段,同时分析下手机厂商对此的限制措施。
先来看看某国产手机的启动管理功能记录的 App 启动记录:
可以看到,你的 Android 手机上有多少 App 想在你不知情的情况下启动,企图干一些不为人知的事情……
下面就先来看下实现 APP 自启动或保活的技术手段都有哪些。
先回顾下广播接收器 Receiver 的两种注册方式:
Context.registerReceiver()
注册广播接收器,此类广播接收器是在应用已启动后,通过代码进行动态注册的,故需要在程序已运行的状态下才能收到指定的广播。可以看到静态注册的广播接收器具有在应用未启动的状态下就接收到广播的特点,故恶意 App 可以尝试借助静态注册的广播接收器监听系统一些事件触发的系统广播(比如网络状态变化广播、电源连接状态变化广播、系统开机广播等等)来实现自启动。
比如 Android 系统在开机完成后会发送一条"android.intent.action.BOOT_COMPLETED"
系统广播,那么恶意 App 可以静态注册如下 Receiver 来实现对开机广播的监听:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<receiver
android:name=“.BootCompleteReceiver”
android:enabled=“true”
android:exported=“true”>
<intent-filter>
<action android:name=“android.intent.action.BOOT_COMPLETED”/>
</intent-filter>
</receiver>
接着在 BootCompleteReceiver 的 onReceive 函数中实现自己启动后相干的动作(此处仅以弹窗为例):
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
}
}
以上通过静态注册的广播接收器来监听系统开机广播并实现开机自启动的方案,在 Android 6.0 模拟器上是完全可行的,至于说对于高版本的 Android 系统已经国内厂商手机的限制策略下是否还可行,后面会单独分析。
除了应用自启动之外,还有一个叫“关联启动”的概念,即 A 应用拉活 B 应用。比如以下来自某国产手机的应用启动管理处的启动记录:
说实话,抖音被今日头条启动我能理解,但是肯德基 App 企图启动动卡空间、知识星球是想干啥……
至于应用关联启动的实现方案就很简单了,A 应用在自身已经启动运行的状态下,通过组件调用的方式拉起 B 应用的组件即可。当然了,从上图可以看到手机厂商的自启动管理机制帮我们自动拦截、禁止了这种操作,具体的启动管理机制下文会展开。
以下为 A 应用通过发送广播给 B 应用静态注册的广播接收器,从而拉活 B 应用的示例代码:
//发送广播给其他应用程序的Receiver
Intent intent = new Intent();
//Android 8以后想给静态注册的广播接收器(应用无需启动即可接收广播)发送广播,必须指定接收器的包名类名
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
intent.setComponent(new ComponentName("com.Tr0e.example", "com.Tr0e.example.MyExampleReciver"));
}
intent.setAction("android.intent.action.Mybroadcast");
sendBroadcast(intent);
整个关联启动的链路如下图所示:
【注意】本小节多数内容摘自:安卓App通过“自启动/关联启动”唤醒会造成用户个人信息泄露吗。
除了自启动、关联启动,还有一类是消息推送 SDK 触发的启动,某米手机的启动管理上可以看到相关信息:
下面就来了解下 “消息推送” 和 “进程保活” 这两个概念的关联关系。
无论是 iOS 系统还是安卓系统,App 从服务器向 App 客户端推送消息是常见的行为。iOS 的消息推送由苹果的系统和服务器统一管理,这套信息推送机制名为苹果推送通知服务 APNs(Apple Push Notification service)。用户的 iPhone 手机与苹果提供的 APNs 服务器保持长连接,应用需要推送消息时,先将这条信息的提醒推送到苹果的服务器端,再由苹果的服务器转发到目标用户手机。于是,用户的手机上就会弹出信息通知。APNs 的好处是只要本地开启了推送权限,应用在未唤醒的条件下无需后台运行就能实现消息推送。因此,iOS应用不需要常驻后台,也不用随时保持网络长连接。
但是,目前国内安卓生态下没有类似 iPhone 的统一消息推送机制,信息推送主要通过三种方式方式实现:
为了保证应用进程被系统清理之后依然能收到推送,有的第三方推送 SDK 采用了联合唤醒的机制,只要使用了同一家的 SDK,启动其中一个App 的时候就会唤醒其它所有集成了该家 SDK 的 App 推送进程,以保证所有 App 消息推送的送达率。
鉴于以上原因,App 为了满足及时向客户端手机推送信息的需求,就要尽可能与消息推送服务器保持长连接。如果 App 没有服务进程,一旦用户选择不主动打开 App,就无法与用户进行任何通信,久而久之用户就会弃之不用甚至卸载,因而 App 会想方设法的让自己在系统中“保活”。如果 App 开发者选择了采用第三方推送 SDK 提供的联合唤醒的机制或者其他类似唤醒机制来“保活”,这就可能导致了大量的服务进程在后台被唤醒、驻留,从而造成了应用的交叉唤醒、关联启动的现象。
为了解决这种乱象,2017年,由工信部指导成立的包含了主流手机厂商和用户基数大的 App 开发商组成的 “安卓统一推送联盟”,旨在推动各应用运营者能够通过的统一推送服务的完成消息推送,各应用无需自己考虑消息推送的问题,把这个问题交由安卓系统层面去解决,从而避免自启动、关联启动方式的滥用。
Android 对于内存的回收主要依靠 Low Memory Killer 完成:系统出于用户体验和性能的考虑,app 在退到后台时,系统并不会立即将其 kill 掉,而是将其缓存起来;但是当打开的应用越多,后台缓存的进程也就越多,也就意味着系统可用内存越来越少;那么当内存不足时,系统就根据 oom_adj 值触发相应力度的进程回收机制来判断要杀死哪些进程,以释放出内存来供当前的应用使用,这套杀死进程回收内存的机制就叫 Low Memory Killer。
进程优先级
其中一个重要的判断依据就是进程优先级,要知道 Android 系统会尽量长时间保持应用进程,但是为了新建进程或运行更重要的进程,需要清除一些旧进程来回收内存;为了确保保留或终止哪些进程,系统会对进程进行分类,确定进程优先级;需要时系统首先清除优先级最低的进程,再清除稍低的进程,以此类推来回收资源;其中进程优先级划分如下:
综上可以看到,如果 App 进程处于后台运行的状态,是存在被 Android 进程回收机制收拾掉的风险的。
为了提高应用的存活时间和存活率,一种常见的保活手段就是调用系统 api 启动一个前台的 Service 进程,这样会在系统的通知栏生成一个 Notification,用来让用户知道有这样一个 app 在运行着,哪怕当前的 app 退到了后台,当时该 App 仍能保持较高的进程优先级。如下方的 LBE 和 QQ 音乐:
应用程序可以通过如下代码定义一个简单的前台服务程序:
public class ForegroundService extends Service{ //服务通信 @Nullable @Override public IBinder onBind(Intent intent){ //与Activity进行通信 return null; }
<span class="token comment">//服务创建时</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">onCreate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//服务创建时创建前台通知</span> <span class="token class-name">Notification</span> notification <span class="token operator">=</span> <span class="token function">createForegroundNotification</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//启动前台服务</span> <span class="token function">startForeground</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span>notification<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//有业务逻辑的代码可写在onCreate下</span> <span class="token punctuation">}</span> <span class="token comment">//服务销毁时</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span> <span class="token comment">//在服务被销毁时,关闭前台服务</span> <span class="token function">stopForeground</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">//创建前台通知,可写成方法体,也可单独写成一个类</span> <span class="token keyword">private</span> <span class="token class-name">Notification</span> <span class="token function">createForegroundNotification</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span> <span class="token comment">//前台通知的id名,任意</span> <span class="token class-name">String</span> channelId <span class="token operator">=</span> <span class="token string">"ForegroundService"</span><span class="token punctuation">;</span> <span class="token comment">//前台通知的名称,任意</span> <span class="token class-name">String</span> channelName <span class="token operator">=</span> <span class="token string">"Service"</span><span class="token punctuation">;</span> <span class="token comment">//发送通知的等级,此处为高,根据业务情况而定</span> <span class="token keyword">int</span> importance <span class="token operator">=</span> <span class="token class-name">NotificationManager</span><span class="token punctuation">.</span>IMPORTANCE_HIGH<span class="token punctuation">;</span> <span class="token comment">//判断Android版本,不同的Android版本请求不一样,以下代码为官方写法</span> <span class="token keyword">if</span><span class="token punctuation">(</span><span class="token class-name">Build</span><span class="token punctuation">.</span>VERSION<span class="token punctuation">.</span>SDK_INT<span class="token operator">>=</span><span class="token class-name">Build<span class="token punctuation">.</span>VERSION_CODES<span class="token punctuation">.</span>O</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span> <span class="token class-name">NotificationChannel</span> channel <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">NotificationChannel</span><span class="token punctuation">(</span>channelId<span class="token punctuation">,</span>channelName<span class="token punctuation">,</span>importance<span class="token punctuation">)</span><span class="token punctuation">;</span> channel<span class="token punctuation">.</span><span class="token function">setLightColor</span><span class="token punctuation">(</span><span class="token class-name">Color</span><span class="token punctuation">.</span>BLUE<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token class-name">NotificationManager</span> notificationManager <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token class-name">NotificationManager</span><span class="token punctuation">)</span><span class="token function">getSystemService</span><span class="token punctuation">(</span><span class="token class-name">Context</span><span class="token punctuation">.</span>NOTIFICATION_SERVICE<span class="token punctuation">)</span><span class="token punctuation">;</span> notificationManager<span class="token punctuation">.</span><span class="token function">createNotificationChannel</span><span class="token punctuation">(</span>channel<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">//点击通知时可进入的Activity</span> <span class="token class-name">Intent</span> notificationIntent <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Intent</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> <span class="token class-name">MainActivity</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token class-name">PendingIntent</span> pendingIntent <span class="token operator">=</span> <span class="token class-name">PendingIntent</span><span class="token punctuation">.</span><span class="token function">getActivity</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">,</span>notificationIntent<span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//最终创建的通知,以下代码为官方写法</span> <span class="token comment">//注释部分是可扩展的参数,根据自己的功能需求添加</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">NotificationCompat<span class="token punctuation">.</span>Builder</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span>channelId<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">setContentTitle</span><span class="token punctuation">(</span><span class="token string">"前台服务测试"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">setContentText</span><span class="token punctuation">(</span><span class="token string">"点击可以查看更多详情噢~"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">setSmallIcon</span><span class="token punctuation">(</span><span class="token class-name">R</span><span class="token punctuation">.</span>mipmap<span class="token punctuation">.</span>ic_launcher<span class="token punctuation">)</span><span class="token comment">//通知显示的图标</span> <span class="token punctuation">.</span><span class="token function">setContentIntent</span><span class="token punctuation">(</span>pendingIntent<span class="token punctuation">)</span><span class="token comment">//点击通知进入Activity</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
}
同时需要声明前台服务权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
最后通过如下代码启动前台服务:
Intent intent = new Intent(MainActivity.this, ForegroundService.class);
startForegroundService(intent);
效果如下图所示:
显然这种方式创建的前台服务,用户是可感知的(所以才能保持较高的进程优先级……),因为通知栏有通知信息。
那么有没有办法,我创建前台服务,但是通知栏又不显示通知呢?这样子不就能悄无声息地保活了吗……看似这个要求有点过分,但是还真有!Android 系统曾经被曝一个 AMS 中对前台服务的处理中逻辑漏洞,漏洞编号为 CVE-2020-0108(评级为 High),成功利用该漏洞的攻击者可以绕过前台服务的通知显示并持续在后台运行。此处不展开,后面单独成文分析该漏洞。
了解完常见的 APP 自启动手段后,来看看当前 Android 系统和国内手机厂商是如何进行限制的。
针对各类应用企图通过静态注册广播接收器监听 Android 系统广播来实现自启动、保活的乱象,Android 为了避免其带来的高耗电、影响系统性能的影响,开始限制第三方应用程序随意接收系统广播。
从 Android 7.0 (API 级别 24)开始,就对广播做了一些限制:
android.net.conn.CONNECTIVITY_CHANGE
;ACTION_NEW_PICTURE
和 ACTION_NEW_VIDEO
广播。在 Android 8.0 上,Google 又进一步的增强了限制,除了以下隐式广播外,其他所有隐式广播均无法通过在 AndroidManifest.xml 中注册监听。
// Android 8.0 上不限制的隐式广播
/**
开机广播
Intent.ACTION_LOCKED_BOOT_COMPLETED
Intent.ACTION_BOOT_COMPLETED
*/
"保留原因:这些广播只在首次启动时发送一次,并且许多应用都需要接收此广播以便进行作业、闹铃等事项的安排。"
/**
增删用户
Intent.ACTION_USER_INITIALIZE
“android.intent.action.USER_ADDED”
“android.intent.action.USER_REMOVED”
*/
“保留原因:这些广播只有拥有特定系统权限的app才能监听,因此大多数正常应用都无法接收它们。”
/**
时区、ALARM变化
“android.intent.action.TIME_SET”
Intent.ACTION_TIMEZONE_CHANGED
AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED
*/
“保留原因:时钟应用可能需要接收这些广播,以便在时间或时区变化时更新闹铃”
/**
语言区域变化
Intent.ACTION_LOCALE_CHANGED
*/
“保留原因:只在语言区域发生变化时发送,并不频繁。 应用可能需要在语言区域发生变化时更新其数据。”
/**
Usb相关
UsbManager.ACTION_USB_ACCESSORY_ATTACHED
UsbManager.ACTION_USB_ACCESSORY_DETACHED
UsbManager.ACTION_USB_DEVICE_ATTACHED
UsbManager.ACTION_USB_DEVICE_DETACHED
*/
“保留原因:如果应用需要了解这些 USB 相关事件的信息,目前尚未找到能够替代注册广播的可行方案”
/**
蓝牙状态相关
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED
BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED
BluetoothDevice.ACTION_ACL_CONNECTED
BluetoothDevice.ACTION_ACL_DISCONNECTED
*/
“保留原因:应用接收这些蓝牙事件的广播时不太可能会影响用户体验”
/**
Telephony相关
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGED
TelephonyIntents.SECRET_CODE_ACTION
TelephonyManager.ACTION_PHONE_STATE_CHANGED
TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED
TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED
*/
“保留原因:设备制造商 (OEM) 电话应用可能需要接收这些广播”
/**
账号相关
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION
*/
“保留原因:一些应用需要了解登录帐号的变化,以便为新帐号和变化的帐号设置计划操作”
/**
应用数据清除
Intent.ACTION_PACKAGE_DATA_CLEARED
*/
“保留原因:只在用户显式地从 Settings 清除其数据时发送,因此广播接收器不太可能严重影响用户体验”
/**
软件包被移除
Intent.ACTION_PACKAGE_FULLY_REMOVED
*/
“保留原因:一些应用可能需要在另一软件包被移除时更新其存储的数据;对于这些应用,尚未找到能够替代注册此广播的可行方案”
/**
外拨电话
Intent.ACTION_NEW_OUTGOING_CALL
*/
“保留原因:执行操作来响应用户打电话行为的应用需要接收此广播”
/**
当设备所有者被设置、改变或清除时发出
DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED
*/
“保留原因:此广播发送得不是很频繁;一些应用需要接收它,以便知晓设备的安全状态发生了变化”
/**
日历相关
CalendarContract.ACTION_EVENT_REMINDER
*/
“保留原因:由日历provider发送,用于向日历应用发布事件提醒。因为日历provider不清楚日历应用是什么,所以此广播必须是隐式广播。”
/**
安装或移除存储相关广播
Intent.ACTION_MEDIA_MOUNTED
Intent.ACTION_MEDIA_CHECKING
Intent.ACTION_MEDIA_EJECT
Intent.ACTION_MEDIA_UNMOUNTED
Intent.ACTION_MEDIA_UNMOUNTABLE
Intent.ACTION_MEDIA_REMOVED
Intent.ACTION_MEDIA_BAD_REMOVAL
*/
“保留原因:这些广播是作为用户与设备进行物理交互的结果:安装或移除存储卷或当启动初始化时(当可用卷被装载)的一部分发送的,因此它们不是很常见,并且通常是在用户的掌控下”
/**
短信、WAP PUSH相关
Telephony.Sms.Intents.SMS_RECEIVED_ACTION
Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION
注意:需要申请以下权限才可以接收
“android.permission.RECEIVE_SMS”
“android.permission.RECEIVE_WAP_PUSH”
*/
“保留原因:SMS短信应用需要接收这些广播”
以上参考: Android官方开发者文档:隐式广播例外情况 。
但是本人在某国产手机(Android 12)上实测发现,实际上三方应用程序静态注册的广播接收器只能接收到以下两类系统广播:
强调一下,以上广播的接受限制,仅限于静态注册的广播监听器!对于动态注册的广播监听器,由于注册逻辑所在 App 已经处于运行状态,它们是能正常监听到系统广播的,比如监听上文提到的 Android 7.0 起开始限制的网络变化的广播android.net.conn.CONNECTIVITY_CHANGE
。
以某为手机的手机管家 App 提高的的自启动管理功能为例:
管理策略大致如下:
其他国内手机厂商的自启动管理机制基本上同上。
此处特意对比了下 Google 原生手机 Pixel 5( Andrid 12) 的启动管理机制,发现有如下特点:
综上,谷歌对于开机自启动的管控跟国内手机厂商策略基本一致,但是对于应用关联启动的策略,跟国内手机厂商的差距还是比较大的,大概是国外的生态比较好、没有互相拉活的乱象??
进程保活最好的方案那肯定是跟各大系统厂商建立合作关系,把 App 加入系统内存清理的白名单。比如微信,降低 oom_adj 值,尽量保证进程不被系统杀死。那问题又来了:什么是 oom_adj 值?
Android 有一个 OOM 的机制,系统会根据进程的优先级,给每个进程一个 OOM 权重值,当系统内存不足时,系统会根据这个优先级去选择将哪些进程杀掉,以腾出空间保证更高优先级的进程能正常运行。要想让进程长期存活,提高优先级是个不二之选。这个可以在 adb 中,通过以下命令查看:su cat /proc/pid/oom_adj
,这个值越小,说明进程的优先级越高,越不容易被进程kill掉。
数值再增大,则优先级逐渐降低,顺序为服务进程,缓存进程,空进程等等。下表做了 oom_adj 值与进程优先级的概括:
此处可以看下实体手机里面微信 App 打开时、退至后台时的 oom_adj 值各是多少:
可以看到微信退至后台的时候,oom_adj 值也是极低的,对比下微软必应 App,那就很惨了……
Android 系统上自启动、关联启动的泛滥,除了对个人信息保护带来隐患外,还会导致占用过多的系统 CPU 和内存资源,造成系统卡顿、电池耗费过快;还可能引入一些包含“恶意代码”的进程被隐蔽启动,避开了杀毒软件等的查杀,有可能威胁到用户通信秘密、财产安全。
当前国产手机厂商已经意识到这种机制带来的隐患,在其 ROM 系统中增加了对应用行为进行监控和记录的功能,让用户可以查看、或者控制 App 的自启动、关联启动机制。谷歌也做了相应的系统机制来限制应用的自启行为。
但是我最后想说的一点的是,虽然手机厂商提供的自启动管理机制能较好地管控应用程序自启动的乱象,但是由于某些利益关联还是会导致某些厂商给部分 APP 开放自启动白名单权限……用户在必要的情况下,可以手动进行自启权限的管理,来更好地维护自身隐私和权益。
本文参考文章:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。