Android中微信搶紅包插件原理解析及開發(fā)思路
一、前言
自從去年中微信添加搶紅包的功能,微信的電商之旅算是正式開始正式火爆起來。但是作為Android開發(fā)者來說,我們在搶紅包的同時意識到了很多問題,就是手動去搶紅包的速度慢了,當然這些有很多原因導致了?;蛟S是網(wǎng)絡的原因,而且這個也是最大的原因。但是其他的不可忽略的因素也是要考慮到進去的,比如在手機充電鎖屏的時候,我們并不知道有人已經(jīng)開始發(fā)紅包了,那么這時候也是讓我們喪失了一大批紅包的原因。那么關于網(wǎng)絡的問題,我們開發(fā)者可能用相關技術無法解決(當然在Google和Facebook看來的話,他們的理想是能夠在任何地方都能連接互聯(lián)網(wǎng),當然在偏遠的農(nóng)村也是,不過我們期待他們有一天能夠普及開來。到時候才是真正的互聯(lián)網(wǎng))。扯得有點遠了。我們回歸到正題,今天我們來看看使用技術來解決其他非網(wǎng)絡問題。在充電鎖屏的時候也可以自動幫我們搶紅包。而且你要知道,機器搶紅包的準確率是100%的,這個也許就是人和機器的區(qū)別。那么保證搶得準確率是100%的話,那就依賴于我們高效準確的算法實現(xiàn)了。下面就來看看原理實現(xiàn)。
當去年我看到搶紅包那么火爆的時候,當時作為一個開發(fā)者心里是多么渴望開發(fā)一個插件出來,可是當時我們能想到的就是使用:
adb shell monkey
命令去模擬點擊屏幕,但是那種方式有一個問題就是是無頭緒的盲目點擊,所以幾乎會出現(xiàn)誤點,點擊成功率極其低下。所以當時就沒有想到其他方法了,因為最近做了有關輔助功能相關的工作的時候,那么就發(fā)現(xiàn)這個功能可以用于搶紅包。
其實現(xiàn)在我們可以去各大市場搜索一下看到,有很多搶紅包的插件了。當然我們并不是用于商業(yè)化,這里只是來解析一下原理。我們會發(fā)現(xiàn)那些插件都有一個共同的特點是:第一步都是引導用戶去開啟輔助功能。
二、原理解析
關于輔助功能(AccessibilityService),如果又不了解的同學可以去Google一下,這個功能其實很有用的,但是他的出現(xiàn)的出發(fā)點是給那些肢體上有障礙的人使用的,比如手指不健全的用戶,怎么才能滑動屏幕,然后打開一個應用呢?那么輔助功能就是干這些事,他的功能其實就是可以概括兩句話:
第一、尋找到我們想要的View節(jié)點
第二、然后模擬點擊,實現(xiàn)特定功能
我們知道Android中的View體系是一個樹形結構,那么每一個View就是一個節(jié)點。所以我們可以查找到指定的節(jié)點,那么我們該如何查找到我們想要的節(jié)點呢?這里我們先看一下輔助功能(AccessibilityService)的用法
第一步、我們需要集成AccessibilityService類
我們需要自定一個Service然后繼承AccessibilityService,當然還需要在AndroidManifest.xml中聲明這個服務:
第二步、聲明權限和配置
這個服務需要注明一個權限:
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
當然還要一個meta-data的聲明,這個聲明是對這個AccessibilityService的配置。我們看一下配置文件內容:
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/desc" android:notificationTimeout="100" android:packageNames="com.tencent.mm" />
這里我們看到有很多選項,我們看一下常用的幾個屬性:
1、android:accessibilityEventTypes="typeAllMask"
看屬性名也差不多可以明白,這個是用來設置響應事件的類型,typeAllMask當然就是響應所有類型的事件了。當然還有單擊、長按、滑動等。
2、android:accessibilityFeedbackType="feedbackSpoken"
設置回饋給用戶的方式,有語音播出和振動。可以配置一些TTS引擎,讓它實現(xiàn)發(fā)音。
3、android:notificationTimeout="100"
響應時間的設置就不用多說了
4、android:packageNames="com.example.android.apis"
可以指定響應某個應用的事件,這里因為要響應所有應用的事件,所以不填,默認就是響應所有應用的事件。比如我們寫一個微信搶紅包的輔助程序,就可以在這里填寫微信的包名,便可以監(jiān)聽微信產(chǎn)生的事件了。
注意:
1、我們這些配置信息除了在xml中定義,同樣也可以在代碼中定義,我們一般都是在onServiceConnected()方法里進行
@Override protected void onServiceConnected() { AccessibilityServiceInfo info = getServiceInfo(); info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN; info.notificationTimeout = 100; setServiceInfo(info); info.packageNames = new String[]{"xxx.xxx.xxx", "yyy.yyy.yyy","...."}; setServiceInfo(info); super.onServiceConnected(); }
2、這里我們一般都會在這里寫上我們需要監(jiān)聽的應用的包名,但是有時候我們需要監(jiān)聽多個應用,那么這時候我們該怎么辦呢?
這時候我們可以這么做:
第一種:我們在代碼中注冊多個應用的包名,從而可以監(jiān)聽多個應用
@Override protected void onServiceConnected() { AccessibilityServiceInfo info = getServiceInfo(); //這里可以設置多個包名,監(jiān)聽多個應用 info.packageNames = new String[]{"xxx.xxx.xxx", "yyy.yyy.yyy","...."}; setServiceInfo(info); super.onServiceConnected(); }
第二種:我們在onAccessibilityEvent事件監(jiān)聽的方法中做包名的過濾(這種方式最常用)
@Override public void onAccessibilityEvent(AccessibilityEvent event) { String pkgName = event.getPackageName().toString(); if("xxx.xxx.xxx".equals(pkgName)){ }else if("yyy.yyy.yyy".equals(pkgName)){ }else if("....".equals(pkgName)){ } }
第三步、在onAccessibilityEvent方法中監(jiān)聽指定的事件
比如我們需要監(jiān)聽有通知欄消息的事件:
@Override public void onAccessibilityEvent(AccessibilityEvent event) { int eventType = event.getEventType(); switch (eventType) { case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: //....... } }
這個事件類型很多的,我們可以查看AccessibilityEvent類的源碼:
@Deprecated public static final int MAX_TEXT_LENGTH = 500; /** * Represents the event of clicking on a {@link android.view.View} like * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. */ public static final int TYPE_VIEW_CLICKED = 0x00000001; /** * Represents the event of long clicking on a {@link android.view.View} like * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. */ public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002; /** * Represents the event of selecting an item usually in the context of an * {@link android.widget.AdapterView}. */ public static final int TYPE_VIEW_SELECTED = 0x00000004; /** * Represents the event of setting input focus of a {@link android.view.View}. */ public static final int TYPE_VIEW_FOCUSED = 0x00000008; /** * Represents the event of changing the text of an {@link android.widget.EditText}. */ public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010; /** * Represents the event of opening a {@link android.widget.PopupWindow}, * {@link android.view.Menu}, {@link android.app.Dialog}, etc. */ public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020; /** * Represents the event showing a {@link android.app.Notification}. */ public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040; /** * Represents the event of a hover enter over a {@link android.view.View}. */ public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080; /** * Represents the event of a hover exit over a {@link android.view.View}. */ public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100; /** * Represents the event of starting a touch exploration gesture. */ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200; /** * Represents the event of ending a touch exploration gesture. */ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400; /** * Represents the event of changing the content of a window and more * specifically the sub-tree rooted at the event's source. */ public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800; /** * Represents the event of scrolling a view. */ public static final int TYPE_VIEW_SCROLLED = 0x00001000; /** * Represents the event of changing the selection in an {@link android.widget.EditText}. */ public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000; /** * Represents the event of an application making an announcement. */ public static final int TYPE_ANNOUNCEMENT = 0x00004000; /** * Represents the event of gaining accessibility focus. */ public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000; /** * Represents the event of clearing accessibility focus. */ public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000; /** * Represents the event of traversing the text of a view at a given movement granularity. */ public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000; /** * Represents the event of beginning gesture detection. */ public static final int TYPE_GESTURE_DETECTION_START = 0x00040000; /** * Represents the event of ending gesture detection. */ public static final int TYPE_GESTURE_DETECTION_END = 0x00080000; /** * Represents the event of the user starting to touch the screen. */ public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000; /** * Represents the event of the user ending to touch the screen. */ public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * The type of change is not defined. */ public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * A node in the subtree rooted at the source node was added or removed. */ public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * The node's text changed. */ public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002; /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * The node's content description changed. */ public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;
這里有很多事件,這些事件我們通過名字就可以看出來有很多我們可能都知道,比如當窗口發(fā)生變化的時候,當某個View被點擊了,被滾動了等消息都是可以知道的。那么我們有了這些事件我們就可以做我們的事情了,因為我們知道事件觸發(fā)了。
第四步、查找到我們想要處理的節(jié)點View
這里系統(tǒng)提供了兩個方法讓我們來進行查找想要的節(jié)點View
第一種是通過節(jié)點View的Text內容來查找
findAccessibilityNodeInfosByText("查找內容")
這種方式查找,就是像TextView,Button等View有文本內容的,可以使用這種方式快速的找到。
第二種是通過節(jié)點View在xml布局中的id名稱
findAccessibilityNodeInfosByViewId("@id/xxx")
這個一般很難知道,但是我們在查找系統(tǒng)控件的時候還是可以做的,因為系統(tǒng)的控件的id是可以知道的,而且是統(tǒng)一的。
(關于這兩個方法我們在寫網(wǎng)頁爬蟲程序的時候可能知道,在html中通過tag/name/id等信息可以找到一個節(jié)點,原理都類似)
第五步、模擬點擊指定事件
我們找到我們想要的View節(jié)點,調用方法模擬事件:
performAction(AccessibilityNodeInfo.ACTION_CLICK)
調用這個方法即可,當然這里的參數(shù)就是指定事件的名稱,這個和AccessibilityEvent中監(jiān)聽的那些事件是一一對應的,這里是模擬點擊事件,我們當然可以模擬View的滾動事件,長按事件等。
三、實戰(zhàn)案例:微信搶紅包插件
上面我們就介紹了一個輔助功能開發(fā)的具體步驟,那么下面就通過一個簡單的例子,來實戰(zhàn)一下
例子:微信自動搶紅包插件
首先我們來看一下微信搶紅包的流程:
第一步、我們在通知欄會接收到一個微信紅包的消息
我們監(jiān)聽通知欄事件:
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED
然后查看通知欄的消息中是否有:[微信紅包] 的文本內容
是的話,就走進入第二步
第二步、我們模擬打開通知欄
打開微信如下圖:
我們查找包含有:領取紅包 的文本內容的節(jié)點View,然后模擬點擊,進入第三步:
第三步、我們點擊領取紅包
'如下圖:
這里我們在查找包含有:拆紅包 的文本內容的節(jié)點View,然后模擬點擊
下面我們來看一下代碼中的具體實現(xiàn):
package krelve.demo.rob; import java.util.List; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; public class RobMoney extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { int eventType = event.getEventType(); switch (eventType) { //第一步:監(jiān)聽通知欄消息 case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: List<CharSequence> texts = event.getText(); if (!texts.isEmpty()) { for (CharSequence text : texts) { String content = text.toString(); Log.i("demo", "text:"+content); if (content.contains("[微信紅包]")) { //模擬打開通知欄消息 if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (CanceledException e) { e.printStackTrace(); } } } } } break; //第二步:監(jiān)聽是否進入微信紅包消息界面 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: String className = event.getClassName().toString(); if (className.equals("com.tencent.mm.ui.LauncherUI")) { //開始搶紅包 getPacket(); } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI")) { //開始打開紅包 openPacket(); } break; } } /** * 查找到 */ @SuppressLint("NewApi") private void openPacket() { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { List<AccessibilityNodeInfo> list = nodeInfo .findAccessibilityNodeInfosByText("搶紅包"); for (AccessibilityNodeInfo n : list) { n.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } } @SuppressLint("NewApi") private void getPacket() { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); recycle(rootNode); } /** * 打印一個節(jié)點的結構 * @param info */ @SuppressLint("NewApi") public void recycle(AccessibilityNodeInfo info) { if (info.getChildCount() == 0) { if(info.getText() != null){ if("領取紅包".equals(info.getText().toString())){ //這里有一個問題需要注意,就是需要找到一個可以點擊的View Log.i("demo", "Click"+",isClick:"+info.isClickable()); info.performAction(AccessibilityNodeInfo.ACTION_CLICK); AccessibilityNodeInfo parent = info.getParent(); while(parent != null){ Log.i("demo", "parent isClick:"+parent.isClickable()); if(parent.isClickable()){ parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } parent = parent.getParent(); } } } } else { for (int i = 0; i < info.getChildCount(); i++) { if(info.getChild(i)!=null){ recycle(info.getChild(i)); } } } } @Override public void onInterrupt() { } }
代碼沒什么好說的了,按照我們之前說的三個步驟來就可以了,但是這里需要注意點細節(jié)上的問題:
1、我們在監(jiān)聽到通知欄的消息的時候,調用如下代碼來進行通知欄的消息點擊
if (content.contains("[微信紅包]")) { //模擬打開通知欄消息 if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (CanceledException e) { e.printStackTrace(); } } }
2、我們在模擬點擊通知欄消息之后,還是需要監(jiān)聽:AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 這個事件,這個事件我們以后會經(jīng)常用到,這個事件就是在窗口發(fā)生改變的時候發(fā)出來的事件,很常用的,比如我們可以通過這個事件來監(jiān)聽TopActivity,然后得到包名,這也是一個實現(xiàn)應用鎖的一個原理。
3、我們在查找領取紅包的時候,模擬點擊的時候做了一個工作,就是從“領取紅包”文本的控件View網(wǎng)上查找,查找到一個可以點擊的View出來,然后模擬點擊
if(info.getText() != null){ if("領取紅包".equals(info.getText().toString())){ //這里有一個問題需要注意,就是需要找到一個可以點擊的View Log.i("demo", "Click"+",isClick:"+info.isClickable()); info.performAction(AccessibilityNodeInfo.ACTION_CLICK); AccessibilityNodeInfo parent = info.getParent(); while(parent != null){ Log.i("demo", "parent isClick:"+parent.isClickable()); if(parent.isClickable()){ parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } parent = parent.getParent(); } } }
這里為什么這么做,其實原理很簡單,因為我們不知道微信他的界面布局,也不知道他對哪個View進行了setOnClickListener。我們可以寫一個例子,performAction方法只對調用了setOnClickListener方法的View模擬點擊才有效,其實看View的源碼也是可以看出來的.這里就不多解釋了。所以我們就需要得到一個View節(jié)點之后,從下往上找,直到找到一個可以click的View為止。
技術延展:
我們其實還可以使用DDMS工具里的Dump View Hierarchy For UI Automator 去分析微信UI結構,這個方法也是我后面才發(fā)現(xiàn)的,比上面的代碼更有效,如下圖:
這里我們可以看到View的詳細布局,還有每個View的屬性,還有很重要的信息resource-id,這個就是我們在xml中定義的id,這個id我們也可以使用前面說到的findAccessibilityNodeInfosByViewId("@id/xxx")來查找控件了
這個也算是學習了,學會使用DDMS來分析View結構。
四、延展
關于微信搶紅包的原理解析上面已經(jīng)做了分析了,但是要想做到極致,這里還有很多問題的,比如我們還需要過濾一些已經(jīng)領取過的紅包,這樣的話效率也是很高的。這個都是算法精確的問題了,我想在這里說的是,我們不僅可以用輔助功能來實現(xiàn)搶紅包,還可以實現(xiàn)很多功能,比如
1、靜默安裝
對于這兩個要求,我們或許很難得到,那么現(xiàn)在如果有了輔助功能,我們就好做了:
我們可以監(jiān)聽系統(tǒng)的這個安裝界面,然后得到安裝節(jié)點View,然后模擬點擊即可,卸載也是同樣的原理
2、強制停止應用
我們知道Android中停止應用有很多方法,kill進程,stopService,但是這些方法,有一些應用它們都是有對策的,那么我們之前用到的強制停止的方法是獲取root權限調用系統(tǒng)的forceStop的api來停止,但是前提還是有root。那么現(xiàn)在如果我們有了輔助功能的話,我們可以這么做:
我們可以監(jiān)聽系統(tǒng)的應用詳情頁面,然后找到:結束運行的節(jié)點View,然后模擬點擊即可
當然上面我就說了兩個簡單的例子,還有很多輔助功能都是可以做的。他的好處就是不需要root權限。但是他也是需要用戶授權的:
如果用戶沒有授權的話,那么所有的工作都沒辦法開始了,所以說這個方法也不是萬能的。當然說句題外話:有了輔助功能的話,他的危險性比root之后的危險性更大,比如我們上面的搶紅包插件,其實我們稍作修改,就可以獲取微信通訊錄信息,微信支付的密碼。這些事都是可以做的,所以說,我們在作為用戶的時候,進行授權的時候還是需要三思而后行。
五、總結
關于輔助功能,之前沒有太多的接觸,是在一次工作中用到了這個功能,就去學習了一下,作為自己的興趣,就延展了學習了如何寫一個微信搶紅包的插件,同時可以考慮了使用輔助功能能夠做我們之前需要root做的事情。當然輔助功能是google對于肢體上有障礙的人開發(fā)出來的一個功能,我們開發(fā)者或許使用這個功能,可以做一下產(chǎn)品的拓展功能,當然這些是google沒有想到的事情,但是這個至少是我們開發(fā)者在以后的開發(fā)道路上的一個解決問題的一個辦法和途徑,謹記此功能!
相關文章
Android實現(xiàn)錄音功能實現(xiàn)實例(MediaRecorder)
本篇文章主要介紹了Android實現(xiàn)錄音的實例代碼(MediaRecorder),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07Android中ConstraintLayout約束布局的最全詳細解析
ConstraintLayout是Google在Google?I/O?2016大會上發(fā)布的一種新的布局容器(ViewGroup),它支持以靈活的方式來放置子控件和調整子控件的大小,下面這篇文章主要給大家介紹了關于Android中ConstraintLayout約束布局詳細解析的相關資料,需要的朋友可以參考下2022-08-08Android利用屬性動畫實現(xiàn)優(yōu)酷菜單
這篇文章主要為大家詳細介紹了Android利用屬性動畫實現(xiàn)優(yōu)酷菜單,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01Android?Studio實現(xiàn)音樂播放器的全過程(簡單易上手)
這篇文章主要給大家介紹了關于Android?Studio實現(xiàn)音樂播放器的相關資料,文中通過實例代碼以及圖文介紹的非常詳細,對各位Android開發(fā)者們具有一定的參考學習價值,需要的朋友可以參考下2022-02-02Android編程中TextView寬度過大導致Drawable無法居中問題解決方法
這篇文章主要介紹了Android編程中TextView寬度過大導致Drawable無法居中問題解決方法,以實例形式較為詳細的分析了TextView設置及xml布局與調用技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10Android?LinearLayout快速設置每個item間隔
這篇文章主要介紹了Android?LinearLayout快速設置每個item間隔的相關資料,需要的朋友可以參考下2023-07-07新版Flutter集成到已有Android項目的實現(xiàn)
這篇文章主要介紹了新版Flutter集成到已有Android項目的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03