1. 程式人生 > >給微信聊天記錄新增截圖功能

給微信聊天記錄新增截圖功能

有時候,知識小叢集裡討論的技術問題,比較有價值,我們會把有價值的內容整理出來供大家查閱。但為了保護群友隱私,需要把暱稱和頭像都打碼,如果碰到幾百條聊天記錄,這樣做簡直要吐血。而且也不能截一張長圖,只能一張一張擷取,然後拼接起來。群聊記錄只能在微信內分享,這也限制了傳播的渠道。為了提高小整合員工作效率,想著能不能給微信做個外掛,解決這些問題。我們一直在追求如何更有效率開展我們的工作,比如使用指令碼自動整理每週小集內容,使用微信小程式給讀者更好閱讀體驗。(呀,還有指令碼,如果你還不知道,那肯定沒有點 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 MsgRecordDetailViewControllerviewWillAppear: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。