Giter Club home page Giter Club logo

keepalive-kotlin's Introduction

KeepAlive-kotlin

Android保活机制探索kotlin版

为什么会诞生这个项目

公司项目是户外运动软件, 保活处理自然是重中之重, 这个DEMO也是对Android保活机制的探索和思考, 顺便也练习下kotlin(这才是重点)

问题

  1. 什么场景会杀进程?
  2. 保活方法都有哪些
  3. 保活的原理是什么?
  4. 提升优先级的方式?

一. 杀进程场景

  • 1, 2两种情况进程属于自然死亡, 我们的需求是在运动中, 应用推到后台, 可以持续记录运动记录, 所以我们要重点解决这两种情况下的保活问题
  • 3-6属于用户主动杀死进程
  • 7属于应用出现异常被系统杀死
序号 场景
1 被lmk杀死
2 成为空进程,缓存进程后被杀
3 最近任务一键清理
4 最近任务上滑被死
5 使用Setting forceStop
6 第三方应用如猎豹,360杀进程
7 出现FC或者ANR

二. 保活方法

序号 保活方法 API限制 效果&原理
1 正常的开启前台Servce 无限制 启动前台Service伴随着Notification,如正在播放音乐、正在导航,系统默认给进程高优先级
2 正常弹出悬浮窗 无限制 展示用户可见的窗口,如一键清理火箭,系统默认给进程高优先级
3 设置persistent=true 无限制 成为系统常驻进程
4 启动前台Service传入无意义Notification API<=17 利用系统漏洞,不展示Notification但享受前台Service优先级待遇
5 启动两个相同id前台Service,stop后者 18<=API<=24 同上,享受前台Service优先级待遇
6 native层保活 API<=23 进程互保,通过文件锁监听死亡,如果死亡则拉起
7 桌面放置一像素页面 未知 享受可见进程优先级待遇
8 静态注册系统常用广播 未知 满足权限的前提下,收到广播前进程会被拉起
9 进程Service/Provider互绑 无限制 效果并不好,forceStop下等场景无法存活,只是procState优先级很高
10 AlarmManager/JobScheduler 未知 守护服务,如果被守护进程死亡则拉起
11 账户同步 未知 Android原生机制会定期唤醒账户更新服务

三. 保活原理

系统按照优先级由低到高杀进程, 本质保活就是提升进程优先级, 所以我们要了解系统是怎么计算进程的优先级的

updateOomAdjLocked(进程优先级更新)

updateOomAdjLocked (1) - 空进程、缓存进程

  • 获取TOP_APP, 即前台Activity, 这个会在后面的updateOomAdjLocked方法中用到
  • 重置所有UIDRecord
  • 获取空进程, 缓存进程个数上限
  • 计算出当前所有正在运行进程的adj并应用
  • 如果到达了上限, 杀死: 空进程 -> 缓存进程 -> 进程是isolated并且没有在运行Service

updateOomAdjLocked (2) - 对进程内存trim

  • 针对后台进程,对内存进行trim
  • 初始化memFactor, 计算memFactor
  • 如果当前进程memLevel小于当前的level,则需要对进程内存做trim
  • 更新UidRecord变化

computeOomAdjLocked(进程优先级的计算)

computeOomAdjLocked (1) - 进行特殊检查

这一步主要是对进程优先级更新时做一次检查;可以看到adj是进程优先级的主要衡量值

  • 检查当前adj是否已经被计算
  • 检查当前进程是否已经死亡
  • 检查当前进程是否被设置了adj下限

computeOomAdjLocked (2) - 根据四大组件计算

以上代码对进程中的四大组件做了检查;进程优先级的计算秉承着从最重要的开始计算原则,但是如果在后面存在优先级更高的情况,则会取这其中的最小adj与procState作为最终的进程优先级

