Android进阶——性能优化之进程保活原理及手段完全解析(一)

版权声明:本文为CrazyMo_原创,转载请在显著位置注明原文链接 https://blog.csdn.net/CrazyMo_/article/details/81807939

#引言

#一、进程和线程概述
某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下同一应用的所有组件在相同的进程和线程(称为“主”线程mainThread)中运行(即一个App对应一个进程)。 但是若某个应用组件已启动且该应用已存在进程(因为存在该应用对应的其他组件),则该组件会在此进程内启动并使用相同的执行线程。虽然默认情况下同一应用的所有组件均在相同的进程中运行,但如果需要控制App中某个组件的运行进程,可在清单文件中进行配置:只需要在清单文件中对应的四大组件节点**< activity >< service >< receiver >** 、 < provider >< application >配置 android:process 属性(用于指定该组件应在哪个进程运行,多个组件也可以共享一个进程)。其中< application > 元素android:process 用于设置适用于所有组件的默认运行进程。

#二、进程分类
一般Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,需要清除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会对进程进行分类。 需要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,依此类推,以回收系统资源。于是Android对进程进行了分类:Foreground Progresses前台/活动进程Visiable Processes可见进程Service Progresses服务进程Background Progresses后台进程Empty Progresses空进程
这里写图片描述

##1、前台进程
用户当前操作所必需依赖的满足以下任一条件的进程则视为前台进程:

  • 用户正在交互的 Activity(即Activity 的 onResume() 方法已被调用

  • 托管某个 Service,后者绑定到用户正在交互的 Activity,简答来说就是在当前用户交互的Activity里绑定了Service,假如此Service运行在其他进程时,这个进程则是前台进程。

  • 托管正在“前台”运行的 Service(服务已调用 startForeground())

  • 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())

  • 托管正执行其 onReceive() 方法的 BroadcastReceiver

##2、可见进程
可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。那些没有任何前台组件、但仍会影响用户在屏幕上所见内容的满足以下任一条件的进程即视为可见进程

  • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。

  • 托管绑定到可见(或前台)Activity 的 Service

##3、服务进程
正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(比如在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。服务的onStartCommand()方法返回的常量,会因为返回值不同而标新不同:

  1. START_STICKY——表示希望系统可用的时候自动重启服务,但不关心是否能获得最后一次的 Intent,例如可以重建自己的状态或者控制自己的 start/stop 生命周期。

  2. START_REDELIVER_INTENT——为那些在被杀死之后重启时重新获得 Intent 的服务的,直到用传递给 onStartCommand() 方法的 startId 参数调用stopSelf()为止。这里会使用 Intent 和 startId作为队列完成工作。

  3. START_NOT_STICKY——用于那些杀掉也没关系的服务。这适合那些管理周期性任务的服务,它们只是等待下一个时间窗口工作。

##4、后台进程
包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 Lru列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

##5、空进程
不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间,只要 Android 需要可以随时杀掉它们。

#三、进程的优先级和Low Memory Killer

##1、进程的优先级
进程优先级除了按照进程种类来确定之外,还会根据一个确切的数值来确定——oom_odj值。进程的优先级通过进程的adj值来反映,它是linux内核分配给每个系统进程的一个值,进程回收机制根据这个值来决定是否进行回收。可以通过以下命令查询

adb shell cat /proc/进程Id/oom_odj

adj的值越小,进程的优先级越高。adj越大,占用内存越多会被最先kill掉,所以保活可以通过了降低oom_adj的值,以及如何使得我们应用占的内存最少

这里写图片描述

##2、Low Memory Killer

Android系统出于体验和性能上的考虑,当App在退到后台时系统并不会真正的杀掉这个进程,而是将其缓存起来。所以打开的应用越多,后台缓存的进程也越多。只有在系统内存不足的情况下或者手动清理时,系统开始依据自身的一套进程回收机制来判断要杀掉哪些进程,以腾出内存来供给需要的App, 这套杀进程回收内存的机制就叫 Low Memory Killer。所以系统还为每种进程设置了内存阈值,内存阈值在不同的手机上不一样,一旦低于该值,Android便开始按顺序关闭进程. 因此Android开始结束优先级最低的空进程,即当可用内存小于空进程的阈值时。
这里写图片描述

#五、通过1像素Activity 提高进程优先级

从上文我们得知了可以通过了降低oom_adj的值来保活,所以前台进程就可以使得odj值降低,以前的QQ就是这样干过。以前其中一种策略就是通过监听手机锁屏广播(不过在高版本中可能没有那么容易监听到开锁屏广播,可以替换其他的策略),在屏幕锁屏时启动1个像素透明的 Activity,然后在用户解锁时将 Activity 销毁掉,使得原本处于后台进程的变成前台进程,从而达到提高进程优先级的作用。实现1像素Activity 提权保活的步骤有:
##1、定义1 像素的Activity

/**
 * 通过监听手机锁屏广播,在屏幕锁屏时启动1个像素透明的 Activity,然后在用户解锁时将 Activity 销毁掉**,使得原本处于后台进程的
 * 变成前台进程,从而达到提高进程优先级的作用。
 */
public class GuardActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Window window=getWindow();
        window.setGravity(Gravity.START|Gravity.TOP);//设置Activity 显示在左上角
        WindowManager.LayoutParams attrs=window.getAttributes();
        attrs.width=1;
        attrs.height=1;
        attrs.x=0;
        attrs.y=0;
        attrs.alpha=1;
        window.setAttributes(attrs);
        GuardManager.getInstance().setGuardActivity(this);
    }
}

