一、界面分析
要在微信中加入功能,首先要分析界面找到切入点。假如我们要在微信的设置界面加入自动抢红包的配置。
进入设置界面分析页面。可以通过cycript
或者view debug
分析(reveal
也可以)。

定位到控制器是
NewSettingViewController<0x12648ee00>
,这个控制器视图是典型的tableView
。
view debug
挂载的时候有可能失败,杀掉重来就可以了。
cycript
也可以定位到
cy# HPCurrentVC()
#"<NewSettingViewController: 0x12648ee00>"
我们现在需要定位到数据源,那么要先定位到TableView
。
cy# pviews()

这样就找到了
WCTableView <0x12651d000>
。
获取datasource
发现数据源是WCTableViewManager< 0x281aeffc0 >
cy# #0x12651d000.dataSource
#"<WCTableViewManager: 0x281aeffc0>"
这个时候我们就找到了NewSettingViewController<0x12648ee00>
以及数据源WCTableViewManager< 0x281aeffc0 >
。
现在需要做的就是找到数据源和视图控制器的对应关系。
看下数据源WCTableViewManager< 0x281aeffc0 >
都有什么东西。
//tableView
cy# #0x281aeffc0.tableView
#"<WCTableView: 0x12651d000; baseClass = UITableView; frame = (0 0; 375 667); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x281aec2a0>; layer = <CALayer: 0x2816f0680>; contentOffset: {0, -64}; contentSize: {375, 621}; adjustedContentInset: {64, 0, 0, 0}; dataSource: <WCTableViewManager: 0x281aeffc0>>"
//sections
cy# #0x281aeffc0.sections
@[#"<WCTableViewSectionManager: 0x283c8a5a0>",#"<WCTableViewSectionManager: 0x283c8a7d0>",#"<WCTableViewSectionManager: 0x283c8a990>",#"<WCTableViewSectionManager: 0x283c8aae0>",#"<WCTableViewSectionManager: 0x283c8abc0>",#"<WCTableViewSectionManager: 0x283c8aca0>"]
这样就验证了数据源中有tableView
和sections
。那么这里section
正好6
个和界面对应,修改tableView
的背景色:
cy# #0x12651d000.backgroundColor = [UIColor redColor]
#"UIExtendedSRGBColorSpace 1 0 0 1"
验证了这个tableView
就是当前页面的tableView
。
接着导出头文件:
class-dump -H WeChat -o ./Headers
在头文件中查找WCTableViewManager
:
@interface WCTableViewManager : NSObject <UITableViewDelegate, UITableViewDataSource, tableViewDelegate>
{
MMTableView *_tableView;
NSMutableArray *_sections;
}
- (void)tableView:(id)arg1 didSelectRowAtIndexPath:(id)arg2;
- (double)tableView:(id)arg1 heightForRowAtIndexPath:(id)arg2;
- (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2;
- (long long)tableView:(id)arg1 numberOfRowsInSection:(long long)arg2;
- (long long)numberOfSectionsInTableView:(id)arg1;
确认WCTableViewManager
持有了tableView
和sections
。并且提供了tableView
相关的代理。
二、界面分析找到注入点
通过上面的分析,我们要在设置页面加一个抢红包设置模块,假如有一个开关,有一个刷新时间。那么我们需要加一个section
和两个cell
。
那么我们需要修改WCTableViewManager
的以下方法达到目的:
- (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2;
- (long long)tableView:(id)arg1 numberOfRowsInSection:(long long)arg2;
- (long long)numberOfSectionsInTableView:(id)arg1;
这样我们基本的hook
框架就搭建好了:
%hook WCTableViewManager
//返回cell
- (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2 {
return %orig;
}
//每一组多少数据
- (long long)tableView:(id)arg1 numberOfRowsInSection:(long long)arg2 {
return %orig;
}
//返回组
- (long long)numberOfSectionsInTableView:(id)arg1 {
return %orig;
}
%end
由于已经知道了这三个方法是tableView
的代理方法,所以优化下参数:
%hook WCTableViewManager
//返回cell
- (id)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return %orig;
}
//每一组多少数据
- (long long)tableView:(UITableView *)tableView numberOfRowsInSection:(long long)section {
return %orig;
}
//返回组
- (long long)numberOfSectionsInTableView:(UITableView *)tableView {
return %orig;
}
%end
三、通过logos修改微信设置界面
首先要修改返回的分组,需要增加一组。
//返回组
- (long long)numberOfSectionsInTableView:(UITableView *)tableView {
//获取成员变量,通过声明sections或者kvc也可以
NSMutableArray *arr = MSHookIvar<NSMutableArray*>(self,"_sections");
NSLog(@"数据个数%ld,orig:%ld",arr.count,%orig);
return %orig;
}
在调试的过程中发现我的页面和设置页面都用到了WCTableViewManager
,通过cycript
也可以验证:
cy# choose(WCTableViewManager)
[#"<WCTableViewManager: 0x28293a610>",#"<WCTableViewManager: 0x282a41ec0>"]
那么由于WCTableViewManager
是个通用类,那么我们只能修改NewSettingViewController
页面的。那么可以通过响应链条来找到控制器。
cy# choose(WCTableViewManager)
[#"<WCTableViewManager: 0x28293a610>",#"<WCTableViewManager: 0x282a41ec0>"]
cy# #0x28293a610.tableView.nextResponder.nextResponder
#"<NewSettingViewController: 0x112cec600>"
cy# #0x282a41ec0.tableView.nextResponder.nextResponder.nextResponder
#"<MoreViewController: 0x112c68a00>"
这样就能通过WCTableViewManager
找到NewSettingViewController
了。
判断是否设置页面
由于其它代理方法中也要有类似逻辑,所以直接封装个方法。为了其它方法中调用能编译通过需要声明:
//为了编译通过
@interface WCTableViewManager
- (BOOL)isNewSettingVC:(UITableView *)tableView;
@end
//由于WCTableViewManager是个通用工具,那么需要定位到设置页面才修改。
%new
- (BOOL)isNewSettingVC:(UITableView *)tableView {
if ([tableView.nextResponder.nextResponder isKindOfClass: %c(NewSettingViewController)]) { //是设置页面
return YES;
}
return NO;
}
注入后完整代码如下:
#import <UIKit/UIKit.h>
//为了编译通过
@interface WCTableViewManager
- (BOOL)isNewSettingVC:(UITableView *)tableView;
- (long long)numberOfSectionsInTableView:(UITableView *)tableView;
@end
%hook WCTableViewManager
- (double)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
//高度在模型数组中,不设置为0。
//double height = %orig;
if ([self isNewSettingVC:tableView] && (indexPath.section == [self numberOfSectionsInTableView:tableView] - 1)) { //设置页面 & 最后一组
return 56.0;
}
return %orig;
}
//返回cell
- (id)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self isNewSettingVC:tableView] && (indexPath.section == [self numberOfSectionsInTableView:tableView] - 1)) { //设置页面 & 最后一组
if(indexPath.row == 0) {//开关cell
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"SwitchCell"];
cell.backgroundColor = [UIColor whiteColor];
cell.textLabel.text = @"SwitchCell";
return cell;
} else if(indexPath.row == 1){//时间cell
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"TimeCell"];
cell.backgroundColor = [UIColor whiteColor];
cell.textLabel.text = @"TimeCell";
return cell;
}
}
return %orig;
}
//每一组多少数据
- (long long)tableView:(UITableView *)tableView numberOfRowsInSection:(long long)section {
if ([self isNewSettingVC:tableView] && (section == [self numberOfSectionsInTableView:tableView] - 1)) { //设置页面 & 最后一组
return 2;
}
return %orig;
}
//返回组
- (long long)numberOfSectionsInTableView:(UITableView *)tableView {
if ([self isNewSettingVC:tableView]) {
// //获取成员变量,通过声明sections或者kvc也可以
// NSMutableArray *arr = MSHookIvar<NSMutableArray*>(self,"_sections");
// NSLog(@"数据个数%ld,orig:%ld",arr.count,%orig);
return %orig + 1;//多加1组
}
return %orig;
}
//由于WCTableViewManager是个通用工具,那么需要定位到设置页面才修改。
%new
- (BOOL)isNewSettingVC:(UITableView *)tableView {
if ([tableView.nextResponder.nextResponder isKindOfClass: %c(NewSettingViewController)]) { //是设置页面
NSLog(@"当前页面是设置页面");
return YES;
}
return NO;
}
%end

