再谈Android客户端进程保活

在很多移动应用中,特别是即时通信类项目中,保活是一个永远无法避免的一个话题。保活,按照我的理解,主要包含两部分:
网络连接保活:如何保证消息接收实时性。
进程保活:尽量保证应用的进程不被Android系统回收。
在很早以前,谈Android的保活都会涉及到进程常驻内存,如何进行性能优化等话题,今天就这些话题,做一个简单的总结。

Android进程

在讨论这个问题之前,我们首先来看一些现象级APP的进程。

搞Android的同学都知道,每一个Android应用启动后至少对应一个进程,有的则有多个进程,大多数主流APP都会包含多个进程,因为除了主要的进程之外,还有诸如长连接、推送等进程。

查看进程

对于任何一个进程,我们都可以通过adb shell ps|grep 的方式来查看。具体方式如下:

上图的具体含义如下:

解释
u0_a16 USER 进程当前用户
3881 进程ID
873024 进程的虚拟内存大小
37108 实际驻留”在内存中”的内存大小

进程划分

Android系统按重要性从高到低把进程的划为了如下几种(严格来说是6种)。

1,前台进程

此种进程指用户正在使用的程序,一般系统是不会杀死前台进程的,除非用户强制停止应用或者系统内存不足等极端情况会杀死。

主要场景

  • 某个进程持有一个正在与用户交互的Activity,并且该Activity正处于resume的状态。
  • 某个进程持有一个Service,并且该Service与用户正在交互的Activity绑定。
  • 某个进程持有一个Service,并且该Service调用startForeground()方法使之位于前台运行。
  • 某个进程持有一个Service,并且该Service正在执行它的某个生命周期回调方法,比如onCreate()、onStart()或onDestroy()。
  • 某个进程持有一个BroadcastReceiver,并且BroadcastReceiver正在执行其onReceive()方法。

2,可见进程

用户正在使用,看得到,但是摸不着,没有覆盖到整个屏幕,只有屏幕的一部分可见进程不包含任何前台组件,一般系统也是不会杀死可见进程的,除非要在资源吃紧的情况下,要保持某个或多个前台进程存活。

主要场景:

  • 拥有不在前台、但仍对用户可见的 Activity(已调用onPause())。
  • 拥有绑定到可见(或前台)Activity 的 Service。

3,服务进程

在内存不足以维持所有前台进程和可见进程同时运行的情况下,服务进程会被杀死。
主要场景:

  • 某个进程中运行着一个Service且该Service是通过startService()启动的,与用户看见的界面没有直接关联。

4,后台进程

后台进程,系统可能随时终止它们,用以回收内存。
主要场景:

  • 在用户按了"back"或者"home"后,程序本身看不到了,但是其实还在运行的程序,比如Activity调用了onPause方法。

空进程

某个进程不包含任何活跃的组件时该进程就会被置为空进程,完全没用,杀了它只有好处没坏处,第一个干它。

内存阈值

上面主要讲的是进程,那么进程是怎么被杀的呢?这不得不提主要的一个原因:内存。在移动设备中内存往往是有限的,打开的应用越多,后台缓存的进程也越多。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程。在Android的内存回收机制中有一个重要的概念:Low Memory Killer。
我们可以使用cat /sys/module/lowmemorykiller/parameters/minfree来查看某个手机的内存阈值。

注意这些数字的单位是page(1 page = 4 kb)。上面的六个数字对应的就是(MB): 72,90,108,126,144,180,这些数字也就是对应的内存阀值,内存阈值在不同的手机上不一样,一旦低于该值,Android便开始按顺序关闭进程. 因此Android开始结束优先级最低的空进程,即当可用内存小于180MB(46080*4/1024)。

读到这里,你或许有一个疑问,假设现在内存不足,空进程都被杀光了,现在要杀后台进程,但是手机中后台进程很多,难道要一次性全部都清理掉?当然不是的,进程是有它的优先级的,这个优先级通过进程的adj值来反映,它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收,adj值定义在com.android.server.am.ProcessList类中,这个类路径是${android-sdk-path}\sources\android-23\com\android\server\am\ProcessList.java。oom_adj的值越小,进程的优先级越高,普通进程oom_adj值是大于等于0的,而系统进程oom_adj的值是小于0的,我们可以通过cat /proc/进程id/oom_adj可以看到当前进程的adj值。

看到adj值是0,0就代表这个进程是属于前台进程,我们再按下Back键,将应用至于后台,再次查看。

