有時(shí)候,知識(shí)小集群里討論的技術(shù)問(wèn)題,比較有價(jià)值,我們會(huì)把有價(jià)值的內(nèi)容整理出來(lái)供大家查閱。但為了保護(hù)群友隱私,需要把昵稱和頭像都打碼,如果碰到幾百條聊天記錄,這樣做簡(jiǎn)直要吐血。而且也不能截一張長(zhǎng)圖,只能一張一張截取,然后拼接起來(lái)。群聊記錄只能在微信內(nèi)分享,這也限制了傳播的渠道。為了提高小集成員工作效率,想著能不能給微信做個(gè)插件,解決這些問(wèn)題。我們一直在追求如何更有效率開(kāi)展我們的工作,比如使用腳本自動(dòng)整理每周小集內(nèi)容,使用微信小程序給讀者更好閱讀體驗(yàn)。(呀,還有腳本,如果你還不知道,那肯定沒(méi)有點(diǎn) star 吧, 傳送門 )
知識(shí)小集是一個(gè)團(tuán)隊(duì)公眾號(hào),每周都會(huì)有 原創(chuàng) 文章分享,我們的文章都會(huì)在公眾號(hào)首發(fā)。知識(shí)小集微信群,短短幾周時(shí)間,目前群友已經(jīng)300+人,很快就要達(dá)到上限(抓住機(jī)會(huì)哦),關(guān)注公眾號(hào)獲取加群方式。
通過(guò)以上痛點(diǎn),可以確定我們要解決的問(wèn)題主要有:
下面這張圖是聊天記錄頁(yè)面,點(diǎn)擊導(dǎo)航右邊按鈕,會(huì)彈出 ActionSheet。從圖中可以看出,添加截圖功能,在 ActionSheet 上添加是不錯(cuò)的選擇。
我們下面主要的工作是:
本文使用 MonkeyDev 工具開(kāi)發(fā)(無(wú)需越獄),重點(diǎn)是教你如何開(kāi)發(fā)一個(gè)微信插件,并不打算介紹工具。關(guān)注我們的朋友應(yīng)該都知道,以往的 #iOS知識(shí)小集 中我已經(jīng)介紹了三個(gè)工具的使用。這三個(gè)工具在下面會(huì)用到。
使用 Reveal 工具查看聊天記錄頁(yè)面對(duì)應(yīng)的 VC,ActionSheet 對(duì)應(yīng)的類名。如何使用 Reveal 調(diào)試第三方 APP,網(wǎng)上有很多教程。使用 MonkeyDev 無(wú)需越獄。
通過(guò)上圖可以看到聊天記錄所對(duì)應(yīng)的VC是 MsgRecordDetailViewController ,使用 MMTableView 展示聊天內(nèi)容。彈出的 ActionSheet 對(duì)應(yīng)的類為 WCActionSheet , 可以發(fā)現(xiàn)它是一個(gè) UIWindow 。 那么我們看看這幾個(gè)類中的內(nèi)容吧。
使用 class-dump 查看第三方 APP 的頭文件。在 #iOS知識(shí)小集 中已經(jīng)介紹過(guò)這個(gè)工具的使用。
在 MsgRecordDetailViewController 的頭文件中發(fā)現(xiàn)有一個(gè) WCActionSheet *favImgLongPressAction; 我們可以斷定出 WCActionSheet 就是我們要找的 ActionSheet。好了,接下來(lái)主要就是看 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個(gè)菜單。下面這些方法似乎對(duì)我們有用。目前想到有兩種方案:
我們所關(guān)心的最主要的問(wèn)題是 buttonTitleList 中存放的的對(duì)象是什么?需要使用Cycript工具,這個(gè)工具在以往的 #iOS知識(shí)小集 介紹過(guò),想了解的朋友可以在知識(shí)小集小程序中搜索 Cycript調(diào)試第三方APP 。
通過(guò) Cycript 可以看到 buttonTitleList 中存放的對(duì)象是 WCActionSheetItem 。我們看看 WCActionSheetItem 的頭文件,發(fā)現(xiàn)其實(shí)就是一個(gè) Model 對(duì)象,用來(lái)表示菜單的標(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 實(shí)例即可。
從上圖可以看出直接調(diào)用 addButtonWithTitle: 這個(gè)方法,返回一個(gè) Index 為 3,說(shuō)明可以直接調(diào)用這個(gè)方法。
接下來(lái)主要的問(wèn)題是,找到添加菜單的時(shí)機(jī)。第一想到的是在 WCActionSheetDelegate 的代理中添加菜單,果斷在 MsgRecordDetailViewController 中 Hook 下面這3個(gè)代理方法,但是通過(guò)實(shí)驗(yàn)證明,發(fā)現(xiàn)最后兩個(gè)方法并沒(méi)有被調(diào)用,因?yàn)?nbsp;MsgRecordDetailViewController 并沒(méi)有實(shí)現(xiàn)這兩個(gè)代理,只好放棄了這種思路。
- (void)actionSheet:(id)arg1 clickedButtonAtIndex:(long long)arg2; - (void)didPresentActionSheet:(WCActionSheet *)arg1; - (void)willPresentActionSheet:(WCActionSheet *)arg1;
無(wú)奈之下看到 WCActionSheet 中有個(gè) showInView: 方法, 可以直接 Hook 這個(gè)方法。但這樣導(dǎo)致所有的 WCActionSheet 都會(huì)被添加了額外的菜單。而我們的目的只是在聊天記錄頁(yè)中的 WCActionSheet 顯示截圖菜單。所以用 [WeChatSaveData defaultSaveData].isNeedAddMenu 加了一個(gè)判斷,isNeedAddMenu 在 MsgRecordDetailViewController 頁(yè)面將要出現(xiàn)的時(shí)候,設(shè)置成 YES,在頁(yè)面將要消失的時(shí)候,設(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
通過(guò)對(duì) MsgRecordDetailViewController 頭文件分析,可以達(dá)到截圖功能,只需要截取 TableView 為一張長(zhǎng)圖即可。
獲取到 MsgRecordDetailViewController 實(shí)例,使用 KVC 的方式即可獲取到 MMTableView
MMTableView *tableView = [viewController valueForKeyPath:@"m_tableView"];
首先需要 Hook actionSheet: clickedButtonAtIndex: 捕獲菜單的點(diǎn)擊事件,做截圖功能。
CHOptimizedMethod2(self, void, MsgRecordDetailViewController, actionSheet, WCActionSheet*, sheet, clickedButtonAtIndex, int, index){ CHSuper2(MsgRecordDetailViewController, actionSheet, sheet, clickedButtonAtIndex, index); [WeChatCapture saveCaptureImageWithSheet:sheet index:index viewController:self]; }
saveCaptureImageWithSheet 這個(gè)方法中主要獲取到 MMTableView 并截圖保存到相冊(cè)。有興趣可以看源碼。
為了保護(hù)用戶的隱私,需要對(duì)用戶的頭像和昵稱做保護(hù),那么我們可以在 TableView 的代理中獲取頭像和昵稱對(duì)應(yīng)的 View,然后替換 View 的內(nèi)容即可。需要 Hook cellForRowAtIndexPath 這個(gè)方法。
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)前是要打碼截圖,需要對(duì)頭像和昵稱的內(nèi)容做處理。這里做一個(gè)特殊的處理,頭像和昵稱我們換成三國(guó)人物的頭像的名字。
+ (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]; } } } }
給繪制的長(zhǎng)圖添加一個(gè)小集的版權(quán) by公眾號(hào) 知識(shí)小集 。
最終效果(伴隨著咔嚓一聲,一張被打碼的照片保存到了相冊(cè),你可以在任意的渠道分享了):
添加額外的菜單后,WCActionSheet 的代理方法 actionSheet: clickedButtonAtIndex: 中的 Index 點(diǎn)擊取消或空白區(qū)域總為 2,也就是我添加菜單第一個(gè)的 Index。導(dǎo)致每次點(diǎn)擊取消或空白區(qū)域時(shí)都會(huì)聽(tīng)到咔嚓一聲截圖。解決方法,就是加一個(gè)沒(méi)有標(biāo)題的菜單,并且高度為 0。
按著這個(gè)思路給收藏中的聊天記錄添加截圖功能,這就是你為什么會(huì)在源碼中看到 FavRecordDetailViewController 的 Hook。
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)