再谈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 ,这样通知栏图标即被移除。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏微服务生态

论代码级性能优化变迁之路(一)

大家好,很久没有和大家一起讨论技术了,那么今天我将和大家一起探讨我负责的某项目的性能变迁之路。

822
来自专栏自由而无用的灵魂的碎碎念

实战解决使用ghost安装系统出现的各种问题

昨天使用ghost给人安装系统时,把另一个分区的数据都搞没了,安装完也只剩下一个分区,相信了解的同志们知道是什么原因。

1413
来自专栏解Bug之路

解Bug之路-记一次JVM堆外内存泄露Bug的查找 顶

JVM的堆外内存泄露的定位一直是个比较棘手的问题。此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤了Bug的源头。笔...

984
来自专栏雨过天晴

转 树莓派无显示器安装系统

1672
来自专栏安恒信息

国内黑客论坛已出现自动攻击已知Struts漏洞的工具

中国黑客现在正是用一个自动工具利用Apache Struts中的已知漏洞,目的是在用这个框架开发的用于托管应用的服务器上安装后门。 Ap...

4225
来自专栏SAP最佳业务实践

关于S4信贷错误的系统解决方案

https://wiki.scn.sap.com/wiki/display/ABAPConn/Technical+Setup+and+Reset+of+the+...

3984
来自专栏Android's Track

当我们按下电源键,Android 究竟做了些什么?

我们会否好奇过,如此复杂的 Android 究竟是怎么运作起来的呢?

1.3K14
来自专栏听雨堂

Apache+wsgi+flask部署

flask自带的web server是开发用途,并不适用与发布,需要借助专业的web服务器。 配置的坑无数,Apache部署,403禁止,莫名其妙无法访问,50...

3829
来自专栏FreeBuf

聊一聊万恶的锁首

当手持8倍镜的98K都不能在使用程序时干掉万恶的锁首时,内心是十万头羊驼奔跑的场景,那我们就来聊一聊市面上常见的锁首方式。 ① :设置OpenHomePage、...

2187
来自专栏程序员的碎碎念

如何解决Python包依赖问题

以简洁高效(指编程较为高效, 而不是运行速度)出名的Python, 在包依赖问题上有时候让人挠头.

1842

扫码关注云+社区

领取腾讯云代金券