摘要

本文研究并实现了一种通过 TCP 网络通信远程控制 Android AccessibilityService(无障碍服务)的方法,以实现高鲁棒性的真机群控自动化操作。系统利用 Node.js 作为客户端,与运行在 Android 设备上的 AccessibilityService 建立长连接。通过发送结构化 JSON 指令,控制设备执行节点检索模拟点击手势滑动全局返回等动作。针对实际群控中遇到的核心痛点——小米(MIUI/澎湃OS)后台温控强杀开机 FBE(文件级加密)锁死无障碍系统广播拦截以及 UI 状态显示滞后等,本文提供了整套深度自愈与性能优化方案,实现了一套低消耗高生存率的工业级群控自动化框架。

关键词:Android Studio AccessibilityService Node.js 网络通信 自动化 远程控制 进程保活 自愈


1. 引言

在群控与自动化测试领域,传统的自动化框架(如 Appium Weditor)常因依赖繁重的 Python 解释器和频繁的驱动重载,在面对多台设备高发热视频挂机任务时发生假死。Android 原生的 AccessibilityService 拥有直接操纵系统 UI 的最高权限。

直接在手机端编写控制逻辑缺乏灵活性。为了使控制端与执行端解耦,本文提出一种**“执行端轻量化,控制端大脑化”**的架构。通过 TCP 网络协议将控制逻辑转移到 PC 端的 Node.js,不仅极大地提升了脚本编写的灵活性,更通过底层数据库读写避开了安卓系统的权限弹窗拦截。


2. 系统架构

本系统采用 C/S(客户端/服务端)架构设计:

  • Android 执行端 (服务端):常驻手机后台,监听 6000 端口,接收并解析 JSON 格式的指令,调用无障碍引擎执行底层动作。
  • Node.js 控制端 (客户端):运行在控制机,通过 TCP 协议下发动作流。同时,配合云端结构化笔记,将“设备当前状态注册”与“历史异常断开日志”进行隔离归档。

图 1. 数据与控制分流拓扑图

                  +-----------------------------------+
                  |        Node.js 控制端 (PC)        |
                  +-----------------+-----------------+
                                    |
                    [指令下发] TCP  |  [状态/日志审计] HTTP
                                    v
+-----------------------------+     |     +-----------------------------+
|   Android 13/14 物理真机     | <---+---> |       云端结构化笔记         |
|  (AccessibilityService)     |           | - 设备注册表 (NOTE_A: 单行)   |
|  - 6000 监听端口            |           | - 关闭日志表 (NOTE_B: 追加)   |
+-----------------------------+           +-----------------------------+

3. Android 核心服务与自愈机制实现

3.1 核心自愈配置 DeviceRegisterManager.kt

负责当前设备的云端状态表更新,以及在检测到异常断开时向关闭日志表追加精确的时差数据。

package com.example.myaccessibility

import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.net.wifi.WifiManager
import android.os.Build
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.NetworkInterface
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone

object DeviceRegisterManager {

    private const val TAG      = "DeviceRegisterManager"
    private const val BASE_URL = "https://note.yysresume.work"
    
    // 【隔离 Note ID】设备状态注册与关闭历史日志完全隔离,防止混乱
    private const val NOTE_ID_REGISTER = "69419c71-732a-459f-9a1f-73e743eebe7a" // 状态表
    private const val NOTE_ID_EXIT_LOG = "5f3b9b75-ebe0-4d3c-92c4-f59872b1708d" // 异常表
    private const val AUTH_TOKEN      = "a_secret_fixed_token"

    const val PREFS_NAME     = "device_register_log"
    private const val KEY_LAST_ACTIVE = "last_active_time"
    private const val KEY_CLEAN_EXIT  = "clean_exit"

