Android进程保活

为什么要保活?

       Android系统的进程保活是一个老生常谈的问题了,经历了历代系统的更新,也产生了很多的保活方案,有的已经失效,有的依然坚挺。如此漫长的保活征程,我们回过头来考虑一下,我们的应用为什么要做保活?或者说我们的应用真的都需要保活吗?
       首先,我想这个问题的答案应该是否定的,我认为并不是所有的应用都应该做保活,对于大多数的应用来说长时间的运行在后台并没有什么太大的好处,也许只是在切换回应用的时候能够得到更积极的响应而已,而这个优点可能要付出非常惨重的代价,而且这个代价是个连锁反应,会传染,当n多个手机上的应用都要做进程保活的时候,可想而知,Android手机会卡成什么样,不管这个手机是1k买的,还是6k买的,我想卡出翔只是个时间问题。所以我认为,在做进程保活之前需要考虑一下,真的有这个必要嘛,当然,也许保不保活不是我们程序员能决定的,得看领导的意思,但是我们多少应该考虑一下吧,万一以后我们中的某些称为领导那!
       其次,我认为大多数应用不需要保活的原因是,Android系统为了维护系统的平稳流畅运行才有了杀进程的机制,我们知道,内存在移动端是稀缺资源,我们必须珍惜的用,不然的话,不管6G还是8G的内存,都有用光的时候,当系统开始杀进程的时候,说明内存真的有点紧张了,需要某些应用做出些牺牲,从系统的角度来看,这种行为是非常合理地,我总不能为了让你们都活着把我自己饿死吧!另外,考虑到进程被杀对于应用来说是一种猝不及防的事件,系统在杀进程之前还会通知应用,也就是onLowMemoryonTrimMemory(int level)会被调用,尤其是onTrimMemory(int level),系统会及时通知应用目前系统的内存状况,也就是会有不同的level,如下;
这里写图片描述
我们完全可以根据目前的内存状况作出自己的响应,比如释放一些内存。这样做,兴许系统内存就够用了,系统不杀进程了,也就不存在保活的顾虑的,当然这种行为是不可靠的,你开发的应用也许做得很好,但是别人的应用在内存方面考虑的不全面,这样系统还是会照杀不误的,你是良民也不行啊!
       最后,对于一些特别的应用,进程保活还是很有必要,甚至是必须的。比如IM,导航,运动类。这些应用的特点就是,有可能并且非常多时候是在后台运行的,没有UI,根据系统的设定,后台进程的优先级很低,被杀的可能性极大,而且这些应用都需要得到及时的响应,你想想,你发个QQ然后切到后台,焦急的等待妹子的回复,结果一直没动静,心灰意冷的重新启动应用的时候发现进程已经被杀死了,因为这个原因又错过一段美好姻缘!再比如,你兴奋的跑了20公里,准备发朋友圈炫耀一下,结果打开Keep,发现进程死了,运动没有被记录下来,多可恨。所以对于这些应用来说进程保活几乎是必须的,即使没法百分百的确保进程能存活下来,但是只要有一线希望就不能放弃。

系统杀进程的依据

本节内容引用自http://geek.csdn.net/news/detail/95035,感谢作者。

       系统要杀你的应用,总不能无缘无故的吧,主要有以下几点:

进程的优先级
进程的重要性,划分5级:
  1. 前台进程(Foreground process)

  2. 可见进程(Visible process)

  3. 服务进程(Service process)

  4. 后台进程(Background process)

  5. 空进程(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
保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

进程的oom_adj阈值

       Android 中对于内存的回收,主要依靠 Lowmemorykiller 来完成,是一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制。
关于 OOM_ADJ 的说明如下:
这里写图片描述

其中红色部分代表比较容易被杀死的 Android 进程(OOM_ADJ>=4),绿色部分表示不容易被杀死的 Android 进程,其他表示非 Android 进程(纯 Linux 进程)。在 Lowmemorykiller 回收内存时会根据进程的级别优先杀死 OOM_ADJ 比较大的进程,对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。

Android 手机中进程被杀死可能有如下情况:这里写图片描述
综上,可以得出减少进程被杀死概率无非就是想办法提高进程优先级,减少进程在内存不足等情况下被杀死的概率。

系统在杀进程的时候,如何保活?

       终于到了要保活的时候了,根据上文,我认为要想做好进程保活,主要有两点,第一点做好性能优化,尽量少的占用内存,营造一个好的环境,这样就能减少系统杀进程的次数;第二点,就是提高系统的优先级,降低oom_adj,减小被杀死的几率。以下是具体的措施:

将Service设置为前台应用

这里写图片描述
       在onCreate中,调用startForeground开启一个前台进程,并且给它设置一个通知。我们可以通过检查当前进程的oom_adj来看看有木有效果。

λ adb shell
vbox86p:/ # su
vbox86p:/ # ps | grep zephyr // zephyr是包名的一部分
u0_a63    1697  266   843320 93968    ep_poll ed4e2bb9 S com.zephyr.app.servicedemo
vbox86p:/ # cat /proc/1697/oom_adj // 当应用在前台的时候
0
vbox86p:/ # cat /proc/1697/oom_adj // 切到了后台了
11
vbox86p:/ #

我们可以明显的看到,当应用在前台的时候oom_adj=0,到后台就变成了oom_adj=11,极易被杀死,这就是为啥后台服务容易被杀的原因。然后我们开启前台服务,重新启动应用看一下;

vbox86p:/ # ps | grep zephyr
u0_a63    1879  266   843324 94196    ep_poll ed4e2bb9 S com.zephyr.app.servicedemo
vbox86p:/ # cat /proc/1879/oom_adj // 前台
0
vbox86p:/ # cat /proc/1879/oom_adj // 切到后台了
3
vbox86p:/ #

效果还是很明显的,oom_adj变成3了,小于4,不那么容易被杀死了。
另外,我们还可以将onStartCommand的返回值设置为START_STICKY,并且在onDestroy中,当服务被杀死的时候,重新开启服务。前者,当系统在条件允许的时候,会重新启动这个Service,但是传过来的intent为null,在原生系统上可以复现,但是对于国内很多深度定制的系统来说并没有什么卵用。后者,Service被杀死的时候,onDestroy不一定被调用,所以也不太靠谱。

1像素Activity

       这种方案流传已久,目的也是为了降低oom_adj,主要就是通过监听系统的锁屏和解锁广播,然后开启和销毁一个1像素的Activity。实现如下;
ScreenListenerManager封装了广播接收器
ScreenManager封装了对1像素Activity的管理
SinglePixelActivity1像素的Activity
MainActivity调起1像素Activity的Activity,比如某运动APP的运动界面,我们要锁屏的时候就调起它

// ScreenListenerManager 
public class ScreenListenerManager {

    private final Receiver mReceiver;
    private WeakReference<Context> mReference;
    private OnScreenListener mScreenListener;

    public ScreenListenerManager(Context context) {
        mReference = new WeakReference<>(context);
        mReceiver = new Receiver();
    }

    public void register() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_USER_PRESENT);
        mReference.get().registerReceiver(mReceiver, filter);
    }

    public void unRegister() {
        mReference.get().unregisterReceiver(mReceiver);
    }

    public class Receiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (mScreenListener == null) return;
            switch (action) {
                case Intent.ACTION_SCREEN_ON: // 开屏
                    mScreenListener.onScreenOn();
                    break;
                case Intent.ACTION_SCREEN_OFF: // 锁屏
                    mScreenListener.onScreenOff();
                    break;
                case Intent.ACTION_USER_PRESENT: // 解锁
                    mScreenListener.onUserresent();
                    break;
            }
        }
    }

    public void setOnScreenListener(OnScreenListener listener) {
        mScreenListener = listener;
    }

    public interface OnScreenListener{
        void onScreenOn();
        void onScreenOff();
        void onUserresent();
    }
}
// ScreenManager 
public class ScreenManager {

    private Context mContext;

    private WeakReference<Activity> mWeakReference;

    private static ScreenManager mScreenManager;

    private ScreenManager(Context context) {
        mContext = context;
    }

    public static ScreenManager getInstance(Context context) {
        if (mScreenManager == null) {
            synchronized (ScreenManager.class) {
                if (mScreenManager == null) {
                    mScreenManager = new ScreenManager(context);
                }
            }
        }
        return mScreenManager;
    }

