1、明确需求

在发现页添加两行信息,自动抢红包和退出微信,且开关状态需要重启App后也能保存,实现退出微信功能。
image.png

2、思路整理

第一步:找到需要hook的类
通过Reveal查看微信发现页的布局,是一个TableView,类名为MMMainTableView:
image.png
如果想修改TableView的显示内容就需要修改其dataSource中的方法,利用Cycript查看MMMainTableView的dataSource:
cy# #0x1164dde00.dataSource

  1. #"<FindFriendEntryViewController: 0x115ba8c00>"

通过打印可以看到是一个类名为FindFriendEntryViewController的控制器,再获取正在显示的视图控制器验证一下:
cy# MJFrontVc()

  1. #"<FindFriendEntryViewController: 0x115ba8c00>"

这样就可以确定了MMMainTableView的dataSource就是FindFriendEntryViewController,也就是“发现页”。
第二步:分析头文件
脱壳后通过class-dump导出微信可执行文件的头文件,找到FindFriendEntryViewController.h:
image.png
可以在头文件中看到numberOfSectionsInTableView等TableView的dataSource方法,下面就可以通过hook这些方法来修改发现页的显示了。

3、编写代码

3.1、添加两行自定义Cell

hook了UITableViewDataSource的几个方法,添加了两个Cell:

  1. #import "FindFriendEntryViewController.h"
  2. %hook FindFriendEntryViewController
  3. // 一共有多少组 %orig代表调用原始方法,参数可以传也可以不传
  4. - (long long)numberOfSectionsInTableView:(id)arg1 {
  5. // 返回原始个数 + 1
  6. return %orig + 1;
  7. }
  8. // 每组有多少行
  9. - (long long)tableView:(id)arg1 numberOfRowsInSection:(long long)arg2 {
  10. [self numberOfSectionsInTableView:arg1];
  11. // 不是最后一组,返回原始行数
  12. if (arg2 != [self numberOfSectionsInTableView:arg1] - 1) {
  13. return %orig;
  14. }
  15. // 最后一组,返回2行
  16. return 2;
  17. }
  18. // 设置行高,通过Reveal查看原始行高
  19. - (double)tableView:(id)arg1 heightForRowAtIndexPath:(id)arg2 {
  20. // 不是最后一组,返回原始高度
  21. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  22. return %orig;
  23. }
  24. // 最后一组,返回自定义高度
  25. return 56;
  26. }
  27. // 每一行的cell
  28. - (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2 {
  29. // 不是最后一组,返回原始cell
  30. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  31. return %orig;
  32. }
  33. // 最后一组返回自定义cell
  34. UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"XLCellID"];
  35. cell.backgroundColor = [UIColor whiteColor];
  36. cell.textLabel.text = @"New cell";
  37. return cell;
  38. }
  39. %end

知识点:
%orig:调用hook的原始方法,参数可以传也可以不传
[arg2 section]:class-dump导出的头文件参数都是id类型,所以调用属性时不能使用点语法,只能通过方法调用
image.png

3.2、添加开关、退出功能