    fun saveHeartbeat(context: Context) {
        val prefs = context.getSharedPreferences(PREFS_NAME Context.MODE_PRIVATE)
        prefs.edit()
            .putLong(KEY_LAST_ACTIVE System.currentTimeMillis())
            .putBoolean(KEY_CLEAN_EXIT false)
            .apply()
    }

    fun markCleanExit(context: Context) {
        val prefs = context.getSharedPreferences(PREFS_NAME Context.MODE_PRIVATE)
        prefs.edit().putBoolean(KEY_CLEAN_EXIT true).apply()
    }

    fun checkAndReportAbnormalExit(context: Context) {
        val prefs = context.getSharedPreferences(PREFS_NAME Context.MODE_PRIVATE)
        val cleanExit = prefs.getBoolean(KEY_CLEAN_EXIT true)

        if (!cleanExit) {
            val lastActive = prefs.getLong(KEY_LAST_ACTIVE 0)
            val lastActiveStr = if (lastActive > 0) {
                val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss" Locale.getDefault()).apply {
                    timeZone = TimeZone.getTimeZone("Asia/Shanghai")
                }
                sdf.format(Date(lastActive))
            } else {
                "未知"
            }
            prefs.edit().putBoolean(KEY_CLEAN_EXIT false).apply()

            Thread {
                runBlocking {
                    val serialNo = getSerialNumber(context)
                    val ip       = getLocalIpAddress(context) ?: "unknown"
                    val model    = "${Build.MANUFACTURER} ${Build.MODEL}".trim()
                    val nowStr   = nowLocalString()
                    val eventMsg = "检测到上一次为非正常退出 (断开时间: $lastActiveStr 恢复拉起时间: $nowStr)"
                    val newLine  = "| $nowStr | $model | $serialNo | $ip | $eventMsg |"
                    appendToNote(newLine)
                }
            }.start()
        } else {
            prefs.edit().putBoolean(KEY_CLEAN_EXIT false).apply()
        }
    }

    fun logExitToCloud(context: Context eventName: String) {
        markCleanExit(context)
        val t = Thread {
            runBlocking {
                val serialNo = getSerialNumber(context)
                val ip       = getLocalIpAddress(context) ?: "unknown"
                val model    = "${Build.MANUFACTURER} ${Build.MODEL}".trim()
                val nowStr   = nowLocalString()
                val eventMsg = if (eventName == "onDestroy") "服务销毁(onDestroy)" else "任务清理(onTaskRemoved)"
                val newLine = "| $nowStr | $model | $serialNo | $ip | $eventMsg |"
                appendToNote(newLine)
            }
        }
        t.start()
        try { t.join(2500) } catch (_: Exception) {}
    }

    private fun appendToNote(text: String): Boolean {
        return try {
            val payload = JSONObject().apply {
                put("noteId"     NOTE_ID_EXIT_LOG)
                put("appendText" text)
                put("updatedAt"  nowUtcIso())
            }.toString()

            val conn = (URL("$BASE_URL/api/note-op").openConnection() as HttpURLConnection).apply {
                requestMethod  = "POST"
                connectTimeout = 15000
                readTimeout    = 15000
                doOutput       = true
                setRequestProperty("Cookie"       "auth_token=$AUTH_TOKEN")
                setRequestProperty("Content-Type" "application/json")
            }
            OutputStreamWriter(conn.outputStream Charsets.UTF_8).use { it.write(payload) }
            val code = conn.responseCode
            conn.disconnect()
            code == 200
        } catch (e: Exception) {
            false
        }
    }

    // (省略获取 Serial IP 等辅助方法,保持与前文一致)
    private fun nowLocalString(): String {
        val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss" Locale.getDefault())
        sdf.timeZone = TimeZone.getTimeZone("Asia/Shanghai")
        return sdf.format(Date())
    }

    private fun nowUtcIso(): String {
        val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'" Locale.US)
        sdf.timeZone = TimeZone.getTimeZone("UTC")
        return sdf.format(Date())
    }
}

3.2 服务端监听 MyAccessibility.java

