有时候,知识小集群里讨论的技术问题,比较有价值,咱们会把有价值的内容整理出来供你们查阅。但为了保护群友隐私,须要把昵称和头像都打码,若是碰到几百条聊天记录,这样作简直要吐血。并且也不能截一张长图,只能一张一张截取,而后拼接起来。群聊记录只能在微信内分享,这也限制了传播的渠道。为了提升小集成员工做效率,想着能不能给微信作个插件,解决这些问题。咱们一直在追求如何更有效率开展咱们的工做,好比使用脚本自动整理每周小集内容,使用微信小程序给读者更好阅读体验。(呀,还有脚本,若是你还不知道,那确定没有点 star 吧,传送门)node
经过以上痛点,能够肯定咱们要解决的问题主要有:git
下面这张图是聊天记录页面,点击导航右边按钮,会弹出 ActionSheet。从图中能够看出,添加截图功能,在 ActionSheet 上添加是不错的选择。github
咱们下面主要的工做是:小程序
本文使用 MonkeyDev 工具开发(无需越狱),重点是教你如何开发一个微信插件,并不打算介绍工具。关注咱们的朋友应该都知道,以往的 #iOS知识小集 中我已经介绍了三个工具的使用。这三个工具在下面会用到。微信小程序
使用 Reveal 工具查看聊天记录页面对应的 VC,ActionSheet 对应的类名。如何使用 Reveal 调试第三方 APP,网上有不少教程。使用MonkeyDev无需越狱。bash
经过上图能够看到聊天记录所对应的VC是 MsgRecordDetailViewController
,使用 MMTableView
展现聊天内容。弹出的 ActionSheet 对应的类为 WCActionSheet
, 能够发现它是一个 UIWindow
。 那么咱们看看这几个类中的内容吧。微信
使用 class-dump 查看第三方 APP 的头文件。在 #iOS知识小集 中已经介绍过这个工具的使用。app
在MsgRecordDetailViewController
的头文件中发现有一个 WCActionSheet *favImgLongPressAction;
咱们能够判定出 WCActionSheet
就是咱们要找的 ActionSheet。好了,接下来主要就是看 WCActionSheet
的头文件,挖掘有用的信息。工具
WCActionSheet头文件ui
@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。
逆向的整个流程能够归结为:
而本文也是按这几个步骤逐渐实现的:
本插件以开源 前往查看 ,若是想安装在本身的手机上,须要安装 MonkeyDev,把图中的文件导入便可
知识小集是一个团队公众号,每周都会有原创文章分享,咱们的文章都会在公众号首发。知识小集微信群,短短几周时间,目前群友已经300+人,很快就要达到上限(抓住机会哦),关注公众号获取加群方式。