状态 adj
是否有前台Activity adj = ProcessList.FOREGROUND_APP_ADJ;
是否在运行intrumentation adj = ProcessList.FOREGROUND_APP_ADJ;
是否正在接收广播 adj = ProcessList.FOREGROUND_APP_ADJ;
是否正在执行Service adj = ProcessList.FOREGROUND_APP_ADJ;
都不符合则暂时为空 adj = cacheAdj;
如果Activity可见 adj = ProcessList.VISIBLE_APP_ADJ;
如果在pause状态 adj = ProcessList.PERCEPTIBLE_APP_ADJ;
如果在stop状态 adj = ProcessList.PERCEPTIBLE_APP_ADJ;

computeOomAdjLocked (3) - 特殊进程

计算优先级的过程中判断了一些特殊进程

状态 adj
检查进程是否为可察觉 adj = ProcessList.PERCEPTIBLE_APP_ADJ;
是否为heavy-weight进程 adj = ProcessList.HEAVY_WEIGHT_APP_ADJ;
是否为home进程 adj = ProcessList.HOME_APP_ADJ;
进程是否之前展示过 adj = ProcessList.PREVIOUS_APP_ADJ;
是否为备份进程 adj = ProcessList.BACKUP_APP_ADJ;

computeOomAdjLocked (4) - Service/Provider绑定

  • 遍历所有运行中的Service,进行计算
  • 对绑定的Service进程优先级进行判断
  • 对绑定的provider进程优先级进行判断
  • 对adj作出最后的调整

优先级计算流程图

总结

  1. 负数的是系统进程, 优先级最高
  2. adj值越低, 进程优先级越高
adj 场景
NATIVE_ADJ -17 native进程,framework中未涉及
SYSTEM_ADJ -16 系统进程,也就是system_server
PERSISTENT_PROC_ADJ -12 系统常驻进程,如telephony, systemui
PERSISTENT_SERVICE_ADJ -11 1) 被系统进程或常驻进程绑定 2) 设置了BIND_ABOVE_CLIENT的flag
FOREGROUND_APP_ADJ 0 1) 当前进程正在显示Activity 2) instrumentation正在运行 3) 正在接收广播 4) 正在运行Service 5) provider被其它进程使用
VISIBLE_APP_ADJ 1 该APP中有可见的Activity
PERCEPTIBLE_APP_ADJ 2 1) Activity正在或已经暂停 2) Activity正在Stop
BACKUP_APP_ADJ 3 备份进程
HEAVY_WEIGHT_APP_ADJ 4 heavy weight进程
SERVICE_ADJ 5 30分钟内Service被启动
HOME_APP_ADJ 6 home进程
PREVIOUS_APP_ADJ 7 运行过上一个显示的Activity
SERVICE_B_ADJ 8 运行Service未显示UI
CACHED_APP_MIN_ADJ 9 空进程最小adj
CACHED_APP_MAX_ADJ 15 app.thread=NULL
UNKNOWN_ADJ 16 adj未知

4. 提升优先级的方式?

让进程的优先级为FOREGROUND_APP_ADJ

1. 当前进程正在显示Activity

注册锁屏广播, 当锁屏时, Activity设置启动模式为singleTask, 启动Activity到栈顶

2. 正在接受广播

静态注册系统广播, 如网络状态广播, 安装卸载广播, 当接收到系统广播, 检查进程是否存活

KeepAliveReceiver

/**
 * @author 邱永恒
 *
 * @time 2018/3/1  13:49
 *
 * @desc
 *  监听系统广播,复活进程
 *  (1) 网络变化广播
 *  (2) 屏幕解锁广播(不能使用静态注册)
 *  (3) 应用安装卸载广播
 *  (4) 开机广播
 */
class KeepAliveReceiver : BroadcastReceiver() {
    companion object {
        val TAG = "KeepAliveReceiver"
    }

    override fun onReceive(context: Context?, intent: Intent?) {
        val action = intent?.action
        Log.d(TAG, "AliveBroadcastReceiver---->接收到的系统广播:" + action!!)
        getNetworkBroadcast(context!!, intent)
        if (SystemUtils.isAppAlive(context, Contants.PACKAGE_NAME)) {
            Log.i(TAG, "AliveBroadcastReceiver---->APP还是活着的")
            return
        }
        val intentAlive = Intent(context, SportActivity::class.java)
        intentAlive.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        context.startActivity(intentAlive)
        Log.i(TAG, "AliveBroadcastReceiver---->复活进程(APP)")
    }