实现无障碍逻辑,监听 6000 端口并处理连接:

package com.example.myaccessibility

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import androidx.core.app.NotificationCompat
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.ServerSocket
import java.net.Socket
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean

public class MyAccessibility extends AccessibilityService {

    private static final String TAG = "MyAccessibilityService"
    public static final int SERVER_PORT = 6000
    private static final int NOTIFICATION_ID = 1
    private static final String CHANNEL_ID = "MyAccessibilityServiceChannel"
    private static final long SERVICE_CHECK_INTERVAL = 30000

    private final Handler mServiceCheckHandler = new Handler(Looper.getMainLooper())
    private final Runnable mServiceCheckRunnable = this::checkAndRecoverService

    private ServerSocket serverSocket
    private ExecutorService executorService
    private final AtomicBoolean isServiceRunning = new AtomicBoolean(false)
    private final AtomicBoolean isServerRunning = new AtomicBoolean(false)
    private boolean isSystemShuttingDown = false

    private final BroadcastReceiver screenReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context Intent intent) {
            String action = intent.getAction()
            if (Intent.ACTION_SCREEN_ON.equals(action)) {
                checkAndRecoverService()
            } else if (Intent.ACTION_SHUTDOWN.equals(action)) {
                isSystemShuttingDown = true
                DeviceRegisterManager.INSTANCE.markCleanExit(context)
            }
        }
    }

    private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context Intent intent) {
            String action = intent.getAction()
            if (Intent.ACTION_SHUTDOWN.equals(action) || "android.intent.action.QUICKBOOT_POWEROFF".equals(action)) {
                isSystemShuttingDown = true
                DeviceRegisterManager.INSTANCE.markCleanExit(context)
            }
        }
    }

    @SuppressLint("ForegroundServiceType")
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected()
        isServiceRunning.set(true)
        configureAccessibilityService()
        startForeground(NOTIFICATION_ID createMinimalNotification())
        registerScreenReceiver()

        // 注册双路关机广播监听 (适配 Android 14 导出规范)
        IntentFilter shutdownFilter = new IntentFilter()
        shutdownFilter.addAction(Intent.ACTION_SHUTDOWN)
        shutdownFilter.addAction("android.intent.action.QUICKBOOT_POWEROFF")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            registerReceiver(shutdownReceiver shutdownFilter Context.RECEIVER_EXPORTED)
        } else {
            registerReceiver(shutdownReceiver shutdownFilter)
        }

        startServer()
        startServiceCheck()
        DeviceRegisterManager.INSTANCE.checkAndReportAbnormalExit(this)
    }

    private void checkAndRecoverService() {
        DeviceRegisterManager.INSTANCE.saveHeartbeat(this)
        if (!isServiceRunning.get()) {
            configureAccessibilityService()
            isServiceRunning.set(true)
        }
        mServiceCheckHandler.postDelayed(mServiceCheckRunnable SERVICE_CHECK_INTERVAL)
    }

    // (省略 TCP 启动接收 Socket 消息处理及常规 getPageSource 等逻辑,保持前文一致)

    private void startServer() {
        if (isServerRunning.get()) return
        executorService = Executors.newFixedThreadPool(4)
        new Thread(() -> {
            try {
                serverSocket = new ServerSocket(SERVER_PORT)
                serverSocket.setReuseAddress(true)
                isServerRunning.set(true)
                while (isServerRunning.get()) {
                    Socket clientSocket = serverSocket.accept()
                    handleClient(clientSocket)
                }
            } catch (Exception ignored) {}
        }).start()
    }

    private void handleClient(Socket socket) {
        executorService.execute(() -> {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
                 PrintWriter out = new PrintWriter(socket.getOutputStream() true)) {
                String cmd
                while ((cmd = in.readLine()) != null) {
                    // 解析指令并执行无障碍操作
                    out.println("{\"result\": true \"message\": \"executed\"}")
                }
            } catch (Exception ignored) {}
        })
    }

    private void registerScreenReceiver() {
        IntentFilter filter = new IntentFilter()
        filter.addAction(Intent.ACTION_SCREEN_ON)
        filter.addAction(Intent.ACTION_SCREEN_OFF)
        filter.addAction(Intent.ACTION_SHUTDOWN)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            registerReceiver(screenReceiver filter Context.RECEIVER_EXPORTED)
        } else {
            registerReceiver(screenReceiver filter)
        }
    }

    private void startServiceCheck() {
        mServiceCheckHandler.postDelayed(mServiceCheckRunnable SERVICE_CHECK_INTERVAL)
    }

    private void configureAccessibilityService() {
        AccessibilityServiceInfo info = new AccessibilityServiceInfo()
        info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
        info.flags = AccessibilityServiceInfo.DEFAULT | AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
        setServiceInfo(info)
    }

    private Notification createMinimalNotification() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID "服务" NotificationManager.IMPORTANCE_MIN)
            getSystemService(NotificationManager.class).createNotificationChannel(channel)
        }
        return new NotificationCompat.Builder(this CHANNEL_ID)
                .setContentTitle("辅助控制正在运行").setSmallIcon(android.R.drawable.ic_menu_info_details).build()
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {}

    @Override
    public void onInterrupt() {
        isServiceRunning.set(false)
    }

    @Override
    public void onDestroy() {
        if (!isSystemShuttingDown) {
            DeviceRegisterManager.INSTANCE.logExitToCloud(this "onDestroy")
        }
        super.onDestroy()
        isServiceRunning.set(false)
        isServerRunning.set(false)
        try { unregisterReceiver(screenReceiver) } catch (Exception ignored) {}
        try { unregisterReceiver(shutdownReceiver) } catch (Exception ignored) {}
        mServiceCheckHandler.removeCallbacks(mServiceCheckRunnable)
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        DeviceRegisterManager.INSTANCE.logExitToCloud(this "onTaskRemoved")
        super.onTaskRemoved(rootIntent)
    }
}

