Android AccessibilityService 实现自动发送微信消息的功能。
最近项目上做了这么一个功能,实现微信消息的自动发送功能。
一顿google后,发现Android提供了辅助服务的方式,可以实现这个功能红包插件想必大家都听过或者使用过,原理实际上也是通过AccessibilityService来实现的。
原理
AccessibilityService运行在后台,并且能够收到由系统发出的一些事件(AccessibilityEvent,这些事件表示用户界面一系列的状态变化),比如焦点改变,输入内容变化,按钮被点击了等等,该种服务能够请求获取当前活动窗口并查找其中的内容.换言之,界面中产生的任何变化都会产生一个时间,并由系统通知给AccessibilityService.这就像监视器监视着界面的一举一动,一旦界面发生变化,立刻发出警报.
参考:https://www.jianshu.com/p/4cd8c109cdfb
http://www.android-doc.com/reference/android/accessibilityservice/AccessibilityService.html
本文主要介绍的是如何通过AccessibilityService实现自动发送微信的功能。
继承AccessibilityService,编写自己的服务类,必须重写onAccessibilityEvent方法,通过这个方法来监听微信界面的变化,还有onInterrupt方法,当AccessibilityService被中断时会调用这个方法。
public class AutoSendMsgService extends AccessibilityService {
/**
* 必须重写的方法,响应各种事件。
*
* @param event
*/
@Override
public void onAccessibilityEvent(final AccessibilityEvent event) {
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
String currentActivity = event.getClassName().toString();
if (hasSend) {
return;
}
if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI)) {
handleFlow_LaunchUI();
} else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CONTACTINFOUI)) {
handleFlow_ContactInfoUI();
} else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CHATUI)) {
handleFlow_ChatUI();
}
}
break;
}
}
@Override
public void onInterrupt() {
}
}
在清单文件中声明我们的服务类,配置好服务所需参数。
<service
android:name=".AutoSendMsgService"
android:enabled="true"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/auto_reply_service_config"/>
</service>
服务参数通过auto_reply_service_config文件配置,此文件位于res->xml目录下。
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_description"
android:notificationTimeout="100"
//表示监听微信
android:packageNames="com.tencent.mm" />
AccessibilityService 提供两种方式获取界面上的View信息。
a,findAccessibilityNodeInfosByText 通过界面上的文本获取对应的View
b,findAccessibilityNodeInfosByViewId 通过View的id来获取对应的View
这两种方式都会在本文中使用。
自动发送微信步骤
手动开启辅助服务->打开微信->点击通讯录->向下滑动通讯录找到对应的联系人->点击联系人进入到联系人信息界面->点击发消息按钮进入聊天界面->文本框中粘贴需要发送的文本信息->点击发送按钮发送
我们来一步步介绍具体过程
第一步,手动开启辅助服务
辅助服务不能通过代码动态启动,只能手动去设置->无障碍->辅助服务->找到我们的辅助服务->开启,如果不开启,辅助服务是不会生效的,可以通过代码跳转到设置页面
Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);
第二步,启动微信
Intent intent = new Intent();
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
intent.setClassName(WeChatTextWrapper.WECAHT_PACKAGENAME, WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI);
startActivity(intent);
第三步,微信打开之后,AccessibilityService 会回调onAccessibilityEvent方法,eventType会发生改变,AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED代表当前界面的状态发生了变化,通过 event.getClassName().toString()可以获取当前Activity的类名,当前类名如果是“com.tencent.mm.ui.LauncherUI”,说明微信已经来到进入到了首页。此时,我们就可以点击通讯录进行下一步操作了。
if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI)) {
handleFlow_LaunchUI();
}
private void handleFlow_LaunchUI() {
try {
//点击通讯录,跳转到通讯录页面
WechatUtils.findTextAndClick(this, "通讯录");
Thread.sleep(50);
//再次点击通讯录,确保通讯录列表移动到了顶部,微信如果本来就是打开的状态,通讯录可能位于其他位置,从顶部开始查找,这样就能确保遍历到了所有联系人。
WechatUtils.findTextAndClick(this, "通讯录");
Thread.sleep(200);
//遍历通讯录联系人列表,查找联系人
AccessibilityNodeInfo itemInfo = TraversalAndFindContacts();
if (itemInfo != null) {
WechatUtils.performClick(itemInfo);
} else {
SEND_STATUS = SEND_FAIL;
resetAndReturnApp();
}
} catch (Exception e) {
e.printStackTrace();
}
}
第四步,遍历查找通讯录主要是放在TraversalAndFindContacts方法中进行的。通讯录界面本身是一个listview,主要流程是,通过listview的id来得到listview对象,id的可以通过andorid SDK提供的uiautomatorviewer工具获取,使用方法自行google ,AccessibilityService只能拿到屏幕上可见的信息,不能取到不可见的listview item信息,遍历当前屏幕上的listview的item也就是联系人信息,看看是不是能找到联系人,如果找不到,就执行向下翻页操作,直到滚动到底部为止,如果中途找到,直接进入下一步,如果找不到,那么微信就发送失败。
private AccessibilityNodeInfo TraversalAndFindContacts() {
if (allNameList != null) allNameList.clear();
//获取窗体内容
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
//通过id找到listview对象
List<AccessibilityNodeInfo> listview = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_LISTVIEW_ID);
//是否滚动到了底部
boolean scrollToBottom = false;
if (listview != null && !listview.isEmpty()) {
while (true) {
//获取当前屏幕上的联系人信息
List<AccessibilityNodeInfo> nameList = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_NAME_ID);
List<AccessibilityNodeInfo> itemList = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_ITEM_ID);
if (nameList != null && !nameList.isEmpty()) {
for (int i = 0; i < nameList.size(); i++) {
if (i == 0) {
//必须在一个循环内,防止翻页的时候名字发生重复
mRepeatCount = 0;
}
//item代表listview的item,因为
AccessibilityNodeInfo itemInfo = itemList.get(i);
AccessibilityNodeInfo nodeInfo = nameList.get(i);
String nickname = nodeInfo.getText().toString();
Log.d(TAG, "nickname = " + nickname);
//判断是不是要发送的联系人
if (nickname.equals(WechatUtils.NAME)) {
return itemInfo;
}
if (!allNameList.contains(nickname)) {
allNameList.add(nickname);
} else if (allNameList.contains(nickname)) {
Log.d(TAG, "mRepeatCount = " + mRepeatCount);
//判断是不是已经滚动到了底部的方法,这个方法其实有问题,如果微信通信录中有多个重复的联系人,这个判断方法会失效。目前没有找到更好的判断是否滑动到了底部的方法,欢迎各位大神沟通交流。
if (mRepeatCount == 3) {
//表示已经滑动到顶部了
if (scrollToBottom) {
Log.d(TAG, "没有找到联系人");
//此次发消息操作已经完成
hasSend = true;
return null;
}
scrollToBottom = true;
}
mRepeatCount++;
}
}
}
if (!scrollToBottom) {
//向下滚动
listview.get(0).performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
} else {
return null;
}
//必须等待,因为需要等待滚动操作完成
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return null;
}
第五步、如果找到了联系人,点击联系人,到联系人信息页面,此时onAccessibilityEvent会回调,窗体信息发生变化,通过handleFlow_ContactInfoUI方法处理这个消息,此方法比较简单,找到页面上的发消息按钮,点击进入下一步聊天页面。
else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CONTACTINFOUI)) {
handleFlow_ContactInfoUI();
}
private void handleFlow_ContactInfoUI() {
WechatUtils.findTextAndClick(this, "发消息");
}
第六步,同上,进入到聊天页面onAccessibilityEvent会回调,通过handleFlow_ChatUI方法处理此事件。
这个方法的逻辑:判断聊天对象是不是正确的,因为可能我们打开微信的时候,微信已经处于聊天界面中了,此时需要退到主页,再重新执行前面的操作,直到找到正确的联系人即可。WechatUtils.findViewByIdAndPasteContent,这个方法用来向文本框中粘贴需要发送的信息,sendContent方法,点击发送按钮,发送消息。
还有种情况是,聊天页面可能处于语音状态,这样就需要点击切换文本按钮,切换回发送文本状态,才能找到文本框并且粘贴信息,否则会找不到文本框,消息发送失败。
else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CHATUI)) {
handleFlow_ChatUI();
}
private void handleFlow_ChatUI() {
//如果微信已经处于聊天界面,需要判断当前联系人是不是需要发送的联系人
String curUserName = WechatUtils.findTextById(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_USERNAME_ID);
if (!TextUtils.isEmpty(curUserName) && curUserName.equals(WechatUtils.NAME)) {
if (WechatUtils.findViewByIdAndPasteContent(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_EDITTEXT_ID, WechatUtils.CONTENT)) {
sendContent();
} else {
//当前页面可能处于发送语音状态,需要切换成发送文本状态
WechatUtils.findViewIdAndClick(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_SWITCH_ID);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (WechatUtils.findViewByIdAndPasteContent(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_EDITTEXT_ID, WechatUtils.CONTENT)) {
sendContent();
}
}
} else {
//回到主界面
WechatUtils.findViewIdAndClick(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_BACK_ID);
}
}
至此,微信发送完毕,细心的朋友可能已经发现所有的点击,粘贴,查找文本信息等信息等操作都放在WechatUtils这个类中进行的,这是我封装的进行服务操作动作的一个类,主要是方便管理和复用。
贴一个找到文本并且进行点击操作的代码,其他方法感兴趣的自行下载Demo查看。
public static void findTextAndClick(AccessibilityService accessibilityService, String text) {
AccessibilityNodeInfo accessibilityNodeInfo = accessibilityService.getRootInActiveWindow();
if (accessibilityNodeInfo == null) {
return;
}
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text);
if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
if (nodeInfo != null && (text.equals(nodeInfo.getText()) || text.equals(nodeInfo.getContentDescription()))) {
performClick(nodeInfo);
break;
}
}
}
}
Demo地址:https://github.com/Clearlee/AutoSendWeChatMsg
推荐阅读
-
Android AccessibilityService 实现自动发送微信消息的功能。
-
如何在Android上自创一个微信聊天机器人:利用Xposed框架打造《微信聊天精灵》,实现按关键字自动回复功能
-
Python脚本实现企业微信自动发送消息
-
Python脚本实现企业微信消息发送,包括图片、语音、视频和普通文件的发送规则
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。
-
微信小程序使用scroll-view标签实现自动滑动到底部功能的实例代码
-
使用VBScript实现自动发送消息的微信功能
-
在微信小程序中实现弹幕发送的视频播放器功能