##2、管理息屏和开屏广播

public class ScreeReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action=intent.getAction();
        if(TextUtils.equals(action,Intent.ACTION_SCREEN_OFF)){
            Log.e("cmo","手机屏幕息屏");
            GuardManager.getInstance().startGuradActivity(context);
        }else if(TextUtils.equals(action,Intent.ACTION_SCREEN_ON)){
            Log.e("cmo","手机屏幕开屏");
            GuardManager.getInstance().finishGuradActivity(context);
        }
    }
}

GuardManager 作为一个工具类只是为了统一管理注册和取消开屏和息屏广播

package com.crazymo.guardback.activity;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;

import java.lang.ref.WeakReference;

/**
 * Created by cmo on 2018/8/18  17:17
 */

public class GuardManager {
    private ScreeReceiver mScreeReceiver;
    private static final GuardManager INSTANCE=new GuardManager();
    private WeakReference<Activity> mGuardAct;//弱引用保存,防止Activity内存泄漏

    private GuardManager(){}

    public static GuardManager getInstance(){
        return  INSTANCE;
    }

    /**
     * 注册息屏开屏广播
     * @param context
     */
    public void registScreenBroadcast(Context context){
        if(context!=null) {
            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(Intent.ACTION_SCREEN_ON);
            intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
            mScreeReceiver = new ScreeReceiver();
            context.registerReceiver(mScreeReceiver,intentFilter);
        }
    }

    /**
     * 取消注册广播接收者
     * @param context
     */
    public void unregisScreenReceiver(Context context){
        if(context!=null){
            context.unregisterReceiver(mScreeReceiver);
        }
    }

