有時候,知識小集群里討論的技術(shù)問題,比較有價值,我們會把有價值的內(nèi)容整理出來供大家查閱。但為了保護群友隱私,需要把昵稱和頭像都打碼,如果碰到幾百條聊天記錄,這樣做簡直要吐血。而且也不能截一張長圖,只能一張一張截取,然后拼接起來。群聊記錄只能在微信內(nèi)分享,這也限制了傳播的渠道。為了提高小集成員工作效率,想著能不能給微信做個插件,解決這些問題。我們一直在追求如何更有效率開展我們的工作,比如使用腳本自動整理每周小集內(nèi)容,使用微信小程序給讀者更好閱讀體驗。(呀,還有腳本,如果你還不知道,那肯定沒有點 star 吧, 傳送門 )
知識小集是一個團隊公眾號,每周都會有 原創(chuàng) 文章分享,我們的文章都會在公眾號首發(fā)。知識小集微信群,短短幾周時間,目前群友已經(jīng)300+人,很快就要達到上限(抓住機會哦),關(guān)注公眾號獲取加群方式。
通過以上痛點,可以確定我們要解決的問題主要有:
下面這張圖是聊天記錄頁面,點擊導(dǎo)航右邊按鈕,會彈出 ActionSheet。從圖中可以看出,添加截圖功能,在 ActionSheet 上添加是不錯的選擇。
我們下面主要的工作是:
本文使用 MonkeyDev 工具開發(fā)(無需越獄),重點是教你如何開發(fā)一個微信插件,并不打算介紹工具。關(guān)注我們的朋友應(yīng)該都知道,以往的 #iOS知識小集 中我已經(jīng)介紹了三個工具的使用。這三個工具在下面會用到。
使用 Reveal 工具查看聊天記錄頁面對應(yīng)的 VC,ActionSheet 對應(yīng)的類名。如何使用 Reveal 調(diào)試第三方 APP,網(wǎng)上有很多教程。使用 MonkeyDev 無需越獄。
通過上圖可以看到聊天記錄所對應(yīng)的VC是 MsgRecordDetailViewController ,使用 MMTableView 展示聊天內(nèi)容。彈出的 ActionSheet 對應(yīng)的類為 WCActionSheet , 可以發(fā)現(xiàn)它是一個 UIWindow 。 那么我們看看這幾個類中的內(nèi)容吧。
使用 class-dump 查看第三方 APP 的頭文件。在 #iOS知識小集 中已經(jīng)介紹過這個工具的使用。
在 MsgRecordDetailViewController 的頭文件中發(fā)現(xiàn)有一個 WCActionSheet *favImgLongPressAction; 我們可以斷定出 WCActionSheet 就是我們要找的 ActionSheet。好了,接下來主要就是看 WCActionSheet 的頭文件,挖掘有用的信息。
@property(strong, nonatomic) NSMutableArray *buttonTitleList; - (void)showInView:(id)arg1; - (long long)addButtonWithItem:(id)arg1 atIndex:(unsigned long long)arg2; - (long long)addButtonWithTitle:(id)arg1 atIndex:(unsigned long long)arg2; - (long long)addButtonWithTitle:(id)arg1;
我們的目標(biāo)是給 WCActionSheet 添加3個菜單。下面這些方法似乎對我們有用。目前想到有兩種方案:
我們所關(guān)心的最主要的問題是 buttonTitleList 中存放的的對象是什么?需要使用Cycript工具,這個工具在以往的 #iOS知識小集 介紹過,想了解的朋友可以在知識小集小程序中搜索 Cycript調(diào)試第三方APP 。
通過 Cycript 可以看到 buttonTitleList 中存放的對象是 WCActionSheetItem 。我們看看 WCActionSheetItem 的頭文件,發(fā)現(xiàn)其實就是一個 Model 對象,用來表示菜單的標(biāo)題,顏色等等。
@interface WCActionSheetItem : NSObject @property(copy, nonatomic) NSString *titleColor; @property(copy, nonatomic) NSString *title; - (id)initWithTitle:(id)arg1 fontSize:(long long)arg2 fontColor:(id)arg3 WithDesc:(id)arg4 descFontSize:(long long)arg5 descFontColor:(id)arg6 enable:(_Bool)arg7; - (id)initWithTitle:(id)arg1;
看到這里,我們可以直接在 buttonTitleList 中添加 WCActionSheetItem 實例即可。
從上圖可以看出直接調(diào)用 addButtonWithTitle: 這個方法,返回一個 Index 為 3,說明可以直接調(diào)用這個方法。
接下來主要的問題是,找到添加菜單的時機。第一想到的是在 WCActionSheetDelegate 的代理中添加菜單,果斷在 MsgRecordDetailViewController 中 Hook 下面這3個代理方法,但是通過實驗證明,發(fā)現(xiàn)最后兩個方法并沒有被調(diào)用,因為 MsgRecordDetailViewController 并沒有實現(xiàn)這兩個代理,只好放棄了這種思路。
- (void)actionSheet:(id)arg1 clickedButtonAtIndex:(long long)arg2; - (void)didPresentActionSheet:(WCActionSheet *)arg1; - (void)willPresentActionSheet:(WCActionSheet *)arg1;
無奈之下看到 WCActionSheet 中有個 showInView: 方法, 可以直接 Hook 這個方法。但這樣導(dǎo)致所有的 WCActionSheet 都會被添加了額外的菜單。而我們的目的只是在聊天記錄頁中的 WCActionSheet 顯示截圖菜單。所以用 [WeChatSaveData defaultSaveData].isNeedAddMenu 加了一個判斷,isNeedAddMenu 在 MsgRecordDetailViewController 頁面將要出現(xiàn)的時候,設(shè)置成 YES,在頁面將要消失的時候,設(shè)置成 NO。所以需要 Hook MsgRecordDetailViewController 的 viewWillAppear: 和 viewWillDisappear: 方法。
CHOptimizedMethod1(self, void, WCActionSheet, showInView, UIView *, view){ if ([WeChatSaveData defaultSaveData].isNeedAddMenu) { // 方案一 [self addButtonWithTitle:@""]; // 填坑 [self addButtonWithTitle:kScreenshotTitle]; [self addButtonWithTitle:kScreenshotTitleMask]; // 方案二 WCActionSheetItem *shotItem = [[objc_getClass("WCActionSheetItem") alloc] initWithTitle:kScreenshotTitle]; WCActionSheetItem *shotItem2 = [[objc_getClass("WCActionSheetItem") alloc] initWithTitle:kScreenshotTitleMask]; [self.buttonTitleList addObject:shotItem]; [self.buttonTitleList addObject:shotItem2]; } CHSuper1(WCActionSheet, showInView, view); }
執(zhí)行結(jié)果如下圖:
@interface MsgRecordDetailViewController: UIViewController { MMTableView *m_tableView; } - (void)viewWillAppear:(_Bool)arg1; - (void)viewWillDisappear:(_Bool)arg1; - (void)actionSheet:(id)arg1 clickedButtonAtIndex:(long long)arg2; - (UITableViewCell *)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2; @end
通過對 MsgRecordDetailViewController 頭文件分析,可以達到截圖功能,只需要截取 TableView 為一張長圖即可。
獲取到 MsgRecordDetailViewController 實例,使用 KVC 的方式即可獲取到 MMTableView
MMTableView *tableView = [viewController valueForKeyPath:@"m_tableView"];
首先需要 Hook actionSheet: clickedButtonAtIndex: 捕獲菜單的點擊事件,做截圖功能。
CHOptimizedMethod2(self, void, MsgRecordDetailViewController, actionSheet, WCActionSheet*, sheet, clickedButtonAtIndex, int, index){ CHSuper2(MsgRecordDetailViewController, actionSheet, sheet, clickedButtonAtIndex, index); [WeChatCapture saveCaptureImageWithSheet:sheet index:index viewController:self]; }
saveCaptureImageWithSheet 這個方法中主要獲取到 MMTableView 并截圖保存到相冊。有興趣可以看源碼。
為了保護用戶的隱私,需要對用戶的頭像和昵稱做保護,那么我們可以在 TableView 的代理中獲取頭像和昵稱對應(yīng)的 View,然后替換 View 的內(nèi)容即可。需要 Hook cellForRowAtIndexPath 這個方法。
CHOptimizedMethod2(self, UITableViewCell *, MsgRecordDetailViewController, tableView, MMTableView *, tableViewArg, cellForRowAtIndexPath, NSIndexPath, *indexPath){ UITableViewCell *cell = CHSuper2(MsgRecordDetailViewController, tableView, tableViewArg, cellForRowAtIndexPath, indexPath); [WeChatCapture updateCellDataWithCell:cell indexPath:indexPath]; return cell; }
獲取到 Cell 如果當(dāng)前是要打碼截圖,需要對頭像和昵稱的內(nèi)容做處理。這里做一個特殊的處理,頭像和昵稱我們換成三國人物的頭像的名字。
+ (void)updateCellDataWithCell:(UITableViewCell *)cell indexPath:(NSIndexPath *)indexPath { if ([WeChatSaveData defaultSaveData].maskType == WeChatSaveDataMaskTypeMast || [WeChatSaveData defaultSaveData].maskType == WeChatSaveDataMaskTypePreview) { NSArray *subviews = [cell.contentView subviews]; FavRecordBaseNodeView *nodeView = [subviews lastObject]; if ([NSStringFromClass([nodeView class]) hasSuffix:@"NodeView"]) { UILabel *nickNameLabel = [nodeView valueForKey:@"m_srcTitleLabel"]; if (nickNameLabel) { CGRect tempFrame = nickNameLabel.frame; tempFrame.size.width = 120; nickNameLabel.frame = tempFrame; } MMHeadImageView *imageView = [nodeView valueForKeyPath:@"m_headImg"]; NSString *nickName = [imageView valueForKey:@"_nsUsrName"]; WeChatUser *aUser = [[WeChatSaveData defaultSaveData].userNameDict objectForKey:nickName?:@""]; if (!aUser) { aUser = [WeChatUser user]; [[WeChatSaveData defaultSaveData].userNameDict setObject:aUser forKey:nickName?:@""]; } nickNameLabel.text = aUser.nickname ?: @""; if (imageView) { [imageView updateUsrName:aUser.nickname withHeadImgUrl:aUser.icon]; } } } }
給繪制的長圖添加一個小集的版權(quán) by公眾號 知識小集 。
最終效果(伴隨著咔嚓一聲,一張被打碼的照片保存到了相冊,你可以在任意的渠道分享了):
添加額外的菜單后,WCActionSheet 的代理方法 actionSheet: clickedButtonAtIndex: 中的 Index 點擊取消或空白區(qū)域總為 2,也就是我添加菜單第一個的 Index。導(dǎo)致每次點擊取消或空白區(qū)域時都會聽到咔嚓一聲截圖。解決方法,就是加一個沒有標(biāo)題的菜單,并且高度為 0。
按著這個思路給收藏中的聊天記錄添加截圖功能,這就是你為什么會在源碼中看到 FavRecordDetailViewController 的 Hook。
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)