adj值变成了8,8代表这个进程是属于不活跃的进程。关于oom_adj进程的相关内容可以参考下表:

adj级别 解释
UNKNOWN_ADJ 16 预留的最低级别,一般对于缓存的进程才有可能设置成这个级别
CACHED_APP_MAX_ADJ 15 缓存进程,空进程,在内存不足的情况下就会优先被kill
CACHED_APP_MIN_ADJ 9 缓存进程,也就是空进程
SERVICE_B_ADJ 8 不活跃的进程
PREVIOUS_APP_ADJ 7 切换进程
HOME_APP_ADJ 6 与Home交互的进程
SERVICE_ADJ 5 有Service的进程
HEAVY_WEIGHT_APP_ADJ 4 高权重进程
BACKUP_APP_ADJ 3 正在备份的进程
PERCEPTIBLE_APP_ADJ 2 可感知的进程,比如那种播放音乐
VISIBLE_APP_ADJ 1 可见进程,如当前的Activity
FOREGROUND_APP_ADJ 0 前台进程
PERSISTENT_SERVICE_ADJ -11 重要进程
PERSISTENT_PROC_ADJ -12 核心进程
SYSTEM_ADJ -16 系统进程
NATIVE_ADJ -17 系统起的Native进程

说明:上表的数字可能在不同系统会有一定的出入。

下面按照网络保活和进程保活来给大家介绍保活的一些策略。

网络连接保活

网络保活,业界主要手段有:
a. GCM;
b. 公共的第三方push通道(信鸽等);
c. 自身跟服务器通过轮询,或者长连接;

GCM即Google Cloud Messaging,主要用于消息推送的,即使在应用没有起来的情况下,客户端也能通过GCM收到来自服务器的消息。GCM支持Android、IOS和Chrome。由于GCM需要google service支持,在国内基本不能用,经常会断线。

push很多也是基于长连接实现的,早年的微信,直接通过Java socket 实现。所以后面我们直接谈长连接。
长连接实现包括几个要素:
a. 网络切换或者初始化时 server ip 的获取。
b. 连接前的 ip筛选,出错后ip 的抛弃。
c. 维护长连接的心跳。
d. 服务器通过长连notify。
e. 选择使用长连通道的业务。
f. 断开后重连的策略。

今天,我们讨论重点即时聊天中的心跳和 notify 机制。

1,心跳机制

通过定期的数据包,对抗NAT超时(一般会设置为5-10秒)。以下是部分地区网络NAT 超时统计。

心跳的实现过程如下:

说明:
a. 连接后主动到服务器Sync拉取一次数据,确保连接过程的新消息。
b. 心跳周期的Alarm 唤醒后,一般有几秒的cpu 时间,无需wakelock。
c. 心跳后的Alarm防止发送超时,如服务器正常回包,该Alarm 取消。
d. 如果服务器回包,系统通过网络唤醒,无需wakelock。

流程基于两个系统特性:

a. Alarm唤醒后,足够cpu时间发包。
b. 网络回包可唤醒机器。

特别是b项,假如Android封堵该特性,那就只能用GCM了。API level >= 23的doze就关闭所有的网络, alarm等。Google也最终在6.0版本加入REQUEST_IGNORE_BATTERY_OPTIMIZATIONS权限。

2,动态心跳

4.5min心跳周期是稳定可靠的,但无法确定是最大值。通过终端的尝试,可以获取到特定用户网络下,心跳的最大值。引入该特性的背景:
a. 运营商的信令风暴
b. 运营商网络换代,NAT超时趋于增大
c. Alarm耗电,心跳耗流量。

动态心跳引入下列状态:
a. 前台活跃态:亮屏,微信在前台, 周期minHeart (4.5min) ,保证体验。
b. 后台活跃态:微信在后台10分钟内,周期minHeart ,保证体验。
c. 自适应计算态:步增心跳,尝试获取最大心跳周期(sucHeart)。
d. 后台稳定态:通过最大周期,保持稳定心跳。

下面是自适应计算态流程:

在自适应态:
a. curHeart初始值为minHeart , 步增(heartStep)为1分钟。
b. curHeart 失败5次, 意味着整个自适应态最多只有5分钟无法接收消息。
c. 结束后,如果sucHeart > minHeart,会减去10s(避开临界),为该网络下的稳定周期。
d. 进入稳定态时,要求连接连续三次成功minHeart心跳周期,再使用sucHeart。

3,notify机制

网络保活的意义在于消息实时。通过长连接,即时通信类产品有下列机制保证消息的实时。

Sync

