android 最新微信红包,GitHub - lthis/WeChatLuckyMoney: 微信抢红包插件 for Android
微信搶紅包插件
這個Android插件可以幫助你在微信群聊搶紅包時戰(zhàn)無不勝。當(dāng)檢測到紅包時,插件會自動點擊屏幕,和人工點擊的速度不可同日而語。
你正在查看的是dev分支,這個分支仍在開發(fā)中,如果你希望有一個可以立即使用的插件請切換到stable分支。
注: stable分支的插件只點擊最新的紅包,根據(jù)目前測試搶紅包成功率100%。dev分支在stable分支的基礎(chǔ)上嘗試了大量修改和優(yōu)化,能使用但無法保證穩(wěn)定性。
下面的文檔僅針對dev分支。
預(yù)期特性
可以搶屏幕上顯示的所有紅包,同類插件往往只能獲取最新的一個紅包。
智能跳過已經(jīng)戳過的紅包,避免頻繁點擊影響正常使用。
紅包日志功能,方便查看搶過的紅包內(nèi)容。
性能優(yōu)化,感受不到插件的存在,可一直后臺開啟,不影響日常聊天。
由于這是一份教學(xué)代碼,項目的文檔和注釋都比較完整,代碼適合閱讀。
注: 其中一些功能還存在問題,仍在調(diào)試中。
實現(xiàn)原理
1. 搶紅包流程的邏輯控制
這個插件通過一個Stage類來記錄當(dāng)前對應(yīng)的階段。Stage類被設(shè)計成單例并惰性實例化,因為一個Service不需要也不應(yīng)該處在不同的階段。對外暴露階段常量和entering和getCurrentStage兩個方法,分別記錄和獲取當(dāng)前的階段。
public class Stage {
private static Stage stageInstance;
public static final int FETCHING_STAGE = 0, OPENING_STAGE = 1, FETCHED_STAGE = 2, OPENED_STAGE = 3;
private int currentStage = FETCHED_STAGE;
private Stage() {}
public static Stage getInstance() {
if (stageInstance == null) stageInstance = new Stage();
return stageInstance;
}
public void entering(int _stage) {
stageInstance.currentStage = _stage;
}
public int getCurrentStage() {
return stageInstance.currentStage;
}
}
1.1 階段說明
階段
說明
FETCHING_STAGE
正在讀取屏幕上的紅包,此時不應(yīng)有別的操作
FETCHED_STAGE
已經(jīng)結(jié)束一個FETCH階段,屏幕上的紅包都已加入待搶隊列
OPENING_STAGE
正在拆紅包,此時不應(yīng)有別的操作
OPENED_STAGE
紅包成功搶到,進(jìn)入紅包詳情頁面
程序以FETCHED_STAGE 開始,將屏幕上的紅包加入待搶隊列:
--> FETCHED_STAGE --> FETCHING_STAGE --> FETCHED_STAGE -->
處理待搶隊列中的紅包:
--> [CLICK] --> OPENING_STAGE --> [CLICK] --> OPENED_STAGE --> [BACK] --> FETCHED_STAGE -->(搶到)
--> [CLICK] --> OPENING_STAGE --> [BACK] --> FETCHED_STAGE -->(沒搶到)
不斷重復(fù)流程1和2
1.2 根據(jù)階段選擇不同的入口
在每次窗體狀態(tài)發(fā)生變化后,根據(jù)當(dāng)前所在的階段選擇入口。
switch (Stage.getInstance().getCurrentStage()) {
case Stage.OPENING_STAGE:
// .......
Stage.getInstance().entering(Stage.FETCHED_STAGE);
performGlobalAction(GLOBAL_ACTION_BACK);
break;
case Stage.OPENED_STAGE:
Stage.getInstance().entering(Stage.FETCHED_STAGE);
performGlobalAction(GLOBAL_ACTION_BACK);
break;
case Stage.FETCHED_STAGE:
if (nodesToFetch.size() > 0) {
AccessibilityNodeInfo node = nodesToFetch.remove(nodesToFetch.size() - 1);
if (node.getParent() != null) {
// .......
Stage.getInstance().entering(Stage.OPENING_STAGE);
node.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
return;
}
Stage.getInstance().entering(Stage.FETCHING_STAGE);
fetchHongbao(nodeInfo);
Stage.getInstance().entering(Stage.FETCHED_STAGE);
break;
}
2. 屏幕內(nèi)容檢測和自動化點擊的實現(xiàn)
和其他插件一樣,這里使用的是Android API提供的AccessibilityService。這個類位于android.accessibilityservice包內(nèi),該包中的類用于開發(fā)無障礙服務(wù),提供代替或增強的用戶反饋。
AccessibilityService 服務(wù)在后臺運行,等待系統(tǒng)在發(fā)生 AccessibilityEvent 事件時回調(diào)。這些事件指的是用戶界面上發(fā)生的狀態(tài)變化, 比如焦點變更、按鈕按下等等。服務(wù)可以請求“查詢當(dāng)前窗口中內(nèi)容”的能力。 開發(fā)輔助服務(wù)需要繼承該類并實現(xiàn)其抽象方法。
2.1 配置AccessibilityService
在這個例子中,我們需要監(jiān)聽的事件是當(dāng)紅包來或者滑動屏幕時引起的屏幕內(nèi)容變化,和點開紅包時窗體狀態(tài)的變化,因此我們只需要在配置XML的accessibility-service標(biāo)簽中加入一條
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
或在onAccessibilityEvent回調(diào)函數(shù)中對事件進(jìn)行一次類型判斷
final int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|| eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
// ...
}
除此之外,由于我們只監(jiān)聽微信,還需要指定微信的包名
android:packageNames="com.tencent.mm"
為了獲取窗口內(nèi)容,我們還需要指定
android:canRetrieveWindowContent="true"
其他配置請看代碼。
2.2 獲取紅包所在的節(jié)點
首先,我們要獲取當(dāng)前屏幕的根節(jié)點,下面兩種方式效果是相同的:
AccessibilityNodeInfo nodeInfo = event.getSource();
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
這里返回的AccessibilityNodeInfo是窗體內(nèi)容的節(jié)點,包含節(jié)點在屏幕上的位置、文本描述、子節(jié)點id、能否點擊等信息。從AccessibilityService的角度來看,窗體上的內(nèi)容表示為輔助節(jié)點樹,雖然和視圖的結(jié)構(gòu)不一定一一對應(yīng)。換句話說,自定義的視圖可以自己描述上面的輔助節(jié)點信息。當(dāng)輔助節(jié)點傳遞給AccessibilityService之后就不可更改了,如果強行調(diào)用引起狀態(tài)變化的方法會報錯。
在聊天頁面,每個紅包上面都有“領(lǐng)取紅包”這幾個字,我們把它作為識別紅包的依據(jù)。因此,如果你收到了這四個字的文本消息,那插件會做出誤判,并沒有什么辦法可以解決。
AccessibilityNodeInfo的API中有一個findAccessibilityNodeInfosByText方法允許我們通過文本來搜索界面中的節(jié)點。匹配是大小寫敏感的,它會從遍歷樹的根節(jié)點開始查找。API文檔中特別指出,為了防止創(chuàng)建大量實例,節(jié)點回收是調(diào)用者的責(zé)任,這一點會在接下來的部分中講到。
List node1 = nodeInfo.findAccessibilityNodeInfosByText("領(lǐng)取紅包");
2.3 對節(jié)點進(jìn)行操作
AccessibilityNodeInfo同樣暴露了一個API——performAction來對節(jié)點進(jìn)行點擊或者其他操作。出于安全性考慮,只有這個操作來自AccessibilityService時才會被執(zhí)行。
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
不過,我們在調(diào)試時發(fā)現(xiàn)"領(lǐng)取紅包"的mClickable屬性為false,說明點擊的監(jiān)聽加在它父輩的節(jié)點上。通過getParent獲取父節(jié)點,這個節(jié)點是可以點擊的。
我們還需要全局的返回操作,最方便的辦法就是performGlobalAction,不過注意這個方法是API 16后才有的。
performGlobalAction(GLOBAL_ACTION_BACK);
3. 獲取屏幕上的所有紅包
和其他插件最大的區(qū)別是,這個插件的邏輯是獲取屏幕上所有的紅包節(jié)點,去掉已經(jīng)獲取過的之后,將待搶紅包加入隊列,再將隊列中的紅包一個個打開。
3.1 判斷紅包節(jié)點是否已被搶過
實現(xiàn)這一點是編寫時最大的障礙。對于一般的Java對象實例來說,除非被GC回收,實例的Id都不會變化。我最初的想法是通過正則表達(dá)式匹配下面的十六進(jìn)制對象id來表示一個紅包。
android.view.accessibility.AccessibilityNodeInfo@2a5a7c; .......
但在測試中,隊列中的部分紅包沒有被戳開。進(jìn)一步觀察發(fā)現(xiàn),新的紅包節(jié)點和舊的紅包節(jié)點id出現(xiàn)了重復(fù),且出現(xiàn)概率較大。由于GC日志正常,我推測AccessibilityNode可能有一個實例池的設(shè)計。獲取當(dāng)前窗體節(jié)點樹的時候,從一個可重用的實例池中獲取一個輔助節(jié)點信息 (AccessibilityNodeInfo)實例。在接下來的獲取時,仍然從實例池中獲取節(jié)點實例,這時可能會重用之前的實例。這樣的設(shè)計是有好處的,可以防止每次返回都創(chuàng)建大量的實例,影響性能。AccessibilityNodeProvider的源碼表明了這樣的設(shè)計。
也就是說,為了標(biāo)識一個唯一的紅包,只用實例id是不充分的。這個插件采用的是紅包內(nèi)容+節(jié)點實例id的hash來標(biāo)記。因為同一屏下,同一個節(jié)點樹下的節(jié)點id是一定不會重復(fù)的,滑動屏幕后新紅包的內(nèi)容和節(jié)點id同時重復(fù)的概率已經(jīng)大大減小。更改標(biāo)識策略后,實測中幾乎沒有出現(xiàn)誤判。
3.2 將新出現(xiàn)的紅包加入待搶隊列
我們維護(hù)了兩個列表,分別記錄待搶紅包和搶過的紅包標(biāo)識。
private List nodesToFetch = new ArrayList<>();
private List fetchedIdentifiers = new ArrayList<>();
在每次讀取聊天屏幕的時候,會檢查這個紅包是否在fetchedIdentifiers隊列中,如果沒有,則加入nodesToFetch隊列。
for (AccessibilityNodeInfo cellNode : fetchNodes) {
String id = getHongbaoHash(cellNode);
/* 如果節(jié)點沒有被回收且該紅包沒有搶過 */
if (id != null && !fetchedIdentifiers.contains(id)) {
nodesToFetch.add(cellNode);
}
}
4. 打開隊列中的紅包
通過紅包打開后顯示的文本判斷這個紅包是否可以搶,進(jìn)行接下來的操作。
4.1 判斷紅包節(jié)點是否被重用
這也是實現(xiàn)時的一個坑。前面提到了實例池的設(shè)計,當(dāng)我們把紅包們加入待搶隊列,戳完一個紅包再回來時,隊列中的其他紅包節(jié)點可能已被回收重用,如果再去點擊這個節(jié)點,顯然沒有什么卵用。
為了解決這個問題,我們只能退而求其次,在點開前做一次檢查。如果發(fā)現(xiàn)被重用了,就舍棄這個節(jié)點,在下一輪fetch的階段重新加入待搶隊列。確認(rèn)沒有重用立即打開,并把節(jié)點hash加入fetchedIdentifiers隊列。這里如果node失效getHongbaoHash會返回null。
AccessibilityNodeInfo node = nodesToFetch.remove(nodesToFetch.size() - 1);
if (node.getParent() != null) {
String id = getHongbaoHash(node);
if (id == null) return;
fetchedIdentifiers.add(id);
Stage.getInstance().entering(Stage.OPENING_STAGE);
node.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
可以看出這并不是很有效率的解決方案。在實測中,有時隊列中中紅包失效后被舍棄,但沒有新的AccessibilityEvent發(fā)生,接下來的操作都被掛起了。在戳過較多紅包之后,這種情況表現(xiàn)得尤為明顯,必須要顯式地改變窗體內(nèi)容才能解決。
4.2 根據(jù)紅包類型選擇操作
紅包被戳開前會進(jìn)行查重。戳開后如果界面上出現(xiàn)了“拆紅包”幾個字,說明紅包還沒有被別人搶走,立刻點擊“拆紅包”并將stage標(biāo)記為OPENED_STAGE。
此時,另三種情況表明搶紅包失敗了,直接返回,接下來狀態(tài)會被標(biāo)記為FETCHED_STAGE。
“過期”,說明紅包超過有效時間
“手慢了”,說明紅包發(fā)完但沒搶到
“紅包詳情”,說明你已經(jīng)搶到過
4.3 防止加載紅包時返回
戳開紅包和紅包加載完之間有一個“正在加載”的過渡動畫,會觸發(fā)onAccessibilityEvent回調(diào)方法。如果在加載完之前判斷,上述文本還沒出現(xiàn),會被默認(rèn)標(biāo)記為FETCHED_STAGE并觸發(fā)返回。因此,我們要在返回前特殊判定這種情形。
我們引入了TTL來記錄嘗試次數(shù),并返回錯誤值-1。如果到達(dá)MAX_TTL時紅包還沒有加載出來就舍棄這個紅包。
Stage.getInstance().entering(Stage.OPENING_STAGE);
ttl += 1;
return -1;
版權(quán)說明
本項目源自小米今年秋季發(fā)布會時演示的搶紅包測試源碼。stable分支基于此代碼繼續(xù)開發(fā),dev分支重寫了幾乎所有的邏輯代碼。應(yīng)用的包名com.miui.hongbao未變。
由于插件可能會改變自然的微信交互方式,這份代碼僅可用于教學(xué)目的,不得更改后用于其他用途。
項目使用MIT許可證。在上述前提下,你可以將代碼用于任何用途。
總結(jié)
以上是生活随笔為你收集整理的android 最新微信红包,GitHub - lthis/WeChatLuckyMoney: 微信抢红包插件 for Android的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sql server 2008 r2卸载
- 下一篇: android横竖屏切换布局闪退,And