本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
0x1、引言
Hi,我是杰哥,在上一节《AccessibilityService实战-微信僵尸老友检测》中带咱们利用所学的AccessibilityService基础知识,借鉴实在老友假转账的原理,实现了自己的专属微信僵尸老友检测东西。相信仔细学完的读者对于自定义无障碍服务的开发流程都了然于胸,今后随手写个自动化小东西估摸着也是手到擒来了~

本节主要是拾遗,补充两点锦上添花的小细节:AccessibilityService实战的保活与防护。不哔哔,直接开始~
0x2、无障碍服务保活

运用保活,陈词滥调的话题了,最早能够追溯到7年前的一个库 MarsDaemon,双进程看护,简略装备几行代码,即可实现进程常驻。
不过好景不长,Android 8.0后这个库就废掉了,后面陆续出现了很多保活的 骚操作,如 1个像素的Activity、播映无声音频 等。
// 例:1像素的Activity
class OnePxActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.attributes = window.apply { setGravity(Gravity.START or Gravity.TOP) }
.attributes.apply {
width = 1
height = 1
x = 1
y = 1
}
}
}
当然这些骚操作,并不太通用靠谱,究竟哪个厂商的底层不魔改一下,每家都有自己的一套办理体系。说句大真话:终极保活的技巧便是钞才能——花钱进厂商白名单。

没有钞才能也不要紧,有一些通用可行的小技巧,能够提高你的APP的优先级,降低进程被杀的概率~
① 前台服务
把本来处于后台运转AccessibilityService设置为前台服务,需求在AndroidManifest.xml清单文件中声明下述权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
不然会报反常:
java.lang.SecurityException: Permission Denial:
startForeground from pid=2345, uid=10395 requires android.permission.FOREGROUND_SERVICE.
接着在 onCreate()
办法中创立Notification途径,并敞开前台服务,在 onDestory()
办法中中止前台服务,直接给出东西代码,读者按需修正即可:
class ClearCorpseAccessibilityService : AccessibilityService() {
...
override fun onCreate() {
super.onCreate()
// 创立Notification途径,并敞开前台服务
createForegroundNotification()?.let { startForeground(1, it) }
}
override fun onDestroy() {
// 中止前台服务
stopForeground(true)
super.onDestroy()
}
private fun createForegroundNotification(): Notification? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
// 创立告诉途径,一定要写在创立显示告诉之前,创立告诉途径的代码只要在第一次履行才会创立
// 今后每次履行创立代码检测到该途径已存在,因而不会重复创立
val channelId = "前台告诉id名,任意"
notificationManager?.createNotificationChannel(
NotificationChannel(
channelId,
"前台告诉称号,任意",
NotificationManager.IMPORTANCE_HIGH // 发送告诉的等级,此处为高
).apply {
// 下述都是非必要的,看自己需求装备
enableLights(true) // 假如设备有指示灯,敞开指示灯
lightColor = Color.GREEN // 设置指示灯色彩
enableVibration(true) // 敞开轰动
vibrationPattern = longArrayOf(100, 200, 300, 400) // 设置轰动频率
setShowBadge(true) // 是否显示角标
setBypassDnd(true) // 是否绕过免打扰形式
lockscreenVisibility = Notification.VISIBILITY_PRIVATE // 是否在锁屏屏幕上显示此频道的告诉
}
)
return NotificationCompat.Builder(this, channelId)
// 设置点击notification跳转,比方跳转到设置页
.setContentIntent(
PendingIntent.getActivity(
this,
0,
Intent(this, SettingActivity::class.java),
FLAG_IMMUTABLE
)
)
.setSmallIcon(R.drawable.ic_service_enable) // 设置小图标
.setContentTitle("告诉标题")
.setContentText("告诉内容")
.setTicker("告诉提示语")
.build()
}
return null
}
...
}
运转后,在顶部告诉栏能够看到前台服务的Notification:

② 撤销电池优化约束
Android 6.0后为了省电,增加了休眠形式,体系待机一段时刻后会杀死后台正常运转的进程,但体系会有一个 后台运转白名单。
前期的原生体系中,顺次点击:设置 → 电池 → 电池优化 → 未优化运用,能够看到这个白名单。
而在后续的体系中(如我的Android 10),得去 运用和告诉 找:找到自己的运用 → 点击电池 → 后台约束:

接着是:判别APP是否受电池优化约束 和 请求撤销电池优化约束 的东西代码:
// 需在AndroidManifest.xml中增加下述权限
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
// 判别APP是否被约束
@RequiresApi(api = Build.VERSION_CODES.M)
private fun isIgnoringBatteryOptimizations() =
(getSystemService(Context.POWER_SERVICE) as PowerManager)
.isIgnoringBatteryOptimizations(packageName)
// 请求撤销约束
@RequiresApi(api = Build.VERSION_CODES.M)
fun requestIgnoreBatteryOptimizations() {
try {
startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:$packageName")
})
} catch (e: Exception) {
e.printStackTrace()
}
}
请求时会有这样的弹窗 (不同手机体系各有差异):

③ 引导用户敞开自发动
先是判别运用 是否敞开自发动权限,很惋惜,笔者并没有找到 通用且公开的API,只找到有人经过 反射办法取得的,测验了一下,并不靠谱。
所以一个折中的计划:存一个自发动引导页是否打开的符号,弹之前判别下弹过没,没弹过就弹,弹过就不弹,假如弹了就修正下符号。
接着是跳转到设置页,由于厂商对体系的不同定制,导致 敞开自发动 (有些都不叫这个姓名) 的设置进口就五花八门,所以需求开发者依据不同品牌机型自行适配。
先判别是哪家的手机,然后跳转对应的设置页,笔者依据网上的几篇文章,简略地整理了一下 (因笔者测验机有限,未能全部测验覆盖,不对的欢迎谈论区提出):
object RomUtil {
// 体系名
const val ROM_MIUI = "MIUI" // 小米
const val ROM_EMUI = "EMUI" // 华为
const val ROM_OPPO = "OPPO" // OPPO
const val ROM_VIVO = "VIVO" // VIVO
const val ROM_SMARTISAN = "SMARTISAN" // 锤子
const val ROM_FLYME = "FLYME" // 魅族
const val ROM_QIKU = "QIKU" // 360
// 对应体系有的特点
private const val KEY_VERSION_MIUI = "ro.miui.ui.version.name"
private const val KEY_VERSION_EMUI = "ro.build.version.emui"
private const val KEY_VERSION_OPPO = "ro.build.version.opporom"
private const val KEY_VERSION_SMARTISAN = "ro.smartisan.version"
private const val KEY_VERSION_VIVO = "ro.vivo.os.version"
// getprop指令去体系build.prop查找是否有对应特点来判别
private fun getProp(name: String): String? {
val line: String?
var input: BufferedReader? = null
try {
val process = Runtime.getRuntime().exec("getprop $name")
input = BufferedReader(InputStreamReader(process.inputStream), 1024)
line = input.readLine()
input.close()
} catch (ex: IOException) {
Log.e(TAG, "Unable to read prop $name", ex)
return null
} finally {
if (input != null) {
try {
input.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
return line
}
// 判别体系的办法
private fun check(rom: String): Boolean {
val tempRom: String?
if (!getProp(KEY_VERSION_MIUI).isNullOrBlank()) {
tempRom = ROM_MIUI
} else if (!getProp(KEY_VERSION_EMUI).isNullOrBlank()) {
tempRom = ROM_EMUI
} else if (!getProp(KEY_VERSION_OPPO).isNullOrBlank()) {
tempRom = ROM_OPPO
} else if (!getProp(KEY_VERSION_VIVO).isNullOrBlank()) {
tempRom = ROM_VIVO
} else if (!getProp(KEY_VERSION_SMARTISAN).isNullOrBlank()) {
tempRom = ROM_SMARTISAN
} else {
val version = Build.DISPLAY
tempRom = if (version.uppercase().contains(ROM_FLYME)) {
ROM_FLYME
} else {
Build.MANUFACTURER.uppercase()
}
}
return rom == tempRom
}
fun isXiaomi() = check(ROM_MIUI)
fun isHuawei() = check(ROM_EMUI)
fun isVivo() = check(ROM_VIVO)
fun isOppo() = check(ROM_OPPO)
fun isFlyme() = check(ROM_FLYME)
fun is360() = check(ROM_QIKU) || check("360")
fun isSmartisan() = check(ROM_SMARTISAN)
// 打开自发动设置页
fun openStart(context: Context) {
if (Build.VERSION.SDK_INT < 23) return
var intent = Intent()
var componentName: ComponentName? = null
when {
isXiaomi() -> {
componentName = ComponentName(
"com.miui.securitycenter",
"com.miui.permcenter.autostart.AutoStartManagementActivity"
)
}
isHuawei() -> {
componentName = ComponentName(
"com.huawei.systemmanager",
"com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"
)
}
isOppo() -> {
componentName = if (Build.VERSION.SDK_INT >= 26) {
ComponentName(
"com.coloros.safecenter",
"com.coloros.safecenter.startupapp.StartupAppListActivity"
)
} else {
ComponentName(
"com.color.safecenter",
"com.color.safecenter.permission.startup.StartupAppListActivity"
)
}
}
isVivo() -> {
componentName = if (Build.VERSION.SDK_INT >= 26) {
ComponentName(
"com.vivo.permissionmanager",
"com.vivo.permissionmanager.activity.PurviewTabActivity"
)
} else {
ComponentName(
"com.iqoo.secure",
"com.iqoo.secure.ui.phoneoptimize.SoftwareManagerActivity"
)
}
}
isFlyme() -> {
componentName = ComponentName.unflattenFromString(
"com.meizu.safe/.permission.PermissionMainActivity"
)
}
else -> {
if (Build.VERSION.SDK_INT >= 9) {
intent.action = "android.settings.APPLICATION_DETAILS_SETTINGS";
intent.data = Uri.fromParts("package", context.packageName, null);
} else if (Build.VERSION.SDK_INT <= 8) {
intent.action = Intent.ACTION_VIEW
intent.setClassName(
"com.android.settings",
"com.android.settings.InstalledAppDetails"
);
intent.putExtra(
"com.android.settings.ApplicationPkgName",
context.packageName
)
}
intent = Intent(Settings.ACTION_SETTINGS)
}
}
componentName?.let { intent.setComponent(it) }
try {
context.startActivity(intent)
} catch (e: Exception) {
// 抛出反常的话直接打开设置页
context.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
}
④ 引导用户在多任务列表窗口加锁
如题,引导用户对 多任务列表的APP窗口加锁,这样点击将清理加速时不会导致运用被杀,如:

别的,还有一个骚操作:在多任务列表把App窗口给隐藏了,避免用户手多划掉,东西代码如下:
fun Context.hideAppWindow(isHide: Boolean) {
try {
(getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
.appTasks[0].setExcludeFromRecents(isHide)
} catch (e: Exception) {
//...
}
}
⑤ 引导用户打开APP后台高耗电开关
部分厂商的手机有这个(如Vivo),设置办法:设置 → 电池 → 后台高耗电 → 找到自己的APP敞开
0x3、无障碍服务防护
在一开始学习AccessibilityService的时候就说到过,这个服务设计的初衷是:为了帮助残障人士能够更好的运用App。

而在国内一些开发者利用它 能监控与操作其它APP的特性 + 体系远超人类的反应速度,在某些竞赛类场景开发出了 做弊外挂,如抢单、秒杀等,对本来公正的竞赛环境产生不公。
作为一名一般的Android开发者,仍是要居安思危,指不定哪天自己开发的APP也会惨招毒手,提早了解一些AccessibilityService的防护办法,不至于真产生时手足无措~
① 检测用户是否装置外挂软件
树立外挂软件黑名单,PackageManager遍历手机已装置的APP,判别是有有黑名单里的包名和运用名,有给个提示,然后退出APP。
但,这需求权限,而且触及到了隐私,所以,能够尝试换个思路 → 检测监控包名的AccessibilityService
能够经过 AccessibilityManagerService
获取所有已装置及已发动的AccessibilityService运用,而它是com.android.server.accessibility包下的类,无法直接运用。但能够经过 AccessibilityManager
来直接操作(Binder)。提供了两个获取 List<AccessibilityServiceInfo>
的办法:
- getInstalledAccessibilityServiceList() → 取得所有已装置的AccessibilityService;
- getEnabledAccessibilityServiceList() → 取得所有已发动的AccessibilityService;
以第一个获取办法为例,写出遍历的东西代码:
// 取得正在监控方针包名的AccessibilityService
fun getInstalledAccessibilityServiceList(targetPackage: String): List<AccessibilityServiceInfo> {
val serviceList = arrayListOf<AccessibilityServiceInfo>()
val manager =
applicationContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
?: return serviceList
val infoList = manager.installedAccessibilityServiceList
if (infoList.isNullOrEmpty()) return serviceList
infoList.forEach {
if (it.packageNames == null) serviceList.add(it) else {
it.packageNames.forEach { pkgName ->
if (targetPackage == pkgName) serviceList.add(
it
)
}
}
}
return serviceList
}
简略调用下 (检测监听微信的无障碍服务有哪些):
getInstalledAccessibilityServiceList("com.tencent.mm").forEach { info ->
logD(
" \n【监听的包名 (null代表所有)】${info.packageNames?.toList()}\n【监听的服务】${info.id}\n【设置页】${info.settingsActivityName}\n【服务描述信息】${
info.description.replace("\n", "").replace(" ", "")
}"
)
}
运转后控制台输出信息如下 (注:info.packageNames为null表示监控所有包名):

能够看到手机装置的所有监听微信的无障碍服务App信息都被打印出来了,接着便是查看这儿面有没有外挂黑名单里的包名了。
至于检测机遇,能够守时或许在特守时刻节点进行,尽量别只在App发动时,究竟用户能够先发动App然后再打开外挂。别的,检测时也能够顺带把觉得能够的App信息也上签到后台,用于完善黑名单。
当然,这种 检测到就不给用的策略 有些过于粗犷,有时还可能造成误伤,究竟运用包名只要不上架商场,随意起啊,你封一次我改一次,所有还得从App自身触发去防护~

② 重写TextView的findViewsWithText()屏蔽案牍查看
咱们知道AccessibilityServices中定位节点的两种惯例办法,一个是id,一个是依据text文本,后者 findAccessibilityNodeInfosByText()
最终调用的实践是View的 findViewsWithText()
。只需对这个办法进行重写即可屏蔽案牍查看,代码示例如下:
class DefensiveTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
AppCompatTextView(context, attrs) {
override fun findViewsWithText(
outViews: ArrayList<View>?,
searched: CharSequence?,
flags: Int
) {
outViews?.remove(this)
}
}
③ 屏蔽点击事件
上面是屏蔽找的,接着是屏蔽点击时刻的,由于AccessibilityServices履行点击最终会调用View的OnClickListener回调onClick()。所以,一种最直接的办法便是自定义View,然后 用onTouch()替换onClick()。
除此之外还有别的一种办法 → 重写performAccessibilityAction()返回true,以此疏忽掉AccessibilityService传递过来的事件。实现办法的话,除了自定义View重写外,还能够调用 setAccessibilityDelegate()
对控件进行设置,直接给出设置的扩展代码,用时直接调就好:
// 控件是否屏蔽无障碍相关
fun View.disableAccessibility(disable: Boolean = true) {
if (!disable) {
this.accessibilityDelegate = null
} else {
this.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun performAccessibilityAction(
host: View?,
action: Int,
args: Bundle?
): Boolean {
// performAction办法触发的行为,拦截View响应无障碍服务模仿事件的API
return true
}
override fun sendAccessibilityEvent(host: View?, eventType: Int) {
// 篡改或屏蔽View发送的无障碍事件
}
override fun onInitializeAccessibilityEvent(
host: View?,
event: AccessibilityEvent?
) {
// 阻止View生成AccessibilityNodeInfo, 然后防止无障碍抓取到内容
}
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfo?
) {
// 阻止View发送出去的AccessibilityEvent
}
override fun dispatchPopulateAccessibilityEvent(
host: View?,
event: AccessibilityEvent?
): Boolean {
// 阻止 AccessibilityEvent 向子 View 传递
return false
}
override fun onRequestSendAccessibilityEvent(
host: ViewGroup?,
child: View?,
event: AccessibilityEvent?
): Boolean {
// 阻止子View请求发送无障碍事件音讯
return false
}
}
}
}
// 调用处:
button.disableAccessibility()
主要是重写setAccessibilityDelegate(),其它计划可按需增删~

对了,泼个冷水哈,上述两种屏蔽办法,都能够经过上一节教的 手势模仿点击 来破解~
④ 自动发送Event搅扰
咱们都知道AccessibilityServices的玩法其实便是:监听方针APP发出的AccessibilityEvent来履行相应操作。
而在APP里,其实能够调用View的 sendAccessibilityEvent()
来自动发送Event,所以一种防护的思路便是闲来无事发几个Event,尝试搅扰外挂程序的正常逻辑。代码示例如:
textview.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
button.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOWS_CHANGED)
不过,这个操作其实有些鸡肋,究竟收到Event后都是检测页面是否有特定因素,然后再履行下一步的。
道高一尺,魔高一丈,上面说到的防护技巧都是有办法绕过的,比方你屏蔽了案牍查看,那我就OCR文字辨认,乃至依据图片匹配。个人感觉还得是 风控,收集用户操作记录,检测到反人类的反常行为时告警,如每次点击都是点一个坐标,如页面操作时刻超短等等。
0x4、小结
本节对 AccessibilityService保活和防护 相关进行了学习,相信咱们学完也会有所裨益。关于AccessibilityService的知识点,就差一篇 源码解读 了,但不影响咱们学习开发自动化脚本,所以将会在本专栏末尾进行讲解。而下节会讲一下运用 AccessibilityService 的最佳拍档 —— Android悬浮框,它两的关系可谓是:吃面不吃蒜,香味少一半。敬请期待~

参考文献:
-
AutoStartUtil【打开自发动设置界面】
-
AccessibilityService剖析与防护
-
专栏:Android 无障碍相关