Android初级第八讲之应用如何保活 您所在的位置:网站首页 android进程保活与入门实践 Android初级第八讲之应用如何保活

Android初级第八讲之应用如何保活

2023-09-09 06:06| 来源: 网络整理| 查看: 265

 本文来自刘兆贤的博客_CSDN博客-Java高级,Android旅行,Android基础领域博主 ,引用必须注明出处!

一般情况下应用保活的场景在于:后台要不断定位描述一条清晰轨迹、IM通信需要及时收到消息提醒等。

所谓保活就是避免被应用杀死,前台应用自然不用说,主要是后台应用。我们知道Android的应用优先级是-16到15,系统进程一般拥有-16到-1的优先级,而普通应用只拥有0-15的优先级;级别越小优先级越高。

首先进入后台的应用不能是流氓进程,比如频率的定位、发送网络请求、不允许屏幕关闭等;如果做心跳,需要选择合适的周期比如2分钟一次,进行socket长连;可以设计一个前台进程一个后台进程,互相监测重启对方;保持至少有1像素位于前台。

其次如果强制保活,那么90%的可能会杀死进程;4.4以前可以用AlarmManager的setRepeating方法可定时保活,之后可以使用setAndAllWhileIdle方法在保持在一段时间内集体唤醒;

再者可以通过注册通知,比如开机启动、时间变动、锁屏解屏、网络开关、收到短信、打入电话等;5.0以后可以使用JobSchedule的Api进行轮循操作,通过主动注册网络切换广播来监听(配置文件注册方式已被禁止)

如果是Service还可以通过startForeGround的方式来让服务处于前台,避免杀死,关闭时使用stopForeGround来去掉。另外在重写onStartCommand,返回START_STICKY,使service被杀死后自动重启

public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; }

还有通过WakeLock,需要申请到权限

 获取锁:

WakeLock mWakeLock=null;  PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);  /**  * PowerManager.PARTIAL_WAKE_LOCK:保持CPU运转,屏幕和键盘灯可能是关闭的  * PowerManager.SCREEN_DIM_WAKE_LOCK:保持CPU运转,运行屏幕显示但是屏幕有可能是灰的,允许关闭键盘灯  * PowerManager.SCREEN_BRIGHT_WAKE_LOCK:保持CPU运转,屏幕高亮显示,允许关闭键盘灯  * PowerManager.FULL_WAKE_LOCK:保持CPU运转,屏幕高亮显示,键盘灯高亮显示  * PowerManager.ON_AFTER_RELEASE:当锁被释放时,保持屏幕亮起一段时间  * PowerManager.ACQUIRE_CAUSES_WAKEUP:强制屏幕亮起  */  mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SoundRecorder");  mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);  mWakeLock.acquire();  

          释放锁   

if (mWakeLock.isHeld()) {        mWakeLock.release();    } 

如果是Root手机,可以设置应用的persistent标签为true。

终归这样的方式很流氓,就像张小龙对微信的期许一下:希望它是个人们有需要用一下,用完就放开的工具。

给Activity设置如下标签,当用户点击最近任务时,App不会显示在内,达到隐藏的目的

android:excludeFromRecents="true"

tips:另外针对线程做双重UncaughtExceptionHandler,有助于防止App直接挂掉。

附斑马行车对用户权限的要求(主要设置白名单)

恭喜你,看到最后发现新天地!无论如何,在保活的时候要考虑下用户的感受,理性保活!

其他如启动1像素的activity,Native进程保活,通知拉起保存Service、监听第三方和系统的公开广播

Android进程保活招数大全

目前市面上的应用,貌似除了微信和手Q都会比较担心被用户或者系统(厂商)杀死问题。本文对 Android 进程拉活进行一个总结。

Android 进程拉活包括两个层面:

A. 提供进程优先级,降低进程被杀死的概率

B. 在进程被杀死后,进行拉活

本文下面就从这两个方面做一下总结。

1. 进程的优先级

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要清除旧进程来回收内存。

为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。

必要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,依此类推,以回收系统资源。

进程的重要性,划分5级:

前台进程(Foreground process)

可见进程(Visible process)

服务进程(Service process)

后台进程(Background process)

空进程(Empty process)

前台进程的重要性最高,依次递减,空进程的重要性最低,下面分别来阐述每种级别的进程

1.1. 前台进程 —— Foreground process

用户当前操作所必需的进程。通常在任意给定时间前台进程都为数不多。

只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。

A. 拥有用户正在交互的 Activity(已调用 onResume())