通过Sync CGI直接请求后台数据。Sync 通过后台和终端的seq值对比,判断该下发哪些消息。终端正常处理消息后,seq更新为最新值。
Sync 的主要场景:
a. 长连无法建立时,通过Sync 定期轮询;
b. 微信切到前台时,触发Sync(保命机制);
c. 长连建立完成,立即触发Sync,防止连接过程漏消息;
d. 接收到Notify 或者 gcm 后,终端触发Sync 接收消息。

Notify:

类似于GCM。通过长连接,后台发出仅带seq的小包,终端根据seq决定是否触发Sync拉取消息。

NotifyData:

在长连稳定, Notify机制正常的情况下(保证seq的同步)。后台直接推送消息内容,节省1个RTT (Sync) 消息接收时间。终端收到内容后,带上seq回应NotifyAck,确认成功。这里会出现Notify和NotifyData状态互相切换的情况:

如NotifyData 后,服务器在没收到NotifyAck,而有新消息的情况下,会切换回到Notify,Sync可能需要冗余之前NotifyData的消息。终端要保证串行处理NotifyData和Sync ,否则seq可能回退。

GCM:

只要机器上有GMS ,启动时就尝试注册GCM,并通知后台。服务器会根据终端是否保持长连,决定是否由GCM通知。GCM主要针对国外比较复杂的网络环境。

进程保活

在Android系统里,进程被杀的原因通常为以下几个方面:
a. 应用Crash;
b. 系统回收内存;
c. 用户触发;
d. 第三方root权限app。

下面分享几个微信和qq关于进程保活的几个方法:

1,进程拆分

俗话说,鸡蛋不能放一个篮子里面,那么为了保活,我们也可以将进程拆分为几个。

例如,上图是微信应用的几个进程:
a. push主要用于网络交互,没有UI
b. worker就是用户看到的主要UI
c. tools主要包含gallery和webview
这样,进程通过拆分之后,单个进程被回收了并不影响其他的进程。拆分网络进程,确实就是为了减少进程回收带来的网络断开。

可以看到push的内存要远远小于worker。而且push的工作性质稳定,内存增长会非常少。这样就可以保证,尽量的减少push 被杀的可能。为了提高线程存活的概率,这里启动一个纯C/C++ 的进程,而不是Java run time。

2,及时拉起

系统回收不可避免,及时重新拉起的手段主要依赖系统特性。从上图看到, push有AlarmReceiver, ConnectReceiver,BootReceiver。这些receiver 都可以在push被杀后,重新拉起。特别AlarmReceiver ,结合心跳逻辑,微信被杀后,重新拉起最多一个心跳周期。

而对于worker,除了用户UI操作启动。在接收消息,或者网络切换等事件, push也会通过LocalBroadcast,重新拉起worker。这种拉起的worker ,大部分初始化已经完成,也能大大提高用户点击微信的启动速度。

历史原因,我们在push和worker通信使用Broadcast和AIDL。实际上,我一直不喜欢这里的实现,AIDL代码冗余多, broadcast效率低。欢迎大家分享更好的思路或者方法。

3,进程优先级

前面说过Low Memory Killer机制,Low Memory Killer 机制决定是否杀进程除了内存大小,还有进程优先级。这个前面也说过。从这个原理来说,我们可以通过提高进程的优先级来保活。

值得注意的是,Android 的前台service机制。但该机制的缺陷是通知栏保留了图标。

对于 API level < 18 :调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。

对于 API level >= 18:在需要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉InnerService ,这样通知栏图标即被移除。

时间: 2024-09-16 17:12:15

再谈Android客户端进程保活的相关文章

再谈Android的许可证

两周前,我写了一篇<Android,开源还是封闭?>.其中有一些内容,我今天要做修正,还想谈一些别的感想. 在谈具体的修正之前,我先来说说,那篇文章的一些情况. 那天白天,我在外面办事,从手机上读到Linux内核撤下所有Android代码的消息,感到很震惊.晚上回家后,仔细读完了相关报道,就一口气写了一些感想.写完已经将近半夜12点.我改了几个错别字,直接把文章贴上网,然后就上床睡觉了.当时也没多想,不觉得它和我的其他文章有何不同. 但是,第二天起床以后,我发现事情变得复杂了.那篇文章被转贴到

再谈Android应用瘦身