    /**
     *
     * @param context
     */
    public void startGuradActivity(Context context){
        if(context!=null) {
            Intent intent = new Intent(context, GuardActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
        }
    }

    /**
     *
     * @param context
     */
    public void finishGuradActivity(Context context){
        if(context!=null){
            if (null != mGuardAct) {
                Activity activity = mGuardAct.get();
                if (null != activity) {
                    activity.finish();
                }
                mGuardAct = null;
            }
        }
    }

    public void setGuardActivity(GuardActivity keep) {
        mGuardAct = new WeakReference<Activity>(keep);
    }

}

##3、配置清单文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.crazymo.guardback">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:theme="@style/GuradActivityTheme"
            android:name=".activity.GuardActivity"
            android:excludeFromRecents="true"
            android:taskAffinity="com.crazymo.guardback"
            />
    </application>

</manifest>

#六、通过startForeground设置前台服务提升进程优先级

从上文得知,在前台进程是优先级最高的类型,其中有一个条件就是调用了startForeground方法的Service,而这个Service运行的进程就属于前台进程,优先级最高。

##1、继承Service并在onCreate方法中调用startForeground

/**
 * Created by cmo on 2018/8/19  13:18
 */

public class ForgroundGuardService extends Service {
	private fianl int 
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        ////startForeground(1,new Notification()); 这里id 传0 的话 虽然可以隐藏Notification
        //让服务变成前台服务
        startForeground(10, new Notification());
        //api> 18 以上的设备,则可以通过 启动一个Service startForeground给相同的id,然后结束这个Service
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            startService(new Intent(this, InnnerService.class));
        }
    }
	/**
	* 为了隐藏掉状态栏上的Notification	
	*/
    public static class InnnerService extends Service {

        @Override
        public void onCreate() {
            super.onCreate();
            startForeground(10, new Notification());
            stopSelf();
        }

        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }
}

##2、在清单文件声明服务

<application>
		...
        <service android:name=".service.ForgroundGuardService" />
        <service android:name=".service.ForgroundGuardService$InnnerService"/>
    </application>

##3、通过startService方法启动服务

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        startService(new Intent(MainActivity.this, ForgroundGuardService.class));
    }
}

#七、通过监听系统广播强行拉活

Android系统在发生特定系统事件时,都会发出全局广播,通过在 AndroidManifest 中静态注册对应的广播监听器,即可在发生响应事件时由系统自动拉活。如果应用注册为接收广播,则在每次发送广播时,应用的接收器都会消耗资源。 如果多个应用注册为接收基于系统事件的广播,触发广播的系统事件会导致所有应用快速地连续消耗资源,从而降低用户体验。为了优化体验,Android 7.0(API 25)开始对广播的使用进行了限制,Android8.0时则更为严格,很多广播已经不能再清单文件中静态注册了,因为注册了也监听不到了,想监听只能通过代码动态注册。以下是8.0之后仍然可以静态注册的广播列表,但是对于强行拉活这个业务功能来说这些可静态注册的广播都不太适合,所以这个拉活手段仅仅适用于以前的低版本,可作为一个可选项。

#八、臭名昭著的“全家桶”拉活
有多个app在用户设备上安装,只要开启其中一个就可以将其他的app也拉活。比如手机里装了手Q、QQ空间、兴趣部落等等,那么打开任意一个app后,其他的app也都会被唤醒。这些都是巨头之间的肮脏交易。

#九、系统Service 机制拉活
在Service的生命周期中有一个方法onStartCommand,他返回一个int类型的值,将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活

  • START_STICKY——“粘性”。如果service进程被kill掉,保留service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试重新创建service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand(Intent,int,int)方法。如果在此期间没有任何启动命令被传递到service,那么参数Intent将为null。

  • START_NOT_STICKY——“非粘性的”。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务。

  • START_REDELIVER_INTENT——重传Intent。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,并将Intent的值传入。

  • START_STICKY_COMPATIBILITY——START_STICKY的兼容版本,但不保证服务被kill后一定能重启。

注意只要 targetSdkVersion 不小于5,就默认是 START_STICKY。但是某些ROM 系统不会拉活。并且经过测试,Service 第一次被异常杀死后很快被重启,第二次会比第一次慢,第三次又会比前一次慢,一旦在短时间内 Service 被杀死4-5次,则系统可能不再拉起,这种拉活机制可能不太靠谱。

ps:未完待续…

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/81807939