    private fun getNetworkBroadcast(context: Context, intent: Intent) {
        val action = intent.action
        // wifi状态改变
        if (WifiManager.WIFI_STATE_CHANGED_ACTION == action) {
            val wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0)
            when (wifiState) {
                WifiManager.WIFI_STATE_DISABLED -> Toast.makeText(context, "wifi关闭", Toast.LENGTH_SHORT).show()
                WifiManager.WIFI_STATE_ENABLED -> Toast.makeText(context, "wifi开启", Toast.LENGTH_SHORT).show()
                else -> {
                }
            }
        }
        // 连接到一个有效wifi路由器
        if (WifiManager.NETWORK_STATE_CHANGED_ACTION == action) {
            val parcelableExtra = intent.getParcelableExtra<Parcelable>(WifiManager.EXTRA_NETWORK_INFO)
            if (null != parcelableExtra) {
                val networkInfo = parcelableExtra as NetworkInfo
                val state = networkInfo.state
                val isConnected = state == NetworkInfo.State.CONNECTED
                if (isConnected) {
                    Toast.makeText(context, "设备连接到一个有效WIFI路由器", Toast.LENGTH_SHORT).show()
                }
            }
        }
        // 监听网络连接状态,包括wifi和移动网络数据的打开和关闭
        // 由于上面已经对wifi进行处理,这里只对移动网络进行监听(该方式检测有点慢)
        // 其中,移动网络--->ConnectivityManager.TYPE_MOBILE;
        //       Wifi--->ConnectivityManager.TYPE_WIFI
        //       不明确类型:ConnectivityManager.EXTRA_NETWORK_INFO
        if (ConnectivityManager.CONNECTIVITY_ACTION == action) {
            val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            val gprs = manager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
            if (gprs.isConnected) {
                Toast.makeText(context, "移动网络打开", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(context, "移动网络关闭", Toast.LENGTH_SHORT).show()
            }
        }
    }

}

3. 正在运行service

双进程守护, 开启前台service, JobService, service播放无声音频

SportService

package com.qyh.keepalivekotlin.service

import android.annotation.SuppressLint
import android.app.Notification
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import com.qyh.keepalivekotlin.DaemonConnection
import com.qyh.keepalivekotlin.R
import com.qyh.keepalivekotlin.utils.BroadcastManager
import com.qyh.keepalivekotlin.utils.Contants.Companion.TIME_ACTION
import java.util.*

/**
 * @author 邱永恒
 *
 * @time 2018/3/2  16:35
 *
 * @desc ${TODD}
 *
 */
class SportService : Service() {
    private val binder : SportBinder by lazy { SportBinder() }
    private val serviceConnection : SportServiceConnection by lazy { SportServiceConnection() }
    private lateinit var runTimer: Timer
    private var timeHour: Int = 0
    private var timeMin: Int = 0
    private var timeSec: Int = 0
    companion object {
        val TAG = "SportService"
        var startMs: Long = 0L
    }

    override fun onBind(intent: Intent?): IBinder {
        return binder
    }

    override fun onCreate() {
        super.onCreate()
        val builder = Notification.Builder(this)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("运动中")
                .setContentText("正在运动...")
        startForeground(200, builder.build())
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 绑定本地service
        bindService(Intent(this, DaemonService::class.java), serviceConnection, Context.BIND_IMPORTANT)
        Log.d(TAG, "$TAG onStartCommand")
        return START_STICKY
    }

    fun startRunTimer() {
        val task = object : TimerTask() {
            @SuppressLint("SetTextI18n")
            override fun run() {
                // 更新UI
                val ms = System.currentTimeMillis() - startMs
                Log.d(TAG, "时间间隔: ${System.currentTimeMillis()}")
                val hms = ms2HMS(ms)
                BroadcastManager.getInstance().sendBroadcast(TIME_ACTION, hms)
            }
        }
        // 每隔1s更新一下时间
        startMs = System.currentTimeMillis()
        Log.d(TAG, "开始时间: $startMs")
        runTimer = Timer()
        runTimer.schedule(task, 0, 1000)
    }