Android应用apk安装包的大小,虽然对于现在WiFi普及情况而言消耗流量已经微乎其微,但是,对于一款好的应用,对于一款负责任的应用,当然是越小越好了.   引言: .应用越小,下载越快,也就意味着新用户能在最短时间内安装,体验应用,而不是看着通知栏里面的丑陋的下载进度条,盯着看几分钟(30-50M的应用很常见,网不好,下载几分钟很正常)就像这样...   . 随着应用的迭代,应用必须满足人们越来越高的体验需求,应用需要更多的代码,更多的第三方库,更多的资源文件,随着设备的分辨率越来越高,资

[译]再谈如何安全地在 Android 中存储令牌

本文讲的是[译]再谈如何安全地在 Android 中存储令牌, 原文地址:A follow-up on how to store tokens securely in Android 原文作者:Enrique López Mañas 译文出自:掘金翻译计划 译者: lovexiaov 校对者:luoqiuyu hackerkevin 作为本文的序言,我想对读者做一个简短的声明.下面的引言对本文的后续内容而言十分重要. 没有绝对的安全.所谓的安全是指利用一系列措施的堆积和组合,来试图延缓必然发生的

详解Android进程保活的方法

关于 Android 平台的进程保活这一块,想必是所有 Android 开发者瞩目的内容之一.你到网上搜 Android 进程保活,可以搜出各种各样神乎其技的做法,绝大多数都是极其不靠谱.前段时间,Github还出现了一个很火的"黑科技"进程保活库,声称可以做到进程永生不死. 怀着学习和膜拜的心情进去Github围观,结果发现很多人提了 Issue 说各种各样的机子无法成功保活. 看到这里,我瞬间就放心了.坦白的讲,我是真心不希望有这种黑科技存在的,它只会滋生更多的流氓应用,拖垮我大

利用android客户端支付宝sdk的jar包和demo的几个处理支付的类再做一个jar包

问题描述 利用android客户端支付宝sdk的jar包和demo的几个处理支付的类再做一个jar包 利用android客户端支付宝sdk的jar包和demo的几个处理支付的类(不包含activity)再做一个jar包,其它项目中只要导入该包和传入几个参数就可以使用支付宝支付功能 解决方案 将项目导出作为一个jar供他人引用不就好了,或者使用webservice

一种Android客户端架构设计分享

前言:技术发展日新月异,业界各种Android客户端架构设计,五花八门,但我们不能简单地说哪种架构更好,因为脱离业务谈架构是没有任何意义的,适合业务的才是好架构.而架构也不是一成不变的,随着业务的发展,也许当初设计的架构已不足以支撑目前的业务,那么就需要改变之前的架构.接下来将分享下我们Android客户端的架构设计,在App的某个业务发展阶段或许有一些参考意义. 分层化与模块化 分层化与模块化应该是任何软件开发的共识. 分层化 在Android应用开发中通常可以分为如下几层:  SDK层:主要

浅谈Android Activity与Service的交互方式_Android

实现更新下载进度的功能 1. 通过广播交互 Server端将目前的下载进度,通过广播的方式发送出来,Client端注册此广播的监听器,当获取到该广播后,将广播中当前的下载进度解析出来并更新到界面上. 优缺点分析: 通过广播的方式实现Activity与Service的交互操作简单且容易实现,可以胜任简单级的应用.但缺点也十分明显,发送广播受到系统制约.系统会优先发送系统级广播,在某些特定的情况下,我们自定义的广播可能会延迟.同时在广播接收器中不能处理长耗时操作,否则系统会出现ANR即应用程序无响应

浅谈Android Activity与Service的交互方式

实现更新下载进度的功能 1. 通过广播交互 Server端将目前的下载进度,通过广播的方式发送出来,Client端注册此广播的监听器,当获取到该广播后,将广播中当前的下载进度解析出来并更新到界面上. 优缺点分析: 通过广播的方式实现Activity与Service的交互操作简单且容易实现,可以胜任简单级的应用.但缺点也十分明显,发送广播受到系统制约.系统会优先发送系统级广播,在某些特定的情况下,我们自定义的广播可能会延迟.同时在广播接收器中不能处理长耗时操作,否则系统会出现ANR即应用程序无响应

Android-进程和线程

当一个应用(application)组件首次启动时,Android系统会为这个应用启动一个新的进程(process),并为之开启一个单独的线程(thread):如果一个应用组件启动时它所再的应用已经创建了一个进程,则它直接运行在这个进程中.默认情况下,一个应用所以的组件(Activity/Service/BroadCastReceiver/ContentProvider)都运行在一个同一个进程的同一个线程中(main thread).当然,也可以指定你的应用组件运行在单独的进程中,你也可以在你的