4. 界面(MainActivity.kt)设计与生命周期优化

4.1 解决“进程挂起引起的状态回弹误差”

在 UI 中直接使用系统的 Settings.Secure 物理状态数据库进行 1:1 精准开关映射,并增加一键点击强制开启功能。同时,结合 Compose 生命周期监听,实现 0 功耗常驻

package com.example.myaccessibility

import android.content.*
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.example.myaccessibility.ui.theme.MyAccessibilityTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAccessibilityTheme(darkTheme = true) {
                Surface(modifier = Modifier.fillMaxSize()) {
                    主界面()
                }
            }
        }
    }
}

@Composable
fun 主界面() {
    val context = LocalContext.current
    var 无障碍已启用 by remember { mutableStateOf(isAccessibilityServiceEnabled(context MyAccessibility::class.java)) }
    
    // 【生命周期联动】:仅在页面返回前台时被动触发检测一次,挂机和后台时 CPU 占用率为 0%
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _ event ->
            if (event == Lifecycle.Event.ON_RESUME) {
                无障碍已启用 = isAccessibilityServiceEnabled(context MyAccessibility::class.java)
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    Column(modifier = Modifier.fillMaxSize()) {
        Button(
            onClick = {
                // 【双重保险】:优先通过 WRITE_SECURE_SETTINGS 权限一键静默强开
                val success = forceEnableAccessibility(context)
                if (success) {
                    无障碍已启用 = true
                    Toast.makeText(context "已强制激活无障碍服务!" Toast.LENGTH_SHORT).show()
                } else {
                    // 降级跳转到系统设置页
                    context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
                }
            }
        ) {
            Text(if (无障碍已启用) "服务运行中" else "未启动(点击开启)")
        }
    }
}

// 强开无障碍核心函数
private fun forceEnableAccessibility(context: Context): Boolean {
    return try {
        val cr = context.contentResolver
        val mySvc = "${context.packageName}/${MyAccessibility::class.java.name}"
        var enabledServices = Settings.Secure.getString(cr Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) ?: ""
        if (!enabledServices.contains(mySvc)) {
            enabledServices = if (enabledServices.isEmpty()) mySvc else "$enabledServices:$mySvc"
            Settings.Secure.putString(cr Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES enabledServices)
        }
        Settings.Secure.putInt(cr Settings.Secure.ACCESSIBILITY_ENABLED 1)
        true
    } catch (e: Exception) {
        false
    }
}

// 基于安全数据库的最可靠状态检测
private fun isAccessibilityServiceEnabled(context: Context serviceClass: Class<*>): Boolean {
    var enabled = 0
    val expectedComponentName = context.packageName + "/" + serviceClass.name
    try {
        enabled = Settings.Secure.getInt(context.contentResolver Settings.Secure.ACCESSIBILITY_ENABLED)
    } catch (_: Exception) {}

    if (enabled == 1) {
        val settingValue = Settings.Secure.getString(context.contentResolver Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
        if (settingValue != null) {
            val splitter = android.text.TextUtils.SimpleStringSplitter(':')
            splitter.setString(settingValue)
            while (splitter.hasNext()) {
                if (splitter.next().equals(expectedComponentName ignoreCase = true)) {
                    return true
                }
            }
        }
    }
    return false
}

5. AndroidManifest.xml 配置与广播拆分优化

在群控系统部署中,我们常会在接收器内增加 <data android:scheme="package" /> 监听覆盖安装。然而,如果把开机广播与包更新广播放在同一个 <intent-filter> 中,会导致开机广播因缺少 Scheme 匹配而被系统全部静默过滤。

必须拆分为两个独立的 <intent-filter>

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

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.MyAccessibility"
        android:usesCleartextTraffic="true">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".MyAccessibility"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:exported="true"
            android:foregroundServiceType="dataSync">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>

        <!-- 开机自启动广播接收器 — 深度拆分过滤器以规避过滤Bug -->
        <receiver
            android:name=".BootReceiver"
            android:enabled="true"
            android:exported="true"
            android:directBootAware="true">
            
            <!-- 过滤器 1:专用于监听系统开机(决不能包含 scheme) -->
            <intent-filter android:priority="1000">
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
            </intent-filter>
            
            <!-- 过滤器 2:专用于监听包覆盖安装(必须包含 scheme) -->
            <intent-filter>
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
                <action android:name="android.intent.action.PACKAGE_REPLACED" />
                <data android:scheme="package" />
            </intent-filter>
        </receiver>

    </application>
</manifest>

(说明:由于篇幅限制,此处省略第 6 节至第 14 节,其具体流程如 Node.js 客户端TCP 数据报文解算及 MIUI 自动化点击详情均保持不变。)


15. 小米手机特有的安全设置前提与后台双恶魔封锁

在搭载 MIUI/澎湃OS 的小米旗舰设备(如小米 13)上,由于温控服务(Joyose)与性能守护进程(PowerKeeper)的强力管控,后台无障碍极易在夜间或高温时被系统暴力注销。

15.1 开启“USB 调试(安全设置)”

必须在开发者选项中开启 “USB 调试(安全设置)”,以允许 ADB 在手机非前台阶段修改底层设置数据库。

15.2 物理屏蔽系统强杀服务

我们在 Node.js 发起配对成功的初始化通信阶段,直接通过 ADB 命令在底层将这两个强杀进程进行冻结挂起。在 client.js 或 Go 端的 setupServiceCoexistence 逻辑中追加如下系统级强控命令:

// 冻结小米电池与性能核心(杀后台主力)
exec("adb shell pm disable-user com.miui.powerkeeper" (err stdout stderr) => {
    if (!err) console.log("✅ [自愈] 成功封锁小米 PowerKeeper 后台强杀恶魔")
})

// 冻结小米云控温控服务
exec("adb shell pm disable-user com.xiaomi.joyose" (err stdout stderr) => {
    if (!err) console.log("✅ [自愈] 成功封锁小米 Joyose 动态温控强杀恶魔")
})

16. 开机自愈启动无障碍服务

开机自动拉起无障碍是设备彻底脱离人工干预(通宵无人值守)的核心闭环逻辑。

16.1 FBE(文件级加密)与 Direct Boot 限制

现代 Android 系统在重启后用户首次解锁屏幕前,出于隐私安全,系统数据库和第三方的私有数据处于完全加密锁死的状态。
通过在 <receiver> 标签中配置 android:directBootAware="true",可以在设备未解锁时唤醒 BootReceiver,并通过特权对系统安全数据库进行直接改写,强制复位总开关。

16.2 极客版 BootReceiver.java 实现

package com.example.myaccessibility

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.util.Log

/**
 * 开机自启动与无障碍服务强制自愈接收器
 */
public class BootReceiver extends BroadcastReceiver {

    private static final String TAG = "BootReceiver"

    @Override
    public void onReceive(Context context Intent intent) {
        String action = intent.getAction()
        Log.i(TAG "收到自愈广播: " + action)

        if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
                Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action) ||
                Intent.ACTION_MY_PACKAGE_REPLACED.equals(action) ||
                Intent.ACTION_PACKAGE_REPLACED.equals(action)) {

            Log.i(TAG "开始执行开机无障碍服务自愈...")

            // 强开系统无障碍总开关
            forceEnableAccessibility(context)

            // 启动前台服务 fallback
            startAccessibilityService(context)

            // 执行开机注册任务
            if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
                    Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
                DeviceRegisterManager.INSTANCE.scheduleRegister(context)
            }
        }
    }

    private void forceEnableAccessibility(Context context) {
        try {
            android.content.ContentResolver cr = context.getContentResolver()
            String mySvc = "com.example.myaccessibility/com.example.myaccessibility.MyAccessibility"

            String enabledServices = Settings.Secure.getString(cr Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
            if (enabledServices == null) {
                enabledServices = ""
            }

            if (!enabledServices.contains(mySvc)) {
                String newServices = enabledServices.isEmpty() ? mySvc : enabledServices + ":" + mySvc
                Settings.Secure.putString(cr Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES newServices)
                Log.i(TAG "已向系统安全数据库中强制追加无障碍服务列表")
            }

            // 强行把开关状态修正为 1 (ON)
            Settings.Secure.putInt(cr Settings.Secure.ACCESSIBILITY_ENABLED 1)
            Log.i(TAG "⚡ [开机自愈] 成功重写系统安全数据库,无障碍强制锁死为 ON 状态!")

        } catch (Exception e) {
            Log.e(TAG "❌ [自愈失败] 权限不足,请确保执行过 adb 授权命令! " + e.getMessage())
        }
    }

    private void startAccessibilityService(Context context) {
        try {
            Intent serviceIntent = new Intent(context MyAccessibility.class)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context.startForegroundService(serviceIntent)
            } else {
                context.startService(serviceIntent)
            }
        } catch (Exception e) {
            Log.e(TAG "自愈辅助启动前台服务失败: " + e.getMessage())
        }
    }
}

 