添加了UISwitch开关和方法,添加了退出微信的功能:

  1. #define XLDefaults [NSUserDefaults standardUserDefaults]
  2. #define XLAutoSwitchKey @"XLAutoSwitchKey"
  3. #import "FindFriendEntryViewController.h"
  4. %hook FindFriendEntryViewController
  5. // 一共有多少组 %orig代表调用原始方法,参数可以传也可以不传
  6. - (long long)numberOfSectionsInTableView:(id)arg1 {
  7. // 返回原始个数 + 1
  8. return %orig + 1;
  9. }
  10. // 每组有多少行
  11. - (long long)tableView:(id)arg1 numberOfRowsInSection:(long long)arg2 {
  12. [self numberOfSectionsInTableView:arg1];
  13. // 不是最后一组,返回原始行数
  14. if (arg2 != [self numberOfSectionsInTableView:arg1] - 1) {
  15. return %orig;
  16. }
  17. // 最后一组,返回2行
  18. return 2;
  19. }
  20. // 设置行高
  21. - (double)tableView:(id)arg1 heightForRowAtIndexPath:(id)arg2 {
  22. // 不是最后一组,返回原始高度
  23. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  24. return %orig;
  25. }
  26. // 最后一组,返回自定义高度
  27. return 56;
  28. }
  29. // 每一行的cell
  30. - (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2 {
  31. // 不是最后一组,返回原始cell
  32. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  33. return %orig;
  34. }
  35. UITableViewCell *cell = nil;
  36. if ([arg2 row] == 0) {
  37. static NSString* cellId = @"xl_cellID_Auto";
  38. cell = [arg1 dequeueReusableCellWithIdentifier:cellId];
  39. if (cell == nil) {
  40. cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
  41. cell.backgroundColor = [UIColor whiteColor];
  42. }
  43. cell.textLabel.text = @"自动抢红包";
  44. UISwitch *switchView = [[UISwitch alloc] init];
  45. switchView.on = [XLDefaults boolForKey:XLAutoSwitchKey];
  46. [switchView addTarget:self action:@selector(xl_autoChange:) forControlEvents:UIControlEventValueChanged];
  47. cell.accessoryView = switchView;
  48. }
  49. if ([arg2 row] == 1) {
  50. static NSString* cellId = @"xl_cellID_Auto";
  51. cell = [arg1 dequeueReusableCellWithIdentifier:cellId];
  52. if (cell == nil) {
  53. cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
  54. cell.backgroundColor = [UIColor whiteColor];
  55. }
  56. cell.textLabel.text = @"退出微信";
  57. }
  58. return cell;
  59. }
  60. // 点击监听
  61. - (void)tableView:(id)arg1 didSelectRowAtIndexPath:(id)arg2 {
  62. // 不是最后一组,实现原始点击方法
  63. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  64. return %orig;
  65. }
  66. // 取消选中
  67. [arg1 deselectRowAtIndexPath:arg2 animated:YES];
  68. // 退出程序
  69. if ([arg2 row] == 1) {
  70. abort();
  71. }
  72. }
  73. // 开关方法,新增方法需要添加%new,并且添加前缀,避免重复
  74. %new
  75. - (void)xl_autoChange:(UISwitch *)switchView {
  76. [XLDefaults setBool:switchView.isOn forKey:XLAutoSwitchKey];
  77. [XLDefaults synchronize];
  78. }
  79. %end

知识点:
abort():abort()退出程序的方法会比exit(0)时间短
%new:在%hook中的方法默认都会被认为是需要hook的方法,也就是原类已经存在的方法。所以如果添加新的方法时,需要在方法前添加%new标明这是新的方法。
image.png

3.3、添加图标