    fun ms2HMS(ms: Long): String {
        val m = ms / 1000
        val hour = m / 3600
        val mint = m % 3600 / 60
        val sed = m % 60
        return String.format("%02d:%02d:%02d", hour, mint, sed)
    }

    @SuppressLint("SetTextI18n")
    fun stopRunTimer() {
        runTimer.cancel()
        startMs = 0
        timeHour = 0
        timeMin = 0
        timeSec = 0

        val ms2HMS = ms2HMS(0)
        BroadcastManager.getInstance().sendBroadcast(TIME_ACTION, ms2HMS)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "$TAG onDestroy")
        startService(Intent(applicationContext, SportService::class.java))
    }

    inner class SportBinder : DaemonConnection.Stub() {
        override fun startRunTimer() {
            [email protected]()
        }

        override fun stopRunTimer() {
            [email protected]()
        }

        override fun getProcessName(): String {
            return "SportService"
        }
    }

    inner class SportServiceConnection : ServiceConnection {
        override fun onServiceDisconnected(name: ComponentName?) {
            [email protected](Intent(this@SportService, DaemonService::class.java))
            [email protected](Intent(this@SportService, DaemonService::class.java), serviceConnection, Context.BIND_IMPORTANT)
        }

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val daemonConnection = DaemonConnection.Stub.asInterface(service)
            Log.d(TAG, "连接远程service: ${daemonConnection.processName}")
        }
    }
}

DaemonService

package com.qyh.keepalivekotlin.service

import android.app.Notification
import android.app.NotificationManager
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import com.qyh.keepalivekotlin.DaemonConnection
import com.qyh.keepalivekotlin.R

/**
 * @author 邱永恒
 *
 * @time 2018/3/1  8:53
 *
 * @desc 后台守护service, 这个Service尽量要轻,不要占用过多的系统资源,否则系统在资源紧张时,照样会将其杀死
 *
 */
class DaemonService : Service() {
    private val binder : DaemonBinder by lazy { DaemonBinder() }
    private val serviceConnection : DaemonServiceConnection by lazy { DaemonServiceConnection() }
    companion object {
        val TAG = "DaemonService"
        val NOTICE_ID = 100
    }

    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 绑定本地service
        bindService(Intent(this, SportService::class.java), serviceConnection, Context.BIND_IMPORTANT)
        // 启动前台进程
        startForeground()
        return START_STICKY
    }

    private fun startForeground() {
        val builder = Notification.Builder(this)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("程序保活")
                .setContentText("守护service正在运行中...")
        startForeground(NOTICE_ID, builder.build())
    }

    override fun onDestroy() {
        super.onDestroy()
        // 取消状态栏广播
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.cancel(NOTICE_ID)
    }

    class DaemonBinder : DaemonConnection.Stub() {
        override fun startRunTimer() {

        }

        override fun stopRunTimer() {

        }

        override fun getProcessName(): String {
            return "DaemonService"
        }
    }

    inner class DaemonServiceConnection : ServiceConnection {
        override fun onServiceDisconnected(name: ComponentName?) {
            [email protected](Intent(this@DaemonService, SportService::class.java))
            [email protected](Intent(this@DaemonService, SportService::class.java), serviceConnection, Context.BIND_IMPORTANT)
        }

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val daemonConnection = DaemonConnection.Stub.asInterface(service)
            Log.d(TAG, "连接本地service: ${daemonConnection.processName}")
        }
    }
}

AIDL

// DeamonConnection.aidl
package com.qyh.keepalivekotlin;

// Declare any non-default types here with import statements

interface DaemonConnection {
    String getProcessName();
    void startRunTimer();
    void stopRunTimer();
}

keepalive-kotlin's People

Contributors

qiu-yongheng avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.