17. 开启资源裁剪与代码极简瘦身以优化后台挂机稳定性

在实际的多设备通宵群控中,挂机设备的进程稳定性(不被系统清理)是衡量系统好坏的第一标准。传统的 Debug 版本应用(通常在 23MB 左右)由于包含了全套无裁剪的第三方类库调试信息以及大量的布局资源,在运行时的物理内存(PSS)通常会高达 53MB 以上 [1 2]。这很容易使其在内存紧张时被系统列为优先强杀的“重载进程” [2]。

本节将介绍如何通过 Gradle 的 R8 摇树优化(Tree Shaking)与资源物理裁剪(Resource Shrinking),将应用 APK 体积断崖式压缩至 2MB 左右,从而在运行期大幅度降低系统级类加载负载与物理内存,保障后台常驻。

17.1 在 build.gradle.kts 中开启 R8 与资源裁剪

代码压缩(Minification)负责移除无用的代码类和重命名混淆而资源压缩(Resource Shrinking)则负责在打包时自动扫描并物理删除所有第三方依赖库中自带但您在代码中完全没有引用到的图标和 XML 资源 [1]。

请在您的 app/build.gradle.kts 文件中配置如下发布属性:

android {
    // ...
    buildTypes {
        release {
            isMinifyEnabled = true   // 1. 开启 R8 代码混淆与无用代码裁剪 (Tree Shaking)
            isShrinkResources = true // 2. 开启无用资源物理裁剪 (必须与 isMinifyEnabled 协同生效)
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt")
                "proguard-rules.pro"
            )
        }
    }
}

