有时候知识集群里讨论的技术问题更有价值,我们会整理出有价值的内容给大家参考。但是为了保护群友的隐私,需要对昵称和头像进行编码。如果遇到几百条聊天记录,会吐血。而且不能剪长图,只能一张一张地剪,然后拼接在一起。群聊记录只能在微信分享,也限制了交流渠道。为了提高小集成员的工作效率,不知道能不能做一个微信插件来解决这些问题。我们一直在追求如何更高效地开展工作,比如用脚本自动组织每周剧集,用微信小程序给读者更好的阅读体验。(啊,有剧本。如果你还不知道,肯定没有星,传送门)
知识收集是团队微信官方账号。每周分享原创文章,我们的文章会先在微信官方账号发表。知识收集微信群,短短几周,现在群友300人,很快就要达到上限了(抓住机会),关注微信官方账号获取加入群的方式。
通过以上痛点,我们可以确定我们需要解决的主要问题有:
截图:将所有聊天记录剪成一张长图保存到相册;截图(马赛克):将所有聊天记录剪成一张长图保存在相册中,需要对群友的头像和昵称进行编码;预览:编码后预览效果。
下图是聊天记录页面。单击导航右侧的按钮,将弹出操作单。从图中可以看出,将截图功能添加到ActionSheet中是一个不错的选择。
我们的主要工作如下:
1.获取ActionSheet并添加3个菜单(截图、截图(马赛克)、预览)2。找到聊天记录所在的页面,找到显示消息的视图;3.获取ActionSheet点击菜单对应的事件;4.实现截取长图保存到相册的功能;5.马赛克头像和昵称;6.添加版权信息。
本文使用MonkeyDev工具开发(无需越狱),重点教你如何开发一个微信插件,没有介绍工具。关注我们的朋友应该都知道,我在之前的#iOS知识相册中已经介绍了三个工具的使用。这三个工具将在下面使用。
使用显示工具检查聊天历史页面对应的VC和ActionSheet的类名。有很多关于如何使用“显示”调试第三方应用程序的教程。没必要用MonkeyDev越狱。
从上图可以看出,聊天记录对应的VC是MsgRecordDetailViewController,聊天内容由MMTableView显示。弹出的ActionSheet对应的类是WCActionSheet,可以发现它是一个UIWindow。然后我们来看看这些课的内容。
使用类转储查看第三方应用的头文件。#iOS知识文库中介绍了该工具的使用。
在MsgRecordDetailViewController的头文件中找到一个wcactionsheet * favimglongpresaction;我们可以得出结论,WCActionSheet就是我们要找的ActionSheet。好了,下一步主要是看WCActionSheet的头文件,挖掘有用的信息。
@property(强,非原子)nsmutatlearray * button title list;- (void)showInView:(id)ar
g1;- (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个菜单。下面这些方法似乎对我们有用。目前想到有两种方案:
我们所关心的最主要的问题是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实例即可。
从上图可以看出直接调用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。所以需要 HookMsgRecordDetailViewController的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);}
执行结果如下图:
@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"];
首先需要 HookactionSheet: 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 的内容即可。需要 HookcellForRowAtIndexPath这个方法。
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 的代理方法actionSheet: clickedButtonAtIndex:中的 Index 点击取消或空白区域总为 2,也就是我添加菜单第一个的 Index。导致每次点击取消或空白区域时都会听到咔嚓一声截图。解决方法,就是加一个没有标题的菜单,并且高度为 0。
按着这个思路给收藏中的聊天记录添加截图功能,这就是你为什么会在源码中看到FavRecordDetailViewController的 Hook。