这个时候需要的两个
cell
就已经注入了。
四、完善cell
添加cell
的UI
:
//返回cell
- (id)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self isNewSettingVC:tableView] && (indexPath.section == [self numberOfSectionsInTableView:tableView] - 1)) { //设置页面 & 最后一组
UITableViewCell *cell = nil;
if(indexPath.row == 0) {//开关cell
cell = [tableView dequeueReusableCellWithIdentifier:@"SwitchCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"SwitchCell"];
}
cell.textLabel.text = @"自动抢红包";
//开关
BOOL switchOn = [HPDefaults boolForKey:HPSwithKey];
UISwitch *switchView = [[UISwitch alloc] init];
switchView.on = switchOn;
[switchView addTarget:self action:@selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = switchView;
cell.imageView.image = [UIImage imageNamed:switchOn ? @"HP_switch_on" : @"HP_switch_off"];
} else if(indexPath.row == 1){//时间cell
cell = [tableView dequeueReusableCellWithIdentifier:@"TimeCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"TimeCell"];
}
cell.textLabel.text = @"等待时间(秒)";
//时间输入框
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 150, 40)];
textField.text = [HPDefaults valueForKey:HPTimeKey];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.keyboardType = UIKeyboardTypeDecimalPad;
cell.accessoryView = textField;
cell.imageView.image = [UIImage imageNamed:@"HP_time"];
}
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
return %orig;
}
%new
- (void)switchChanged:(UISwitch *)switchView {
[HPDefaults setBool:switchView.isOn forKey:HPSwithKey];
[HPDefaults synchronize];
[MSHookIvar<UITableView*>(self,"_tableView") reloadData];
}
- 通过
NSUserDefaults
持久化数据。 - 通过将资源文件加入到
.app
中重新打包从而注入资源文件。
资源文件添加
1.可以将Bundle
资源和storyboard
添加到Resources
中:
image.png
2.也可以直接放入到Copy Bundle Resouces
:
image.png
3.将资源文件加入到.app
中重新打包zip -ry WeChat.ipa Payload/
最终视图如下:

五、页面优化
设置时间持久化:
//时间cell中
//监听变化,微信目前支持iOS11以上,所以可以不移除。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldDidChangeValue:) name:UITextFieldTextDidChangeNotification object:textField];
%new
- (void)textFieldDidChangeValue:(NSNotification *)notification {
UITextField *textField = (UITextField *)[notification object];
[HPDefaults setValue:textField.text forKey:HPTimeKey];
[HPDefaults synchronize];
}
在输入时间时发现当前页面没有管理键盘的弹出,所以需要增加对键盘的监听处理,那么键盘需要针对页面而言,所以在NewSettingViewController
页面中监听键盘,直接在viewDidLoad
中添加:
#define HP_SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define HP_SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
@interface NewSettingViewController : UIViewController
@end
%hook NewSettingViewController
- (void)viewDidLoad {
%orig;
//监听键盘
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}
%new
- (void)keyboardWillShow:(NSNotification *)notification {
UIView *view = self.view;
//这里是改变view的frame并不是Tableiew的。由viewdebug可以看出来TabelView在整个view上面,在这里挪动view最快捷。
CGRect keyBoardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
view.frame = CGRectMake(0, -keyBoardRect.size.height, HP_SCREEN_WIDTH, HP_SCREEN_HEIGHT);
}
%new
- (void)keyboardWillHide:(NSNotification *)notification {
UIView *view = self.view;
view.frame = CGRectMake(0, 0, HP_SCREEN_WIDTH, HP_SCREEN_HEIGHT);
}
%end
在键盘的操作中,改变的是view
的frame
并不是TableView
的。由viewdebug
可以看出来TabelView
在整个view
上面,在这里挪动view
最快捷。
这个时候页面能够正常展示了,但是键盘收起没有触发。分析数据源发现有scrollView
相关的代理,那么在 拖拽事件中处理键盘收起:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
%orig;
if ([self isNewSettingVC:scrollView]) {
[MSHookIvar<UITableView *>(self,"_tableView") endEditing:YES];
}
}
至此整个UI
逻辑就完成了,当然可以对输入框的输入和剪切板粘贴等做进一步限制处理。
详细代码逻辑见Demo
网友评论