17.2 规避 Compose 图标扩展包的体积深渊

在引入 Jetpack Compose 时,开发者常因图方便而在 dependencies 中添加以下依赖:
implementation("androidx.compose.material:material-icons-extended")

  • 影响分析:该扩展图标包里包含了数千个不常用到的矢量图标,即使开启混淆,其类引用和元数据也会导致 APK 体积无谓膨胀 10MB 以上。
  • 解决对策:请在依赖中彻底删除这一行,仅保留核心包。对于生僻图标,建议手动将 SVG 转换为 XML 矢量图并局部引入,以最大化精简依赖。

17.3 瘦身后的实测性能与内存对比

通过 ./gradlew assembleRelease 编译出极简正式包,并在真机上进行 dumpsys meminfo 物理内存测算,能获得非常震撼的优化结果 [1 2]:

  1. APK 物理包体积:从未优化的 23MB 暴跌至 2.02MB(瘦身超 90%) [1 2]。
  2. 系统级映射开销(System PSS):从 44.3MB 暴降至 19.6MB [2]。
    • 原理解析:由于 R8 对未使用的类进行了彻底的“物理移除”,Android 虚拟机(ART)在启动应用时,需要加载和解析的 DEX 字节码数据极大幅度减少,直接为手机系统节省了 24.7MB 的系统级内存索引负担 [2]。
  3. 整机实际运行内存(TOTAL PSS):从 53.1MB 缩减至 33.5MB [2]。
    • 自愈意义:Android 系统的低内存清理机制(LMK)在扫描后台进程时,会为 PSS 占用较小的应用打出极低的杀进程评分(OOM Score) 。通过此优化,App 会被系统自动归档为**“超轻量绿色安全进程”**,在手机高负载和高热时,会被系统主动跳过清理,极大地提升了通宵无障碍自动化任务的挂机存活率! 

 


18. 结论

本文提出并实现了一种通过 TCP 网络通信远程控制 Android 无障碍辅助服务的自动化方案,并通过多次架构重构和实战演进,解决了工业级真机群控中长期面临的多项稳定性痛点。

通过将 App 运行生命周期(DisposableEffect 监听)与系统底层安全设置(Settings.Secure)深度联动,成功抹平了因系统后台挂起或温控干预导致的状态回弹偏差。利用永久性的 WRITE_SECURE_SETTINGS 特权,辅以 FBE 开机自愈广播的精准分流,实现了设备在通宵运行时无需人工干预断电重启后秒级全自动恢复的闭环管理。

同时,通过在控制机端远程冻结小米系统的温控与杀后台驻留程序(PowerKeeperJoyose),从根源上将手机系统的后台资源控制权重新收归于开发者,实现了极其稳定的群控底座。该方案在实际自动化控制场景中展现出了极低的功耗开销与接近 100% 的通宵生存率,为工业级 Android 自动化系统提供了高参考价值的技术方案。