添加了图片资源:

  1. #define XLDefaults [NSUserDefaults standardUserDefaults]
  2. #define XLAutoSwitchKey @"XLAutoSwitchKey"
  3. #define XLFile(path) @"/Library/Caches/XLWeChat/" #path
  4. #import "FindFriendEntryViewController.h"
  5. %hook FindFriendEntryViewController
  6. // 一共有多少组 %orig代表调用原始方法,参数可以传也可以不传
  7. - (long long)numberOfSectionsInTableView:(id)arg1 {
  8. // 返回原始个数 + 1
  9. return %orig + 1;
  10. }
  11. // 每组有多少行
  12. - (long long)tableView:(id)arg1 numberOfRowsInSection:(long long)arg2 {
  13. [self numberOfSectionsInTableView:arg1];
  14. // 不是最后一组,返回原始行数
  15. if (arg2 != [self numberOfSectionsInTableView:arg1] - 1) {
  16. return %orig;
  17. }
  18. // 最后一组,返回2行
  19. return 2;
  20. }
  21. // 设置行高
  22. - (double)tableView:(id)arg1 heightForRowAtIndexPath:(id)arg2 {
  23. // 不是最后一组,返回原始高度
  24. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  25. return %orig;
  26. }
  27. // 最后一组,返回自定义高度
  28. return 56;
  29. }
  30. // 每一行的cell
  31. - (id)tableView:(id)arg1 cellForRowAtIndexPath:(id)arg2 {
  32. // 不是最后一组,返回原始cell
  33. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  34. return %orig;
  35. }
  36. // 自定义cell
  37. UITableViewCell *cell = nil;
  38. NSString* cellId = [arg2 row] == 0 ? @"xl_cellID_Auto" : @"xl_cellID_Auto";
  39. cell = [arg1 dequeueReusableCellWithIdentifier:cellId];
  40. if (cell == nil) {
  41. cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
  42. cell.backgroundColor = [UIColor whiteColor];
  43. // 图片
  44. cell.imageView.image = [UIImage imageWithContentsOfFile:XLFile(Alien.png)];
  45. }
  46. if ([arg2 row] == 0) {
  47. cell.textLabel.text = @"自动抢红包";
  48. //开关
  49. UISwitch *switchView = [[UISwitch alloc] init];
  50. switchView.on = [XLDefaults boolForKey:XLAutoSwitchKey];
  51. [switchView addTarget:self action:@selector(autoChange:) forControlEvents:UIControlEventValueChanged];
  52. cell.accessoryView = switchView;
  53. }
  54. if ([arg2 row] == 1) {
  55. cell.textLabel.text = @"退出微信";
  56. }
  57. return cell;
  58. }
  59. // 点击监听
  60. - (void)tableView:(id)arg1 didSelectRowAtIndexPath:(id)arg2 {
  61. // 不是最后一组,实现原始点击方法
  62. if ([arg2 section] != [self numberOfSectionsInTableView:arg1] - 1) {
  63. return %orig;
  64. }
  65. // 取消选中
  66. [arg1 deselectRowAtIndexPath:arg2 animated:YES];
  67. // 退出程序
  68. if ([arg2 row] == 1) {
  69. abort();
  70. }
  71. }
  72. // 开关方法,新增方法需要添加%new,并且添加前缀,避免重复
  73. %new
  74. - (void)xl_autoChange:(UISwitch *)switchView {
  75. [XLDefaults setBool:switchView.isOn forKey:XLAutoSwitchKey];
  76. [XLDefaults synchronize];
  77. }
  78. %end

知识点:
layout:theos有规定,打包资源需要在项目的跟路径新建一个layout文件夹,并把资源添加到layout文件夹内,资源在layout文件夹中的路径也就对应着在iPhone上存储的路径(Layout/xxx/xxx/1.png -> Device/xxx/xxx/1.png)。资源最好保存在/Library/Caches/
宏定义资源路径:根据OC语法,下面两行代码是等价的:

  1. @"abcdef"
  2. @"abc" "def"

所以资源路径可以定义为:

  1. #define XLFile(path) @"/Library/Caches/XLWeChat/" #path

path 是宏定义语法:# + 参数 会自动给参数添加双引号

4、最终效果

image.png

5、错误处理

5.1、self报错

使用self调用方法时,会报下面错误:

  1. Tweak.x:14:3: error: receiver type 'FindFriendEntryViewController' for instance message is a forward declaration
  2. [self numberOfSectionsInTableView:arg1];
  3. ^~~~
  4. Tweak.x:25:8: note: forward declaration of class here
  5. @class FindFriendEntryViewController;
  6. ^

需要在Tweak.x同级目录下新建一个hook类的同名头文件,并在Tweak.x中引用这个头文件:

  1. @interface FindFriendEntryViewController
  2. @end
  1. #import "FindFriendEntryViewController.h"

或者直接在Tweak.x中声明:

  1. @interface FindFriendEntryViewController
  2. @end

5.2、找不到方法

make时会报错没有声明方法:

  1. Tweak.x:14:8: error: no visible @interface for 'FindFriendEntryViewController' declares the selector 'numberOfSectionsInTableView:'
  2. [self numberOfSectionsInTableView:arg1];
  3. ~~~~ ^~~~~~~~~~~~~~~~~~~~~~~~~~~

self调用的方法需要在头文件里进行声明,但是由于现在hook的方法都是UITableViewDataSource中的方法,所以让FindFriendEntryViewController遵守UITableViewDataSource即可,并且导入UIKit头文件:

  1. #import <UIKit/UIKit.h>
  2. @interface FindFriendEntryViewController<UITableViewDataSource>
  3. @end

如果不是协议中的方法,可以直接添加方法:

  1. #import <UIKit/UIKit.h>
  2. @interface FindFriendEntryViewController<UITableViewDataSource>
  3. - (void)function(id)arg;
  4. @end