B. 拥有某个 Service,后者绑定到用户正在交互的 Activity

C. 拥有正在“前台”运行的 Service(服务已调用 startForeground())

D. 拥有正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())

E. 拥有正执行其 onReceive() 方法的 BroadcastReceiver

1.2. 可见进程 —— Visible process

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。

可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

A. 拥有不在前台、但仍对用户可见的 Activity(已调用 onPause())。

B. 拥有绑定到可见(或前台)Activity 的 Service

1.3. 服务进程 —— Service process

尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。

因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

A. 正在运行 startService() 方法启动的服务,且不属于上述两个更高类别进程的进程。

1.4. 后台进程 —— Background process

后台进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 

通常会有很多后台进程在运行,因此它们会保存在 LRU 列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。

如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,

因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

A. 对用户不可见的 Activity 的进程(已调用 Activity的onStop() 方法)

1.5. 空进程 —— Empty process

保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 

为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

A. 不含任何活动应用组件的进程

详情参见:http://developer.android.com/intl/zh-cn/guide/components/processes-and-threads.html

2. Android 进程回收策略

Android 中对于内存的回收,主要依靠 Lowmemorykiller 来完成,是一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制。

关于 OOM_ADJ 的说明如下:

其中

红色部分代表比较容易被杀死的 Android 进程(OOM_ADJ>=4),

绿色部分表示不容易被杀死的 Android 进程,

其他表示非 Android 进程(纯 Linux 进程)。

在 Lowmemorykiller 回收内存时会根据进程的级别优先杀死 OOM_ADJ 比较大的进程,

对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。

Android 手机中进程被杀死可能有如下情况:

综上,可以得出减少进程被杀死概率无非就是想办法提高进程优先级,减少进程在内存不足等情况下被杀死的概率。

3. 提升进程优先级的方案

3.1. 利用 Activity 提升权限

3.1.1. 方案设计思想

监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素的 Activity,在用户解锁时将 Activity 销毁掉。

注意该 Activity 需设计成用户无感知。

通过该方案,可以使进程的优先级在屏幕锁屏时间由4提升为最高优先级1。

3.1.2. 方案适用范围

适用场景: 

本方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间(一般为5分钟以内)内会杀死后台进程,已达到省电的目的问题。

适用版本: 适用于所有的 Android 版本。

3.1.3. 方案具体实现

首先定义 Activity,并设置 Activity 的大小为1像素:

其次,从 AndroidManifest 中通过如下属性,排除 Activity 在 RecentTask 中的显示:

最后,控制 Activity 为透明:

Activity 启动与销毁时机的控制:

3.2. 利用 Notification 提升权限

3.2.1. 方案设计思想

Android 中 Service 的优先级为4,通过 setForeground 接口可以将后台 Service 设置为前台 Service,使进程的优先级由4提升为2,

从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低。

3.2.2. 方案实现挑战

从 Android2.3 开始调用 setForeground 将后台 Service 设置为前台 Service 时,

必须在系统的通知栏发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的。

对于不需要常驻通知栏的应用来说,该方案虽好,但却是用户感知的,无法直接使用。

3.2.3. 方案挑战应对措施

通过实现一个内部 Service,在 LiveService 和其内部 Service 中同时发送具有相同 ID 的 Notification,然后将内部 Service 结束掉。

随着内部 Service 的结束,Notification 将会消失,但系统优先级依然保持为2。

3.2.4. 方案适用范围

适用于目前已知所有版本。

3.2.5. 方案具体实现

4. 进程死后拉活的方案

4.1. 利用系统广播拉活

#####4.1.1. 方案设计思想

在发生特定系统事件时,系统会发出响应的广播,通过在 AndroidManifest 中“静态”注册对应的广播监听器,即可在发生响应事件时拉活。

常用的用于拉活的广播事件包括:

4.1.2. 方案适用范围

适用于全部 Android 平台。但存在如下几个缺点:

1) 广播接收器被管理软件、系统软件通过“自启管理”等功能禁用的场景无法接收到广播,从而无法自启。

2) 系统广播事件不可控,只能保证发生事件时拉活进程,但无法保证进程挂掉后立即拉活。

因此,该方案主要作为备用手段。

4.2. 利用第三方应用广播拉活

4.2.1. 方案设计思想

该方案总的设计思想与接收系统广播类似,不同的是该方案为接收第三方 Top 应用广播。

通过反编译第三方 Top 应用,如:手机QQ、微信、支付宝、UC浏览器等,以及友盟、信鸽、个推等 SDK,找出它们外发的广播,