    public void setSinglePixelActivity(Activity activity) {
        mWeakReference = new WeakReference<>(activity);
    }

    public void startSinglePixelActivity(Activity from) {
        Intent intent = new Intent(from, SinglePixelActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
    }

    public void finishSinglePixelActivity() {
        if (mWeakReference != null &&mWeakReference.get() != null) {
            mWeakReference.get().finish();
        }
    }
}
// SinglePixelActivity 
public class SinglePixelActivity extends AppCompatActivity {
    private static final String TAG = "SinglePixelActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Window window = getWindow();
        WindowManager.LayoutParams attributes = window.getAttributes();
        attributes.x = 0;
        attributes.y = 0;
        attributes.gravity = Gravity.LEFT | Gravity.TOP;
        attributes.width = 1;
        attributes.height = 1;
        window.setAttributes(attributes);
        ScreenManager.getInstance(getApplicationContext()).setSinglePixelActivity(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}
// MainActivity
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private ScreenListenerManager mManager;
    private ScreenManager mScreenManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mManager = new ScreenListenerManager(this);
        mManager.register();
        mManager.setOnScreenListener(new ScreenListenerManager.OnScreenListener() {
            @Override
            public void onScreenOn() {
                Log.d(TAG, "onScreenOn: ");
            }

            @Override
            public void onScreenOff() {
                Log.d(TAG, "onScreenOff: ");
                // 开启1像素的Activity
                mScreenManager.startSinglePixelActivity(MainActivity.this);
            }

            @Override
            public void onUserresent() {
                Toast.makeText(MainActivity.this, "onUserresent!", Toast.LENGTH_LONG).show();
                Log.d(TAG, "onUserresent: ");
                // 关闭1像素的Activity
                mScreenManager.finishSinglePixelActivity();
            }
        });
        mScreenManager = ScreenManager.getInstance(getApplicationContext());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
      mManager.unRegister();
    }
}
        <activity
            android:name=".SinglePixelActivity"
            android:configChanges="keyboardHidden|orientation|screenSize|navigation|keyboard"
             <!--用于控制SinglePixelActivity不在最近任务列表中显示 -->
            android:excludeFromRecents="true"
            <!--用于标记当用户再起启动应用(TASK)时是否关闭已经存在的Activity的实例,false表示不关闭; -->
            android:finishOnTaskLaunch="false" 
            android:launchMode="singleInstance"
            android:theme="@style/SingleActivityStyle">
        </activity>
<style name="SingleActivityStyle" parent="AppTheme">
        <!-- 窗体背景颜色为透明 -->
        <item name="android:windowBackground">@android:color/transparent</item>
        <!-- 窗体没有边框 -->
        <item name="android:windowFrame">@null</item>
        <!-- 窗体不包含标题栏 -->
        <item name="android:windowNoTitle">true</item>
        <!-- 窗体悬浮 -->
        <item name="android:windowIsFloating">true</item>
        <!-- 自定义TitleBar时去掉多余的阴影-->
        <item name="android:windowContentOverlay">@null</item>
        <!-- 不允许窗体背景变暗-->
        <item name="android:backgroundDimEnabled">false</item>
        <!-- 窗体切换无动画-->
        <item name="android:windowAnimationStyle">@null</item>
        <!-- 禁用窗口的预览动画-->
        <item name="android:windowDisablePreview">true</item>
        <item name="android:windowNoDisplay">false</item>
    </style>

以上就是具体的实现。

被系统杀死了如何复活?

       到这里,这个应用已经没治了。死了还要救过来,虽然随着系统的更新,很多拉活的方法已经失效,但是也有新的方法产生,总之就是道高一尺魔高一丈。
        目前比较有效的方法有JobScheduler, 使用特定平台的推送等。由于这些行为实在是太流氓了,没有用过,推荐看下http://www.52im.net/thread-1140-1-1.html

写在最后

       合理保活进程,维护Android系统的平稳流畅,是每个Android开发者的责任和义务,毕竟谁也不想用卡成翔的手机!

参考:
http://geek.csdn.net/news/detail/95035
https://juejin.im/entry/59e9aa7b5188257f6970bb4a

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旅行者40

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值