我们来聊聊目前可用的App保活技术。这些方法在目前看来都还可以用,并且效果也很好。但无法保证长期可用,因为Android操作系统本身可能会更改其策略,而且各厂商在定制Android时也会引入自家节电策略。这些都有可能破坏我们原本可用的保活逻辑,所以当Android系统本身发生策略改变时,还需要去查看官方文档,关注有关影响保活的策略改变,并适配它们。此外,针对各厂商定制的系统,还应该尽可能地多做测试,尽可能地确保App在大部分设备上是正常运行的。
Foreground/Active process | 用户当前操作的进程,包括用户正在交互的Activity,绑定用户正在交互Activity的Service,使用 startForeground 的 Service,正在执行 onReceive 的 BroadcastReceiver 等 |
---|---|
Vsible process | 会影响用户所见内容的进程,如onPause 状态的 Activity 等 |
Service process | 后合服务,如正在运行 startservice 启动的 Service。 |
Background process | 对用户交互无影响,如 onStop 状态的 Activity 等,系统可能随时对其进行终止。 |
Empty process | 一般用作缓存以缩短下次启动时间,系统往往会终止这些空进程。 |
static uint32_t lowmem_debug_level = 2;
static int lowmem_adj[6] = {
0,
1,
6,
12,
};
static int lowmem_adj_size = 4;
static int lowmem_minfree[6] = {
3 * 512, /* 6MB */
2 * 1024, /* 8MB */
4 * 1024, /* 16MB */
16 * 1024, /* 64MB */
};
static int lowmem_minfree_size = 4;
在LMK 中通过进程的oom_adj 与占用内存的大小决定要杀死的进程,oom_adj值越小,越不容易被杀死。其中,lowmem_minfiee 是杀进程的时机,谁被杀,则取决于 lowmem_adj,具体值参考ProcessList 类。
在init.rc中定义了init 进程(系統进程)的oom_adj 为-16,其不可能会被杀死(init 的PID是1),而前台进程是 0。(这里的前台进程是指用户正在使用的 activity所在的进程),例如用户按Home 键回到桌面时的优先级是6,普通的 Service 的进程优先级是 8。
adb shell
ps|grep 进程名
cat /proc/PID/oom_adj //其中PID是上述grep得到的进程号
例如:拨打电话
adb shell
am start -a android.intent.action.CALL -d tel:10086。
App保活的第一种就是添加白名单,让系统的电池优化机制忽略相应的App。
原生Android进行白名单的添加很方便,各品牌厂商的系统由于定制化了原生Android,在添加白名单的方法上略有不同。除了按照原生Android的逻辑进行添加外,还需要对不同品牌的机型做单独的逻辑处理。
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
该权限无须用户批准,直接声明即可。
程序运行时先检查自身是否已经在电池优化白名单中,如果在,就无须任何操作;如果不在,就弹出请求添加到白名单的窗口,这个窗口由Android系统提供。无论用户选择拒绝添加活接受添加,我们都要获取用户的选择结果,以便今后在适当的时机弹出提示。
public class BackActivity extends Activity {
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initData();
}
private void initData(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
if(!isIgnoreBatteryOptimizationStatus(this)){
requireIgnoreBatteryOptimization(this);
}
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
private boolean isIgnoreBatteryOptimizationStatus(Activity context){
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return powerManager != null && powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
}
@RequiresApi(api = Build.VERSION_CODES.M)
private void requireIgnoreBatteryOptimization(Activity context){
try{
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:"+context.getPackageName()));
context.startActivityForResult(intent,0x00);
}catch (Exception e){
e.printStackTrace();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 0x00){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
Log.d("Activity","Ignore Battery Optimization Status is "+isIgnoreBatteryOptimizationStatus(this));
}
}
}
}
我们都知道,Andnoid 操作系统有这样的机制:当内存空间不足、系统资源过低时,系统会按照 App 的优先级清理掉那些重要度不高的程序。如果我们的推送服务恰好在被清理的名单中,那么被清理只是时问问题。可见,在测试人员处能及时发现这个问题是当么宝费。毕竟用户的手机可能会同时运行更多的 App,被清理的可能性会更大,也可能会更加频繁。
那么,如何判断确实是由于内存不足造成App被杀呢?这就要通过获取App运行状态来判断了。
首先,我们要拿到那台可以复现问题的机器,然后到去看看它触发内存清理的时刻。
Android 系统中使用一个配置文件来表示内存阀值,该值根据 App 不同运行状态有多个值。一旦可用内存小于等于这个值,相应运行状态的 App 就会被清理掉。下表是配置了2GB内存的小米手机的内存清理阀值以及对应的App的运行状态。
运行状态 | 內存阈值 |
---|---|
FOREGROUD APP (前台进程) | 14746(大约 57MB) |
VISIBLE APP(可见进程) | 18432 (72MB) |
SECONDARY SERVER(次要服务) | 22118 (大约86MB) |
HIDDEN APP(后台进程) | 25805(大约 100MB) |
CONTENT PROVIDER(内容提供者) | 40000(大约156MB) |
EMPTY APP(空进程) | 55000 (44 215MB) |
很明显,在这合设备上,一旦可用内存小于或等于 72MB,只要程序不在前台运行,将会被清理掉。
这里要特别说明前台进程和可见进程的区别。一般来讲,它们本质的不同是当前是否正在处于与用户交互的状态。比如,你现在正在和一个名为A的 App 进行交互,突然名为B的App弹出了一个对话框,此时A 由前合进程转为可见进程(此时 A 的界面被 B的对话框挡住,虽然部分可见,但用户无法直接和 A 交互),B变为前合进程。
注意,即使是相同生产厂商,内存阙值也会由于设备本身搭载内存大小的不同而有所变化。所以,笔者建议在做保活测试时,最好选择一款内存小的机器,并同时运行多个市场上常见的 App,让问题尽快暴露。
只知道系统清理内存的原则还不够,还需要了解你开发的App占用内存以及各进程优先级的情况,这样才能确定问题是否确实是由于内存不足造成的。
要获取App的内存占用量,一种方法是利用Android Profiler,只要App处于Debug模式,就可以使用;另一种方法是通过adb命令,该方法适用型更广泛,不要求App必须对于Debug模式,而且能看到整个手机的内存占用情况。
adb shell dumpsys meminfo
接着,获取该App的运行优先级(需要取得Root权限)
cat /proc/进程ID/oom_adj
OOM_ADJ值 | 含义 |
---|---|
-16 | 一般指system进程 |
-11 | 框架进程或常驻App,即AndroidManifest中persistent值为true的app |
0 | 前台App |
1 | 可见应用或有persistent进程关联的Service或Provider |
2 | 可感知进程,通常指startService且调用了startForeground的服务 |
5/8 | startService启动的服务,但没有Activity在运行 |
6 | Home Launcher |
9-16 | 缓存进程,通常是切回Home的App、Empty进程等 |
看到这,你或许明白了那些网上所谓的通告提高优先级或保持前台的方式来保活App的原理了。实际上,它们就是利用OOM_ADJ值的不同,努力多争取进程不被清理掉的可能。
此外,对于没有Root权限的设备,还可以通过执行:
adb shell dumpsys meminfo
通过名称来获取进程优先级
系统总共的可用内存量可以通过以下命令查看:
adb shell cat /proc/menifo
现在,有了系统可用内存、App所属进程优先级以及App内存占用量的信息,足够我们做出判断了。
如何保证推送服务在设备上稳定运行呢?
单方面提升进程优先级,或直接长时间播放无声音频。。。如果每个App都如此暴力,最终的结果就是手机整体运行体验不佳。
class CounterService extends Service{
@Override
public void onCreate() {
super.onCreate();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
接着,在AndroidManifest文档中注册该Service
<service
android:name = ".CounterService"
android:process = ":counter"/>
此外,如果读者想要进一步增强进程的存活能力,还可以监听某些系统广播,实现更快地重启进程。当然,这要求用户己经将 App 添加到白名单作为前提。
另一方面,由于用户将我们的 App 添加到白名单中,这就等于充分相信 App 不会在手机上胡作非为,包括系统资源的滥用,这是作为开发者特别要警戒的一点。
2. 集成厂商推送
另一种方式是集成各厂商的推送服务。别急,这里并不是要求开发者去各厂商分别下载推送 SDK,分别阅读开发文档,这样太费事了。
笔者建议使用极光推送,截至作者撰稿时,该平台已经集合了华为、小米、魅族、Vivo 和Oppo 五家手机厂商的推送通道,此外,还支持 Google 的Firebase 以及自建推送通道。
有了这几家厂商的推送通道,实际上就覆盖了市场上的大部分用户。虽然通过极光使用各厂商自己的推送通道可以达到一次集成的目的,但各厂商的 AppID、AppKey 等参数还需要分别进行注册才行。但是,接入这种第三方推送有一个弊端,会被流量劫持。
如果想要进一步了解进程保活相关的技术,可以参考下面这篇博客:《一种提高Android应用进程存活率新方法 》