在应用中进行监听,这样当这些应用发出广播时,就会将我们的应用拉活。

4.2.2. 方案适用范围

该方案的有效程度除与系统广播一样的因素外,主要受如下因素限制:

1) 反编译分析过的第三方应用的多少

2) 第三方应用的广播属于应用私有,当前版本中有效的广播,在后续版本随时就可能被移除或被改为不外发。

这些因素都影响了拉活的效果。

4.3. 利用系统Service机制拉活

4.3.1. 方案设计思想

将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活:

4.3.2. 方案适用范围

如下两种情况无法拉活:

Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,

一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。

进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。

4.4. 利用Native进程拉活

#####4.4.1. 方案设计思想

**主要思想:**利用 Linux 中的 fork 机制创建 Native 进程,在 Native 进程中监控主进程的存活,

当主进程挂掉后,在 Native 进程中立即对主进程进行拉活。

**主要原理:**在 Android 中所有进程和系统组件的生命周期受 ActivityManagerService 的统一管理。

而且,通过 Linux 的 fork 机制创建的进程为纯 Linux 进程,其生命周期不受 Android 的管理。

#####4.4.2. 方案实现挑战

挑战一:在 Native 进程中如何感知主进程死亡。

要在 Native 进程中感知主进程是否存活有两种实现方式:

在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,档主进程不存活时进行拉活。

该方案的很大缺点是不停的轮询执行判断逻辑,非常耗电。

在主进程中创建一个监控文件,并且在主进程中持有文件锁。

在拉活进程启动后申请文件锁将会被堵塞,一旦可以成功获取到锁,说明主进程挂掉,即可进行拉活。

由于 Android 中的应用都运行于虚拟机之上,Java 层的文件锁与 Linux 层的文件锁是不同的,

要实现该功能需要封装 Linux 层的文件锁供上层调用。

封装 Linux 文件锁的代码如下:

Native 层中堵塞申请文件锁的部分代码:

挑战二:在 Native 进程中如何拉活主进程。

通过 Native 进程拉活主进程的部分代码如下,即通过 am 命令进行拉活。

通过指定“–include-stopped-packages”参数来拉活主进程处于 forestop 状态的情况。

挑战三:如何保证 Native 进程的唯一。

从可扩展性和进程唯一等多方面考虑,将 Native 进程设计层 C/S 结构模式,主进程与 Native 进程通过 Localsocket 进行通信。

在Native进程中利用 Localsocket 保证 Native 进程的唯一性,不至于出现创建多个 Native 进程以及 Native 进程变成僵尸进程等问题。

4.4.3. 方案适用范围

该方案主要适用于 Android5.0 以下版本手机。

该方案不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。

对于 Android5.0 以上手机,系统虽然会将native进程内的所有进程都杀死,

这里其实就是系统“依次”杀死进程时间与拉活逻辑执行时间赛跑的问题,

如果可以跑的比系统逻辑快,依然可以有效拉起。

记得网上有人做过实验,该结论是成立的,在某些 Android 5.0 以上机型有效。

4.5. 利用 JobScheduler 机制拉活

4.5.1. 方案设计思想

Android5.0 以后系统对 Native 进程等加强了管理,Native 拉活方式失效。

系统在 Android5.0 以上版本提供了 JobScheduler 接口,系统会定时调用该进程以使应用进行一些逻辑操作。

在本项目中,我对 JobScheduler 进行了进一步封装,兼容 Android5.0 以下版本。

封装后 JobScheduler 接口的使用如下:

4.5.2. 方案适用范围

该方案主要适用于 Android5.0 以上版本手机。

该方案在 Android5.0 以上版本中不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以上版本拉活效果非常好。

仅在小米手机可能会出现有时无法拉活的问题。

4.6. 利用账号同步机制拉活

#####4.6.1. 方案设计思想

Android 系统的账号同步机制会定期同步账号进行,该方案目的在于利用同步机制进行进程的拉活。添加账号和设置同步周期的代码如下:

该方案需要在 AndroidManifest 中定义账号授权与同步服务。

####4.6.2. 方案适用范围

该方案适用于所有的 Android 版本,包括被 forestop 掉的进程也可以进行拉活。

最新 Android 版本(Android N)中系统好像对账户同步这里做了变动,该方法不再有效。

5. 其他有效拉活方案

经研究发现还有其他一些系统拉活措施可以使用,但在使用时需要用户授权,用户感知比较强烈。

这些方案包括:

