进程
当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。
各类组件元素的清单文件条目<activity>
、<service>
、<receiver>
和 <provider>
—均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。
5个常用进程
优先级:前台进程>可见进程>服务进程>后台进程>空进程(Empty Process)
前台进程
杀死前台进程需要用户交互。
- 进程持有一个正与用户交互的Activity
- 进程持有一个Service,这个Service处于这几种状态:
- Service与用户正在交互的Activity绑定
- Service调用了
startForeground()
,在前台运行 - Service执行在用户正在交互的Activity的生命周期回调函数中(
onCreate()、onStart()、onDestory()
)
- 进程持有一个BroadcastReceiver,这个BroadcastReceiver正在执行它的
onReceive()
方法
可见进程
进程不含任何前台的组件,仍可被用户在屏幕上所见。
- 进程持有一个Activity,此Activity不在前台,但仍可被用户可见(处于
onPause()
但没调用onStop()
状态,如:前台Activity打开了一个对话框) - 进程持有一个Service,此Service和一个可见(或者前台的)Activity绑定
服务进程
一个进程中运行着一个Service,此Service是通过startService()
开启的,如后台播放音乐、后台下载等。
后台进程
通常有很后台进程存在,它们被保存在LRU(least recently used)列表中,即保证了最近使用得activity最后被销毁。
- 进程持有一个用户不可见的Activity(activity的
onStop()
被调用),即可认为此进程是后台进程。后台进程不直接影响用户体验,系统会为了优先级高的进程而任意杀死后台进程。
空进程Empty process
一个进程中没有数据在运行了,但内存为此应用驻留了一个进程空间。保存这种进程的唯一理由是为了缓存的需要,为了加快下次启动这个进程中组件的启动时间。系统为了平衡进程缓存和底层内核缓存的资源,经常会杀死空进程。
- 一个进程不包含任何活跃的应用组件,则认为是空进程(android设计的,为了下次启动更快而采取的一个权衡)
进程生命周期
1、前台进程
- 托管用户正在交互的 Activity(已调用 Activity 的
onResume()
方法) - 托管某个 Service,后者绑定到用户正在交互的 Activity
- 托管正在“前台”运行的 Service(服务已调用
startForeground()
) - 托管正执行一个生命周期回调的 Service(
onCreate()
、onStart()
或onDestroy()
) - 托管正执行其
onReceive()
方法的 BroadcastReceiver
2、可见进程
- 托管不在前台、但仍对用户可见的 Activity(已调用其
onPause()
方法)。例如,如果 re前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。 - 托管绑定到可见(或前台)Activity 的 Service
3、服务进程
- 正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。
4、后台进程
- 包含目前对用户不可见的 Activity 的进程(已调用 Activity 的
onStop()
方法)。通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。
5、空进程
- 不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
在Service中新开线程比在Activity中新开线程的区别
在Service中新开线程至少有服务进程的优先级;
在Activity中新开线程,当Activity退出到桌面或其他情况时,会成为后台进程;
综上:若要处理长时间操作的话,新建Service再开启线程比在Activity中直接开线程好,可以保证这个长时间操作的稳定。
多进程
如果注册的四大组件中的任意一个组件时用到了多进程,运行该组件时,都会创建一个新的 Application 对象。对于多进程重复创建 Application 这种情况,只需要在该类中对当前进程加以判断即可。
单应用多进程:给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidManifest中指定android:process
属性。MainActivity运行在默认进程(默认进程名是包名)中。
andorid:process
内容以“:”开头属于当前应用的私有进程,其他应用组件不可以和它跑在同一个进程中;内容是完整包名的是全局进程,其他应用可通过ShareUID(需要ShareUID、签名相同)和它跑在同一个进程中。每个应用都有个唯一的UID,有相同UID的应用才能共享数据。
1 | public class MyApplication extends Application { |
多进程导致的问题
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharedPreferences的可靠性下降
- Application会多次创建
上面问题1、2:每个进程都会分配一个独立的虚拟机,在内存分配上有不同的地址空间。导致不同虚拟机访问同一个类的对象会产生多份副本。((对对象、全局类)加锁是没有用的(不同进程锁的不是同一个对象了))
上面问题3:因为SharedPreferences(底层通过读/写XML文件来实现的)不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失。甚至并发读/写都有可能出问题。
上面问题4:新进程分配新的虚拟机,此过程其实就是启动一个应用的过程。相当于系统又把这个应用重新启动了一遍,会创建新的Application。
进程存活
OOM_ADJ
ADJ级别 | 取值 | 解释 |
---|---|---|
UNKNOWN_ADJ | 16 | 一般指将要会缓存进程,无法获取确定值 |
CACHED_APP_MAX_ADJ | 15 | 不可见进程的adj最大值 |
CACHED_APP_MIN_ADJ | 9 | 不可见进程的adj最小值 |
SERVICE_B_AD | 8 | B List中的Service(较老的、使用可能性更小) |
PREVIOUS_APP_ADJ | 7 | 上一个App的进程(往往通过按返回键) |
HOME_APP_ADJ | 6 | Home进程 |
SERVICE_ADJ | 5 | 服务进程(Service process) |
HEAVY_WEIGHT_APP_ADJ | 4 | 后台的重量级进程,system/rootdir/init.rc文件中设置 |
BACKUP_APP_ADJ | 3 | 备份进程 |
PERCEPTIBLE_APP_ADJ | 2 | 可感知进程,比如后台音乐播放 |
VISIBLE_APP_ADJ | 1 | 可见进程(Visible process) |
FOREGROUND_APP_ADJ | 0 | 前台进程(Foreground process) |
PERSISTENT_SERVICE_ADJ | -11 | 关联着系统或persistent进程 |
PERSISTENT_PROC_ADJ | -12 | 系统persistent进程,比如telephony |
SYSTEM_ADJ | -16 | 系统进程 |
NATIVE_ADJ | -17 | native进程(不被系统管理) |
进程被杀情况
进程杀死场景 | 调用接口 | 可能影响范围 |
---|---|---|
触发系统进程管理机制 | Lowmemorykiller | 从进程importace值由大到小依次杀死,释放内存 |
被第三方应用杀死(无Root) | killBackgroundProcess | 只能杀死OOM_ADJ为4以上的进程 |
被第三方应用杀死(有Root) | force-stop或者kill | 理论上可以杀所有进程,一般只杀非系统关键进程和非前台和可见进程 |
厂商杀进程功能 | force-stop或者kill | 理论上可以杀所有进程,包括Native进程 |
用户主动“强行停止”进程 | force-stop | 只能停用第三方和非system/phone进程应用(停用system进程应用会造成Android重启) |
进程保活方案
- 开启一个像素的Activity
- 使用前台服务
- 多进程相互唤醒
- JobSheduler唤醒
- 粘性服务&与系统服务捆绑
保证Service不死的方案
设置优先级
AndroidManifest.xml
中intent-filter
标签可加android:priority="1000"
属性,设置优先级,1000时最高值startForeground()
在
onStartCommand()
里调用startForeground()
将Service提升为前台进程。在onDestory
里记得调用stopForeground()
onStartCommand()
,返回START_STICKY
在进程不被干掉的情况下:在
onStartCommand()
中手动返回START_STICKY
,当Service因内存不足被kill,当内存又存在时,Service又被重新创建1
2
3
4public int onStartCommand(Intent intent, int flags, int startId){
flags = START_STICKY;
return super.onStartCommand(intent, flags, startId);
}onStartCommand返回int整型,有4个取值:
- START_STICKY:“粘性的”。若service进程被kill,保留service状态为开始状态,但不保留递送的intent对象。随后系统尝试重建service,因服务为开始状态,故创建服务后一定会调用
onStartCommand(Intent, int, int)
,若在此期间没有任何启动命令传递到service,则参数Intent将为null - START_NOT_STICKY:“非粘性的”。在执行玩
onStartCommand(Intent, int, int)
后,服务被异常kill掉,系统不会自动重启该服务。 - START_REDELIVER_INTENT:重传Intent。执行完
onStartCommand
后,服务被异常kill掉,系统不会自动重启该服务。 - START_STICKY_COMPATIBILITY:START_STICKY的兼容版本。但不保证服务被kill后一定能重启。
- START_STICKY:“粘性的”。若service进程被kill,保留service状态为开始状态,但不保留递送的intent对象。随后系统尝试重建service,因服务为开始状态,故创建服务后一定会调用
在onDestroy中发广播重启Service
service走onDestory时,发个自定义广播,当收到广播时重启service。(第三方应用或是在setting里–应用–强制停止时,APP进程直接被干掉了,onDestory方法进不来,故无法保证会执行)
监听(系统)广播判断Service状态
大厂的做法。监听系统、QQ、微信、系统应用、友盟推送等发送游戏额广播,来唤醒自己的App
Application加上Persistent属性
某个进程不想被杀死(数据缓存进程、状态监控进程、远程服务进程)
1
2//加上以上属性相当于将该进程设置为常驻内存进程
add android:persistent="true" into the section in your AndroidManifest.xml这个不可滥用,一般只适用于放在
/system/app
下的app。这里app一多容易造成系统崩溃。类似监听锁屏的方法
1
2
3
4
5
6
7
8
9
10
11
12QQ采取在锁屏的时候启动一个1个像素的Activity,当用户解锁以后将这个Activity结束掉(顺便同时把自己的核心服务再开启一次)。
故事:小米撕逼。
背景:当手机锁屏的时候什么都干死了,为了省电。
锁屏界面在上面盖住了。
监听锁屏广播,锁了---启动这个Activity。
监听锁屏的,开启---结束掉这个Activity。
要监听锁屏的广播---动态注册。
ScreenListener.begin(new xxxListener
onScreenOff()
);
被系统无法杀死的进程。双进程守护+锁屏
双进程守护+JobScheduler调度
其他:App运营商与手机厂商合作列入白名单;利用账号同步机制唤醒我们的进程;NDK来解决,Native进程来实现双进程守护。
进程保活神器
进程间通信
- bind机制(IPC –> AIDL)
- Linux级共享内存
- Broadcast
- ContentProvider
IPC
IPC 即 Inter-Process Communication (进程间通信)。Android 基于 Linux,而 Linux 出于安全考虑,不同进程间不能之间操作对方的数据,这叫做“进程隔离”。
在 Linux 系统中,虚拟内存机制为每个进程分配了线性连续的内存空间,操作系统将这种虚拟内存空间映射到物理内存空间,每个进程有自己的虚拟内存空间,进而不能操作其他进程的内存空间,只有操作系统才有权限操作物理内存空间。 进程隔离保证了每个进程的内存安全。
基础概念介绍
Serializable
Java提供的一个序列化的空接口。实体类实现Serializable即可【再声明一个serialVersionUID】(需要大量I/O操作,开销大)
1 | //序列化过程 |
应该手动指定serialVersionUID(指定为1L也可以,AS会根据当前类结构去生成hash值),反序列化时会检测是否与当前类的serialVersionUID一致,如果发生过某些变化(比如成员变量的数量、类型改变)就无法正常反序列化会报java.io.InvalidClassException:Main;local class incompatible:stream classdesc serialVersionUID=87113688010083044,local class serialVersionUID=8711368828010083043.
Parcelable
Android提供的新的序列化方式。一个类对象只要实现这个接口,就可以实现序列化并可以通过Intent和Binder传递。(效率更高,写法复杂)
1 | public class User implements Parcelable{ |
Parcelable的方法说明
方法 | 功能 | 标记位 |
---|---|---|
createFromParcel(Parcel in) | 从序列化后的对象中创建原始对象 | |
newArray(int size) | 创建指定长度的原始对象数组 | |
User(Parcel in) | 从序列化后的对象中创建原始对象 | |
writeToParcel(Parcel out, int flags) | 将当前对象写入序列化结构中,其中flags标识有两种值:0或1(参见右侧标记位)。 为1时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0 |
PARCEL_ABLE_WRITE_RETURN_VALUE |
describeContents | 返回当前对象地内容描述。如果含有文件描述符,返回1(参见右侧标记位),否则返回0,几乎所有情况都返回0 | CONTENTS_FILE_DESCRIPTOR |
系统已提供许多实现了Parcelable接口的类,Intent、Bundle、Bitmap等,同时List和Map也可以序列化,前提是其内每个元素都是可序列化的。
Binder
Binder是Android中的一个类,实现了IBinder接口。是Android中的一种跨进程通信方式。可理解为一种虚拟的物理设备,其设备驱动是/dev/binder,该通信方式在Linux中没有;从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager等等)和相应ManagerService的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
Binder主要用于Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及进程间通信,较为简单。Messenger底层是AIDL。
IPC方式
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件间的进程间通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间即时通信 | 无并发访问情形,交换简单的数据实时性不高的场景 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信 | 使用稍复杂,需要处理好线程同步 | 一对多通信且有RPC需求 |
Messenger | 功能一般,支持一对多串行通信,支持实时通信 | 不能处理高并发线程,不支持RPC,数据通过Message进行传输,因此只能传输Bundle支持的数据类型 | 低并发的一对多即时通信,无RPC需求,或者无需返回结果的RPC需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过Call方法扩展其他操作 | 可以理解为受约束的AIDL,主要提供数据源的CRUD(Create、Retrieve、Update、Delete)操作 | 一对多的进程间数据共享 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍微有点烦琐,不支持直接的RPC | 网络数据交换 |
AIDL
Android Interface Definition Language
- 新建AIDL接口文件
1
2
3
4
5
6
7
8// RemoteService.aidl
package com.example.mystudyapplication3;
interface IRemoteService {
int getUserId();
} - 创建远程服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class RemoteService extends Service {
private int mId = -1;
private Binder binder = new IRemoteService.Stub() {
public int getUserId() throws RemoteException {
return mId;
}
};
public IBinder onBind(Intent intent) {
mId = 1256;
return binder;
}
} - 声明远程服务
1
2
3<service
android:name=".RemoteService"
android:process=":aidl" /> - 绑定远程服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class MainActivity extends AppCompatActivity {
public static final String TAG = "wzq";
IRemoteService iRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
iRemoteService = IRemoteService.Stub.asInterface(service);
try {
Log.d(TAG, String.valueOf(iRemoteService.getUserId()));
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName name) {
iRemoteService = null;
}
};
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindService(new Intent(MainActivity.this, RemoteService.class), mConnection, Context.BIND_AUTO_CREATE);
}
}
Messenger
Messenger可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,底层实现是AIDL。
后台运行白名单-保活方式
后台运行白名单设置步骤
- 获取权限
- 判断应用是否在白名单中
- 未在白名单中,申请加入白名单
具体步骤详情
- 获取权限
AndroidManifest.xml
1 | <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> |
- 判断应用是否在白名单中
1 |
|
- 未在白名单中,申请加入白名单
1 | @RequiresApi(api = Build.VERSION_CODES.M) |
注意:
申请加入白名单会弹出申请的弹窗,弹窗中有必要加用户会影响电池续航的提醒。
如果点击了允许,可以在申请的时候调用startActivityForResult,在onActivityResult里再判断一次是否在白名单中。
厂商后台管理
上面的添加到到后台运行白名单,但也会被厂商的后台管理干掉。所以要把应用也加入到厂商系统的后台管理白名单。
整理了部分主流Android厂商,先给出两个方法
1 | /** |
以下是厂商判断,跳转方法及对应设置步骤。另外,跳转方法不保证在所有版本上都能成功所以都要加try catch
华为
1 | /** |
1 | /** |
操作步骤:应用启动管理–》关闭应用开关–》打开允许自启动
小米
1 | /** |
1 | /** |
操作步骤:授权管理–》自启动管理–》允许应用自启动
OPPO
1 | /** |
1 | /** |
操作步骤:权限隐私 -> 自启动管理 -> 允许应用自启动
VIVO
1 | /** |
1 | /** |
操作步骤:权限管理 -> 自启动 -> 允许应用自启动
魅族
1 | /** |
1 | /** |
操作步骤:权限管理 -> 后台管理 -> 点击应用 -> 允许后台运行
三星
1 | /** |
1 | /** |
操作步骤:自动运行应用程序 -> 打开应用开关 -> 电池管理 -> 未监视的应用程序 -> 添加应用
乐视
1 | /** |
1 | /** |
操作步骤:自启动管理 -> 允许应用自启动
锤子
1 | /** |
1 | /** |
操作步骤:权限管理 -> 自启动权限管理 -> 点击应用 -> 允许被系统启动
研究TIM保活方式
原文:史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术
总结本文思路
- 先有了初步分析过程中对一些常规套路的可能性的排除,并嗅到callingPid=0的异常举动;
- 沿着蛛丝马迹,不断反复杀进程,从中寻找更多的规律,不断地向自己提出疑问;
- 结合signal、strace、traces、ps、binder、linux、kill等技能不断地给解答自己的疑惑。
主要提出过这些疑惑:
问题1:安全中心已配置了禁止TIM的自启动,并且安全中心和Whetstone都有对进程自启动以及级联启动的严格限制,为何会有漏网之鱼?
问题2:startService()的callingPid怎么可能等于0?
问题3:进程内的名叫“Thread-89”的线程具有什么特点,如何做到把进程杀掉?
问题4:为何需要4个indicator文件?
问题5:这4个进程到底是什么如何相互监听的呢?
问题6:app_d到底是如何创建出来?又是如何成为init进程的子进程的?
问题7:为何单杀daemon,会牵连app_d进程被杀,这是什么原理?
问题8:app_d是由daemon进程间接fork出来的,会共享binder fd,所以即便daemon进程被杀,死亡回调也不会触发,这又是如何触发的呢?
问题9:TIM到底对Binder框架做了什么级别的修改?这4个互保进程,既然callingPid=0,有没有办法知道到底是谁拉起谁的?
本文总结(TIM保活技术要点)
- 通过flock的文件排他锁方式来监听进程存活状态
- 先采用一对普通的进程Daemoon和MSF相互监听文件的方式获得对方进程是否存活的状态;
- 同时再采用一对退孤给init进程的app_d进程相互监听文件的方式来获得对方进程是否存活的状态; 而这两个进程都有间接由Daemon和MSF进程所fork而来;双重保险。
- 不采用系统框架中startService的Binder框架代码,而是自身在Native层通过自己去查询获取BpActivityManager代理对象,然后自己实现startService接口,并修改为ONEWAY的binder调用,既增加分析问题的难度,也进一步隐藏自身策略;
- 当监听进程死亡,则通过自身实现的StartService的Binder call去拉起对方进程,系统对于这种方式启动进程并没有拦截机制。
这种flock方式至少比网上常说的通过循环监听的方式,要强很多。
比往常的互保更厉害的是TIM共有6个进程(说明:使用过程也还会创建一些进程),其中4个进程,形成两组互动进程,其中一组利用Linux基础托孤原理,可谓是隐藏得很深来互保,进一步确保进程永生。
当然,进程收到signal信号后,如果恰巧这四个进程在同一个时刻点退出,那么还是有概率会被杀。
不走系统框架代码,自己去实现启动服务的binder call也是一大亮点,不过还有更高级的玩法,直接封装ioctl跟驱动交互。之前针对这个问题,做过反保活方案,后来为了某些功能缘故又放开对这个的限制,这里就不再继续展开了。
TIM保活还有改进空间
TIM分别在Java层和Native层,主动向ServiceManager进程查询AMS后,获取BpActivityManager代理对象,然后继续使用框架中的IPCThreadState跟Binder驱动交互,并没有替换掉libbinder.so
其实,还可以更高级的玩法,连IPCThreadState这些框架通信代码也不使用,彻底地去自定义Binder交互代码,类似于servicemanager的方式。可以自己封装ioctl(),直接talkWithDriver。TIM保活还有改进空间, 提供保活变种方案,这样的话,上面的调试代码也拦截不了其对flags修改为ONEWAY的过程。 即使如此,一切都在Control之中, 完全可以在Binder Driver中拦截再定位其策略, 玩得再高级也主要活动在用户态, 内核态的策略还是相对安全的, 此所谓“魔高一座,道高一尺”。