給微信聊天記錄新增截圖功能
有時候,知識小叢集裡討論的技術問題,比較有價值,我們會把有價值的內容整理出來供大家查閱。但為了保護群友隱私,需要把暱稱和頭像都打碼,如果碰到幾百條聊天記錄,這樣做簡直要吐血。而且也不能截一張長圖,只能一張一張擷取,然後拼接起來。群聊記錄只能在微信內分享,這也限制了傳播的渠道。為了提高小整合員工作效率,想著能不能給微信做個外掛,解決這些問題。我們一直在追求如何更有效率開展我們的工作,比如使用指令碼自動整理每週小集內容,使用微信小程式給讀者更好閱讀體驗。(呀,還有指令碼,如果你還不知道,那肯定沒有點 star 吧, 傳送門 )
知識小集公眾號
知識小集是一個團隊公眾號,每週都會有 原創 文章分享,我們的文章都會在公眾號首發。知識小集微信群,短短几周時間,目前群友已經300+人,很快就要達到上限(抓住機會哦),關注公眾號獲取加群方式。
提出問題
通過以上痛點,可以確定我們要解決的問題主要有:
- 截圖:把所有的聊天記錄截一張長圖並儲存到相簿;
- 截圖(馬賽克):把所有的聊天記錄截一張長圖並儲存到相簿,需要把群友的頭像和暱稱打碼;
- 預覽:預覽打碼後的效果。
如何解決
下面這張圖是聊天記錄頁面,點選導航右邊按鈕,會彈出 ActionSheet。從圖中可以看出,新增截圖功能,在 ActionSheet 上新增是不錯的選擇。
我們下面主要的工作是:
- 1.拿到 ActionSheet 並新增3個選單(截圖,截圖(馬賽克),預覽)
- 2.找到聊天記錄所在的頁面,找到展示訊息的檢視;
- 3.獲取 ActionSheet 點選選單對應的事件;
- 4.實現擷取長圖並儲存到相簿功能;
- 5.對頭像和暱稱打馬賽克;
- 6.新增版權資訊。
實現
本文使用 工具開發(無需越獄),重點是教你如何開發一個微信外掛,並不打算介紹工具。關注我們的朋友應該都知道,以往的 #iOS知識小集 中我已經介紹了三個工具的使用。這三個工具在下面會用到。
給 ActionSheet 新增3個選單
使用 Reveal 工具檢視聊天記錄頁面對應的 VC,ActionSheet 對應的類名。如何使用 Reveal 除錯第三方 APP,網上有很多教程。使用 無需越獄。
通過上圖可以看到聊天記錄所對應的VC是 MsgRecordDetailViewController
,使用 MMTableView
展示聊天內容。彈出的 ActionSheet 對應的類為 WCActionSheet
UIWindow
。 那麼我們看看這幾個類中的內容吧。使用 class-dump 檢視第三方 APP 的標頭檔案。在 #iOS知識小集 中已經介紹過這個工具的使用。
在 MsgRecordDetailViewController
的標頭檔案中發現有一個 WCActionSheet *favImgLongPressAction;
我們可以斷定出 WCActionSheet
就是我們要找的 ActionSheet。好了,接下來主要就是看 WCActionSheet
的標頭檔案,挖掘有用的資訊。
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;
我們的目標是給 WCActionSheet
新增3個選單。下面這些方法似乎對我們有用。目前想到有兩種方案:
1.在 buttonTitleList 中新增一個物件
我們所關心的最主要的問題是 buttonTitleList
中存放的的物件是什麼?需要使用Cycript工具,這個工具在以往的 #iOS知識小集 介紹過,想了解的朋友可以在知識小集小程式中搜索 Cycript除錯第三方APP 。
通過 Cycript 可以看到 buttonTitleList
中存放的物件是 WCActionSheetItem
。我們看看 WCActionSheetItem
的標頭檔案,發現其實就是一個 Model 物件,用來表示選單的標題,顏色等等。
@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
例項即可。
2.直接呼叫 addButtonWithTitle:
方法
從上圖可以看出直接呼叫 addButtonWithTitle:
這個方法,返回一個 Index 為 3,說明可以直接呼叫這個方法。
接下來主要的問題是,找到新增選單的時機。第一想到的是在 WCActionSheetDelegate
的代理中新增選單,果斷在 MsgRecordDetailViewController
中 Hook 下面這3個代理方法,但是通過實驗證明,發現最後兩個方法並沒有被呼叫,因為 MsgRecordDetailViewController
並沒有實現這兩個代理,只好放棄了這種思路。
- (void)actionSheet:(id)arg1 clickedButtonAtIndex:(long long)arg2;
- (void)didPresentActionSheet:(WCActionSheet *)arg1;
- (void)willPresentActionSheet:(WCActionSheet *)arg1;
無奈之下看到 WCActionSheet
中有個 showInView:
方法, 可以直接 Hook 這個方法。但這樣導致所有的 WCActionSheet
都會被添加了額外的選單。而我們的目的只是在聊天記錄頁中的 WCActionSheet
顯示截圖選單。所以用 [WeChatSaveData defaultSaveData].isNeedAddMenu
加了一個判斷,isNeedAddMenu 在 MsgRecordDetailViewController
頁面將要出現的時候,設定成 YES,在頁面將要消失的時候,設定成 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);
}
執行結果如下圖:
找到聊天記錄頁面和展示訊息的檢視
MsgRecordDetailViewController標頭檔案
@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 的代理中獲取頭像和暱稱對應的 View,然後替換 View 的內容即可。需要 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 如果當前是要打碼截圖,需要對頭像和暱稱的內容做處理。這裡做一個特殊的處理,頭像和暱稱我們換成三國人物的頭像的名字。
+ (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];
}
}
}
}
新增版權資訊
給繪製的長圖新增一個小集的版權 by公眾號 知識小集 。
最終效果(伴隨著咔嚓一聲,一張被打碼的照片儲存到了相簿,你可以在任意的渠道分享了):
踩坑
WCActionSheet的代理方法Index不對
新增額外的選單後,WCActionSheet 的代理方法 actionSheet: clickedButtonAtIndex:
中的 Index 點選取消或空白區域總為 2,也就是我新增選單第一個的 Index。導致每次點選取消或空白區域時都會聽到咔嚓一聲截圖。解決方法,就是加一個沒有標題的選單,並且高度為 0。
收藏的聊天記錄也需要有截圖功能
按著這個思路給收藏中的聊天記錄新增截圖功能,這就是你為什麼會在原始碼中看到 FavRecordDetailViewController
的 Hook。