利用系统通知管理权限进行拉活

利用辅助功能拉活,将应用加入厂商或管理软件白名单。

这些方案需要结合具体产品特性来搞。

上面所有解释这些方案都是考虑的无 Root 的情况。

其他还有一些技术之外的措施,比如说应用内 Push 通道的选择:

国外版应用:接入 Google 的 GCM。

国内版应用:根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送;其他手机可以考虑接入腾讯信鸽或极光推送与小米推送做 A/B Test。

两个进程Service互相拉起,一个本进程Service,一个新开进程Service,利用aidl监听对方死亡通知。

LocalService:

public class LocalService extends Service { private static final String TAG = LocalService.class.getName(); private MyBinder mBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service); try { Log.e("LocalService", "connected with " + iMyAidlInterface.getServiceName()); //TODO whh 本地service被拉起,检测如果mainActivity不存在则拉起 if (ExitApplication.getInstance().isEmpty()) { Intent intent = new Intent(LocalService.this.getBaseContext(), MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getApplication().startActivity(intent); } } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { Toast.makeText(LocalService.this, "链接断开,重新启动 RemoteService", Toast.LENGTH_LONG).show(); Log.e(TAG, "onServiceDisconnected: 链接断开,重新启动 RemoteService"); startService(new Intent(LocalService.this, RemoteService.class)); bindService(new Intent(LocalService.this, RemoteService.class), connection, Context.BIND_WAIVE_PRIORITY); } }; public LocalService() { } @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e(TAG, "onStartCommand: LocalService 启动"); Toast.makeText(this, "Service:LocalService" + "启动", Toast.LENGTH_LONG).show(); startService(new Intent(LocalService.this, RemoteService.class)); bindService(new Intent(LocalService.this, RemoteService.class), connection, Context.BIND_WAIVE_PRIORITY); return START_STICKY; } @Override public IBinder onBind(Intent intent) { mBinder = new MyBinder(); return mBinder; } private class MyBinder extends IMyAidlInterface.Stub { @Override public String getServiceName() throws RemoteException { return LocalService.class.getName(); } @Override public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException { } } }

RemoteService:

public class RemoteService extends Service { private static final String TAG = RemoteService.class.getName(); private MyBinder mBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service); try { Log.e(TAG, "connected with " + iMyAidlInterface.getServiceName()); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { Log.e(TAG, "onServiceDisconnected: 链接断开,重新启动 LocalService"); String startService = AppProxyFactory.getProxy().getPrefManager().getValue("Service", ""); Toast.makeText(RemoteService.this, "链接断开,重新启动 LocalService" + ",不同进程可以共用sharePreference吗?" + !TextUtils.isEmpty(startService), Toast.LENGTH_LONG).show(); startService(new Intent(RemoteService.this, LocalService.class)); bindService(new Intent(RemoteService.this, LocalService.class), connection, Context.BIND_IMPORTANT); } }; public RemoteService() { } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e(TAG, "onStartCommand: RemoteService 启动"); AppProxyFactory.getProxy().getPrefManager().saveValue("RemoteService2", ProcessUtils.getCurrProcessName(this)); String startService = AppProxyFactory.getProxy().getPrefManager().getValue("LocalService", ""); Toast.makeText(this, "RemoteService 启动" + ProcessUtils.getCurrProcessName(this)+",不同进程可以共用sharePreference吗?" + !TextUtils.isEmpty(startService)+startService, Toast.LENGTH_LONG).show(); bindService(new Intent(this, LocalService.class), connection, Context.BIND_IMPORTANT); return START_STICKY; } @Override public IBinder onBind(Intent intent) { mBinder = new MyBinder(); return mBinder; } private class MyBinder extends IMyAidlInterface.Stub { @Override public String getServiceName() throws RemoteException { return RemoteService.class.getName(); } @Override public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException { } } }

AIDL:

interface IMyAidlInterface { /** * Demonstrates some basic types that you can use as parameters * and return values in AIDL. */ void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString); String getServiceName(); }

AndroidManifest.xml

小结:Service默认跟运行在主进程中(applicationId);添加android:process标签,value以":"开头代表运行在当前进程的私有进程中(applicationId:remote),否则需要以"."代表公共进程(.remote)。

在本进程中启动:

startService(new Intent(this, LocalService.class));

实测8.0以上机器,第二次主进程Crash后,手机判断RemoteService所在为非法进程,主动杀死,并一直重启失败。

参考:http://dev.qq.com/topic/57ac4a0ea374c75371c08ce8



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有