利他才能利己


  • 首页

  • 标签

  • 归档

  • 搜索

iOS 逆向: 越狱使用 SSH

发表于 2018-05-12 | 分类于 iOS |

最近在看人工智能相关的知识,无意中发现了一个巨牛的 人工智能教程,分享一下给大家。

教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家。点 这里 可以直接看教程。

越狱

使用苹果设备或者做 iOS 开发的朋友们,几乎都应该听说过越狱这个词. 那么究竟什么是越狱呢?

iOS越狱(英语:iOS Jailbreaking)是获取 iOS 设备的 Root 权限的技术手段。iOS 设备的 Root 权限一般是不开放的。由于获得了Root 权限,在越狱之前无法查看的 iOS 的文件系统也可查看.

iOS 的越狱就类似 Android 获取 root 权限一样的道理,说简单点就是获取系统的 root 权限做一些更高级的事情。

手头上面之前闲置了一台 iPhone4s,闲来无事就把他越狱了,后面不知道怎么操作的,手机变成了白苹果(开机就白屏,无法进入系统),最后没办法,只能升级系统,这台手机也算是废了.

当时使用的工具是 盘古越狱,越狱的 iOS 系统是 7.x. 现在 iOS 已经升级到 11.x 了,很多 APP 和游戏已经不再支持 iOS8 以下的系统了,系统的安全性也越来越好了,越狱破解的难度也随之提高了,目前市面上能越狱的最高版本应该是 10.3.x 的系统(截止到该篇文章写的时候).

为了能够分析和学习一些优秀 App 的设计,我索性买了台iPhone6sPlus 拿来越狱,所以接下来的分享都是基于这台手机的.

越狱工具和设备

[1]. 设备 iPhone6s Plus,系统 iOS10.3.2
[2]. 越狱工具: 爱思助手和 doubleH3lix.ipa

安装并打开 pc 上面的 爱思助手 直接开始越狱,爱思助手会帮您在设备上安装 doubleH3lix. 运行 doubleH3lix 应用,点击 jailbreak 按钮,等待设备自行重启.

手机重启后,你会看到 Cydia 静静的呆在那里了,这种方式的越狱属于非完美越狱,换句话说就是没有真正的破解 iOS 系统.

如果发现手机断电关机或者重启后 Cydia 一打开就闪退,就使用 doubleH3lix 重新越狱就可以了,如果还是不行就需要重新使用 爱思助手 了。

参考越狱教程 64位设备iOS 10-10.3.3爱思助手一键越狱教程.

安装 iOS 终端

安装终端,在 Cydia 中搜索 MTerminal,安装即可.

安装好终端工具后,打开终端,输入

1
su

紧接着让你输入密码,默认的 root 用户密码是 alpine. 输入改密码回车即可。

为了安全起见,修改默认密码 alpine.

1
passwd

连续输入相同的密码即为呢修改后的新密码。

配置 SSH

配置和使用 SSH 功能是后续越狱工作的基础,这样就可以在 MacOS 的终端中访问 iOS 系统了,各种 shell 命令让你倍感亲切,所以大家尽量要完成该步骤. 试想一下,本来手机上面文字输入的体验就不好,还需要在 iPhone 设备上面的终端中敲着命令…没法忍!

iOS 10 越狱以后自带了 SSH,所以不需要单独去 Cydia 下载和安装 OpenSSH.

很多人越狱后,发现也安装了 OpenSSH,但是一旦连接 iPhone 设备就无法连接甚至报错. 如果你已经安装了 OpenSSH 并且无法使用 SSH 来操作你的 iPhone 设备,请继续往下看.

[1]. 打开 Cydia,分别搜索 OpenSSL 和 Openssh,然后分别卸载 OpenSSL 和 Openssh.

[2]. 打开 Cydia,添加源:http://cydia.ichitaso.com/test

点击左上角的 添加,在弹框中输入对应的源地址即可.

[3]. 在 Cydia 中搜索 dropbear 并安装.

安装即可.

安装成功后,一般就可以正常使用 SSH 功能了.

打开 MacOS 终端,开始使用 SSH 连接你的 iPhone 设备.

[1]. 查看 iPhone 设备的(wifi) IP 地址. 如我的是 192.168.1.105.
[2]. 使用 MacOS 终端,输入:

1
ssh root@192.168.1.105

成功的话,它先会警告你是否继续操作,输入 yes 即可,然后又提示你输入 root 密码,密码就是前面说的修改后的密码. 如果你没有修改密码,那就是 alpine. 操作流程如下:

1
2
3
4
5
The authenticity of host '192.168.1.105 (192.168.1.105)' can't be established.
ECDSA key fingerprint is SHA256:ANF7Cvc1yM/ZdaHyz9V1EHjG115cylIcHWyOzCL+kzs.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.1.105' (ECDSA) to the list of known hosts.
root@192.168.1.105's password:

之前在 iOS 10.2.x 越狱修改对应的配置文件,就可以使用 SSH 功能了,但是在 iOS10.3.x 无济于事,如果你是iOS 10.2.x 的越狱手机,可以参考本文末附录配置 SSH,也是我之前实践的结果.

usbmuxd

上面是通过 wifi 来连接 iPhone 设备的,其实还有一种方式,就是借助于 usbmuxd 这个工具通过 USB 连接(手机数据线连接电脑)不需要 wifi,USB连接比 wifi 响应速度快,且无网络环境的限制.

使用 usbmuxd 的前提是你的手机可以正常的使用 SSH 功能,否则 usbmuxd 也无法救你。

[1]. MacOS 安装 usbmuxd

1
brew install usbmuxd

[2]. 使用 usbmuxd 自带工具 iproxy.

iproxy 可以快捷的操作连接 iPhone 等操作.

MacOS 上只支持 4 位的端口号,需要把 iPhone 的默认端口 22 映射到 Mac 上面一个 4 位端口号的端口上面,相当于建立一个 Mac 和 iPhone 之间的通道. iproxy 具体使用,后面再讲. 大家先安装一下即可.

1
2
iproxy 5678 22
waiting for connection

以上命令就是把当前连接设备的 22 端口(SSH端口)映射到电脑的 5678 端口,那么想和设备 22 端口通信,直接和本地的 5678 端口通信就可以了。

终端提示 waiting for connection,表示这两个端口之间已经可以通信了,保留当前终端窗口(如果关闭就停止端口之间的通信了),新建另一个终端输入,默认密码 root 密码。

1
ssh -p 5678 root@127.0.0.1

执行过程如下,类似上面通过 WIFI 操作的方式。

1
2
3
4
5
The authenticity of host '[127.0.0.1]:5678 ([127.0.0.1]:5678)' can't be established.
ECDSA key fingerprint is SHA256:ANF7Cvc1yM/ZdaHyz9V1EHjG115cylIcHWyOzCL+kzs.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[127.0.0.1]:5678' (ECDSA) to the list of known hosts.
root@127.0.0.1's password:

接下来,你就可以在 MacOS 终端上面操作你的 iPhone 设备了,尽情的享受 shell 命令吧。不使用的时候,输入 exit 即可退出 SSH.

附录

[1]. iOS 10.2.x 越狱使用 SSH 功能.

首先修改 root 密码。

iOS 10.2 (越狱后)自带 OpenSSH,不需要自己重新安装 OpenSSH,但是需要修改一个文件才能使用。

在手机上安装 MTERMINAL,运行 su,输入 root 密码,执行:

1
ps aux|grep dropbear

如果你看到的进程没有 dropbear,执行:

1
/usr/local/bin/dropbear -F -R -p 22

然后就可以连接了,重启依然有效。

如果你看到进程是这样的:

1
/usr/local/bin/dropbear -F -R -p 127.0.0.1:22

意思是只能数据线重定向连接 SSH,如果你想改成 WIFI 可以访问,需要修改 /private/var/containers/Bundle/Application/D9185B6D-EA9E-4678-B59C-BF43DEFE67BF/yalu102.app/dropbear.plist
把里面有个参数 127.0.0.1:22 改成22 保存,重启手机即可。

[2]. 使用 usbmuxd 无法使用 SSH

执行 ssh -p 5678 root@127.0.0.1 后报错,如下:

1
ssh_exchange_identification: read: Connection reset by peer

这种情况一般是没有正确的使用数据线连接 Mac 电脑,使用数据线正常连接 Mac 电脑即可解决问题。

越狱系列文章

基于 iOS10.3.1 进行的实践。

  • iOS 逆向: dumpdecrypted 砸壳记

  • iOS 逆向: 砸壳

  • iOS 逆向: 查看系统文件目录和结构

  • iOS 逆向: 越狱使用 SSH

  • dumpdecrypted 砸壳:导出头文件


关注不是目的,交流、学习,一起进步才是关键

RunLoop: NSTimer 实现常驻线程的问题

发表于 2018-05-06 | 分类于 iOS |

可行性

在 常驻线程是一种什么体验 这篇文章中给大家分享了如何利用 RunLoop 的特性, 结合 NSMachPort 实现一个 常驻线程 的主题内容.

今天我们探讨一下使用 NSTimer 如何实现 常驻线程 以及注意事项.

从 RunLoop 的特性来看, 只要有 Source 或者 Timer 都会使其能自循环使用, 不会立即终止当前线程的执行, 所以从理论上来看 NSTimer 是可以达到创建 常驻线程 的目的的.

开始实践

完整的例子代码, 可以从文章的附录获取和查看, 这里只给出核心代码.

创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSThread *)permanentThread {

static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

thread = [[NSThread alloc] initWithTarget:self selector:@selector(asyncRun) object:nil];
[thread setName:@"veryitman-thread"];

// 同一个线程连续多次 start 会导致 crash
[thread start];
});

return thread;
}

线程执行的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)asyncRun {

@autoreleasepool {

NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

// 保持常驻线程: 使用 NSTimer
[self _attachTimerToRunLoop];

NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);

// 执行其他逻辑
//...

// 手动开启 RunLoop
[runLoop run];

NSLog(@"veryitman--asyncRun. End Run.");
}
}

创建定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)_attachTimerToRunLoop {

if (nil == self.timer) {

// scheduledTimerWithTimeInterval 这种方式
// 创建的 Timer 会默认加入到当前的 RunLoop 的 NSDefaultRunLoopMode 中
_timer = [NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:YES];
}
}

可以看出, _attachTimerToRunLoop 中是将 timer 加入到当前的 RunLoop 当中了. 这里注意, repeats 值被设置为 YES 了.

跟之前一样, 可以使用点击事件来模拟和验证常驻线程的有效性.

1
2
3
4
5
6
7
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");

[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

每点击一次屏幕, 都会对应执行 runAnyTime 里面的内容.

对 repeat 的思考

在上面的示例中, 我将 repeat 参数设置为 YES, 试想一下如果把 repeat 参数设置为 NO, 会不会造成常驻线程失效呢?

动手试试…

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)_attachTimerToRunLoop {

if (nil == self.timer) {

// scheduledTimerWithTimeInterval 这种方式
// 创建的 Timer 会默认加入到当前的 RunLoop 的 NSDefaultRunLoopMode 中
_timer = [NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:NO];
}
}

再次点击屏幕若干次, 同样会执行对应函数里面的内容. 这就说明了即使将 repeat 参数设置为 NO, 也不会影响常驻线程.

那我们再来点具有挑战的活动…

将当前页面加入 UIScrollview 这个视图, 还是保持 repeat 参数设置为 NO.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidLoad {

[super viewDidLoad];

self.view.backgroundColor = [UIColor lightGrayColor];
self.navigationItem.title = @"NSTimer 创建常驻线程";

// 加入滚动视图
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.scrollView];
self.scrollView.contentSize = CGSizeMake(1000, 1000);
self.scrollView.delegate = self;

// 启动线程
[self permanentThread];
}

因为加入了滚动视图, 我们换一种方式来模式和验证常驻线程.

在 UIScrollview 代理中来模拟, 示例如下:

1
2
3
4
5
6
7
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");

[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

运行后进入该页面, 可以发现常驻线程被终止了.

1
2
veryitman--timerRun.
veryitman--asyncRun. End Run.

当除我以为更换一下模式即使 将 repeat 参数设置为 NO, 也不会出现常驻线程被终止的问题. 如下面的代码:

1
2
_timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

这样更换模式为 NSRunLoopCommonModes 也不行.

在这种情况(有滚动视图的)下, 将 repeat 参数设置为 YES 就不会导致常驻线程被终止了, 无论哪种方式创建的 Timer.

总结

1.子线程创建中的 RunLoop 的模式不会与主线程中 RunLoop 的模式冲突, 各自运行在各自的 mode 当中.

2.使用 NSTimer 来创建常驻线程, 在有 UIScrollview 或者其子类的情况下, 需要将 repeats 设置为 YES, 否则不会创建常驻线程. 没有滚动视图的情况下, repeats 设置为 NO 也没有关系.

3.创建 NSTimer

下面两种创建 Timer 的效果是一致的.

1
2
3
4
5
[NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:YES];

scheduledTimerWithTimeInterval 默认会将 Timer 加入到当前的 RunLoop 中.

1
2
[NSTimer timerWithTimeInterval:2 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

附录

完整示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#import "MZTimerPermanentThreadController.h"

@interface MZTimerPermanentThreadController () <UIScrollViewDelegate>

@property (nonatomic, strong) NSTimer *timer;

@property (nonatomic, strong) UIScrollView *scrollView;

@end


@implementation MZTimerPermanentThreadController

- (void)dealloc {

NSLog(@"veryitman---MZTimerPermanentThreadController dealloc.");
}

- (void)viewDidLoad {

[super viewDidLoad];

self.view.backgroundColor = [UIColor lightGrayColor];
self.navigationItem.title = @"NSTimer 创建常驻线程";

// 加入滚动视图
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.scrollView];
self.scrollView.contentSize = CGSizeMake(1000, 1000);
self.scrollView.delegate = self;

// 启动线程
[self permanentThread];
}

- (void)viewDidDisappear:(BOOL)animated {

[super viewDidDisappear:animated];

// 取消线程
// 实际业务场景中自行决定 canCancel 的设置, 这里只是示例
BOOL canCancel = YES;
if (canCancel) {

[self.timer invalidate];
_timer = nil;

[[self permanentThread] cancel];
}
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");

[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");

[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

- (NSThread *)permanentThread {

static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

thread = [[NSThread alloc] initWithTarget:self selector:@selector(asyncRun) object:nil];
[thread setName:@"veryitman-thread"];

// 同一个线程连续多次 start 会导致 crash
[thread start];
});

return thread;
}

- (void)asyncRun {

@autoreleasepool {

NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

// 保持常驻线程: 使用 NSTimer
[self _attachTimerToRunLoop];

NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);

// 执行其他逻辑
//...

// 手动开启 RunLoop
[runLoop run];

NSLog(@"veryitman--asyncRun. End Run.");
}
}

- (void)runAnyTime {

NSLog(@"veryitman--runAnyTime. Current Thread: %@", [NSThread currentThread]);
}

- (void)_attachTimerToRunLoop {

if (nil == self.timer) {

// scheduledTimerWithTimeInterval 这种方式
// 创建的 Timer 会默认加入到当前的 RunLoop 的 NSDefaultRunLoopMode 中
_timer = [NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:YES];

#if 0
_timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
#endif
}
}

- (void)runTimer {

NSLog(@"--veryitman--timerRun.");
}

常驻线程是一种什么体验

发表于 2018-04-21 | 分类于 iOS |

简说 RunLoop

关于 iOS RunLoop 网上很多文章都有介绍过, 很多技术面试官也会问关于 RunLoop 的相关知识. 我把自己工作中遇到的问题和总结的经验分享出来(会做成一系列的文章), 也算是对自己的一个总结和沉淀, 欢迎大家交流.

网上的文章基本都是针对于 Apple Developer Doc - Run Loops 这篇来展开的, 所以建议大家认真的去通读这篇文章, 并写代码验证, 实践.

可以简单粗暴的这么理解一下 RunLoop, 基于事件驱动的死循环(由内核来调度和管理的), 在需要处理事情的时候就出来干点事, 否则休眠待命.
RunLoop 的核心是基于 machport 的,其进入休眠时调用的函数是 mach_msg().

类似下面的代码来说明一下:

1
2
3
4
5
6
7
8
BOOL stopRunning = NO;

do {

// 处理 App 中各种操作各种事件
// 点击屏幕, 触摸到硬件也会唤醒 RunLoop

} while(!stopRunning);

说到这里, 随便提及一下, 学习过 Android 开发的同事应该和好理解 RunLoop 了, iOS 的 RunLoop 跟 Android 的 Looper 机制几乎一样, 只是不同的系统之间实现有差异罢了!

有兴趣的朋友可以看一下我之前写的文章 Handler: 更新UI的方法.


今天跟大家分享如何在 iOS 中结合 RunLoop 和 machport 实现常驻线程, 先跟着实例走, 后续再去总结 RunLoop 的各种细节点.

神奇的 main

开发过 iOS 应用中的朋友, 对 main.m 再也熟悉不过了, main 函数正是应用的入口函数.

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

我们将 return 代码分开写, 看看有什么蛛丝马迹可寻.

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char * argv[]) {

@autoreleasepool {

int ret = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

NSLog(@"veryitman--- UIApplicationMain end.");

return ret;
}
}

无论如何你也看不到日志 veryitman--- UIApplicationMain end. 的打印, 这说明 UIApplicationMain 一直在呵护着 APP 的运行, 哈哈.

我们不妨再改一次, 如下:

1
2
3
4
5
6
int main(int argc, char * argv[]) {

@autoreleasepool {
return 0;
}
}

再去运行 APP, 你会发现根本没有让 APP 运行起来, 再次说明没有了 UIApplicationMain 的呵护, APP 无法起死回生.

猜测在 UIApplicationMain 函数中,开启了和主线程相关的 RunLoop,使 UIApplicationMain 不会返回一直在运行中,从而保证了程序的持续运行, 最大的功臣就是 RunLoop.

普通线程

一般我们开启的线程在执行完任务后, 就会结束该线程. 除非你写了类似下面的代码:

1
2
3
while(1) {
// 业务处理
}

或者

1
2
3
while (条件满足) {
// 业务处理
}

开启一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSThread *)permanentThread {

static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

thread = [[NSThread alloc] initWithTarget:self selector:@selector(asyncRun) object:nil];
[thread setName:@"veryitman-thread"];

// 同一个线程连续多次 start 会导致 crash
[thread start];
});

return thread;
}

执行对应的 asyncRun 函数, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)asyncRun {

@autoreleasepool {

NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);

// 执行其他逻辑
//...

NSLog(@"veryitman--asyncRun. End Run.");
}
}

可以发现 asyncRun 很快就可以执行完成 (End Run).

1
2
veryitman--asyncRun. Current Thread: <NSThread: 0x60000066c400>{number = 3, name = veryitman-thread}
veryitman--asyncRun. End Run.

子线程开启 RunLoop

主线程是默认开启 RunLoop 的即 mainRunLoop 是系统默认开启的, 但是子线程中的 RunLoop 需要我们自己手动开启.

关于为什么子线程中需要手动开启, 后续文章结合源码给大家分析, 这里暂时可以理解为获取 RunLoop 对象是一种懒加载模式. 只不过主线程中, 系统帮我们开启了, 然而子线程中需要我们手动开启而已.

类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)asyncRun {

@autoreleasepool {

NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);

// 执行其他逻辑
//...

// 手动开启 RunLoop
[runLoop run];

NSLog(@"veryitman--asyncRun. End Run.");
}
}

在控制台可以看到输出:

1

RunLoop 没有任何输入源(input source) 和定时器(timer), 这时即使开启了 RunLoop 也不会让其等待执行, 换句话说会立即结束当前的 RunLoop.

既然这样我们给子线程的 RunLoop 添加源或者定时器即可. 这里以添加 NSPort 为例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)asyncRun {

@autoreleasepool {

NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

// 添加 source
NSPort *port = [NSMachPort port];
[runLoop addPort:port forMode:NSRunLoopCommonModes];

NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);

// 执行其他逻辑
//...

// 手动开启 RunLoop
[runLoop run];

NSLog(@"veryitman--asyncRun. End Run.");
}
}

再次运行, 你会发现 End Run 这个 Log 是不会打印出来的. 对应当前的 RunLoop 也有了源和定时器, 如图所示:
1

关于定时器和 RunLoop 的结合, 下篇再分享.

现在有这样一个需求, 需要在指定的线程中执行某项任务, 显然使用上面的方法来满足需求, 下面进入今天的正题.

验证常驻线程

一定到 常驻 这个词, 就知道是能够让该线程随时待命, 保证其不挂掉.

iOS 中默认就有个主线程即 main 线程, 我们的 UI 线程指的就是主线程, 一般都是在主线程中操作 UI, 从某个角度来说, 主线程就是一个常驻线程.

我们开启其他线程, 目的是为了异步完成一些任务, 这些任务一般都比较耗时, 如果放在主线程当中完成这些任务就会导致主线程的卡顿, 用户体验极其差.

说了这么多, 也许你会问, 为什么要常驻线程呢?

频繁的创建和销毁线程,会造成资源(主要是内存)的浪费, 我们为什么不让频繁使用的子线程常驻在内存中, 想用的时候就用, 不用的时候让他休眠呢?!

上面已经使用 RunLoop 来实现了让线程长时间存活而不被销毁了.

用 touchesBegan 来模拟在指定线程中再次执行任务(runAnyTime)的方法.

1
2
3
4
5
6
7
8
9
10
11
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");
[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

- (void)runAnyTime {

NSLog(@"veryitman--runAnyTime. Current Thread: %@", [NSThread currentThread]);
}

对应上面的 asyncRun 实现即可, 你会发现在当前页面每次点击屏幕都会执行 runAnyTime.

附录

代码的完整实现

常驻线程, 可以参考具体的注释.

MZCreatePermanentThreadController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@implementation MZCreatePermanentThreadController

- (void)dealloc {

NSLog(@"veryitman---MZCreatePermanentThreadController dealloc.");
}

- (void)viewDidLoad {

[super viewDidLoad];

self.view.backgroundColor = [UIColor lightGrayColor];
self.navigationItem.title = @"创建常驻线程";

// 启动线程
[self permanentThread];
}

- (void)viewDidDisappear:(BOOL)animated {

[super viewDidDisappear:animated];

// 取消线程
// 实际业务场景中自行决定 canCancel 的设置, 这里只是示例
BOOL canCancel = YES;
if (canCancel) {
[[self permanentThread] cancel];
}
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");

[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

- (NSThread *)permanentThread {

static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

thread = [[NSThread alloc] initWithTarget:self selector:@selector(asyncRun) object:nil];
[thread setName:@"veryitman-thread"];

// 同一个线程连续多次 start 会导致 crash
[thread start];
});

return thread;
}

- (void)asyncRun {

@autoreleasepool {

NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

// 添加 source
NSPort *port = [NSMachPort port];
[runLoop addPort:port forMode:NSRunLoopCommonModes];

NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);

// 执行其他逻辑
//...

// 手动开启 RunLoop
[runLoop run];

NSLog(@"veryitman--asyncRun. End Run.");
}
}

- (void)runAnyTime {

NSLog(@"veryitman--runAnyTime. Current Thread: %@", [NSThread currentThread]);
}

参考文档

Toll-Free Bridging

Run Loops

发人员需具备一点产品能力

发表于 2018-04-17 | 分类于 随笔 |

为了开始今天的话题, 我先来借助几个角色, 模拟一下工作中的某些场景.

设立人物

小 A 是名刚入门的技术人员, 比较听话.
小 B 是有工作经验的一名技术人员, 平时喜欢看看产品相关的书籍, 喜欢和产品经理撕逼.
小 C 就厉害了, 不仅技术厉害, 也具有产品思维, 除了阅读产品类的书籍, 还写产品体验报告和体验竞品(和当前自己产品相关的产品).
小 D 就是一名产品经理, 具有一定的产品能力, 数据分析能力和管理能力.

人物已经设立好了, 现在让他们登场.

情景再现: 需求评审

这天风和日丽, 小 D(产品经理) 通知项目组的所有成员, 包括设计/开发/运营/测试等人员按时参加产品需求评审会议.

为了缓解大家的情绪, 小 D 买了很多零食和水果, 也算是犒劳一下大家. 小 D 开始给大家讲需求了, 像往常一样, 拿出精湛的原型, 口沫直飞的向大家展示其绵绵不绝的口才和产品思维. 小 A 一直在点头, 也不知道到底有没有听懂小 D 在说什么, 反正我看到有部分设计人员已经开始打瞌睡了.

突然, 小 C 中断了正在口述的小 D, 小 D 不慌不忙的停止了手中的一切动作, 聚精会神的听小 C 的意见和建议. 小 C 也有条不紊的将刚才的某项需求口述了一遍, 按照小 C 的逻辑, 目前的需求还是存在一些漏洞的, 这个时候, 小 D 开始认真的跟小 C 开始交流, 但是没有马上肯定小 C, 只是说我暂时记下这个点, 回头在思考一番. 产品小 D 稳如老狗, 继续自己的表演.

产品需求基本已经讲完了, 现在是大家提问题和交流的时间, 我看到有些同事揉了揉眼睛, 像是如梦初醒般的看着产品小 D, 不是旁边的同事拉住 Ta, 估计都能冲出会议室. 小 B 也不淡定了, 提出了不少问题, 并从技术的角度说明了实现的难度. 产品经理听的也是一脸懵逼, 心理想: “你实现是否有困难管我鸟事?”.

小 B 在产品小 D 那里并没有得到应有的表扬和鼓励, 反遭到同事小 C 的鄙视, 小 C 说, 你先不要告诉他实现方案, 先讨论这个需求的场景和真伪度. 小 D 默默的对小 C 投过赞赏的目光. 紧接着测试和其他人员提出了几个不痛不痒的问题, 都被老练的小 D 一一破解, 那气势, 啧啧! 势如破竹!

需求评审会议就这样结束了.

情景再现: 需求变更

在产品进入开发和设计阶段, 按理说需求变更也算是比较正常的一件事情.

这年头, 唯一不变的就是变化.

小 B 正在聚精会神的写代码, 突然小 D 跟他说, 这个地方的需求需要修改一下, 你看改动有多大?

小 B 鄙视的看着产品经理小 D, 心理在说改动有多大你心里还没有点 B数嘛. 小 B 还是控制了自己的情绪, 接着说道, 你当初应该好好思考的, 你看我都快做完了, 你才告诉我需要改动.

噼里啪啦的说完, 估计小 B 心理也暗爽了不少, 最终还是接受了这次的变动.

小 A 负责的模块, 也被产品经理修改过, Ta 和产品经理小 D 的交流基本是, 哦, 恩, 好, 可以!

当小 C 找到小 D 的时候, 并没有直接告诉小 D 需要改东改西, 而是问问小 D, 你看这样会不会更好一些?

小 C 当然明白小 D 的目的了, 于是拿出自己看过的竞品, 并说出了自己的意见, 愉快的和小 D 交流后, 居然 TMD 的砍了一个需求. 这让旁边的小 A 和小 B 羡慕不已.

总结

上面的两种情景, 我相信做过开发的同事应该都深有体会, 但是千万不要对号入座, 我只是打个比方.

从上面看出, 小 C 是一个很不错的角色, 无论是思维还是沟通能力都有别与他人, 最重要的是他没有仅仅把自己当做一名开发人员, 而是站在产品的角度去思考问题和解决问题.

需求变更是常态, 作为开发人员, 要最大限度的给产品以支持. 产品经理也是人, 当然有考虑不周的地方, 如果你不能想出更好更完美的解决方案, 请支持他的决定.

多站在别人的角度去思考问题, 换位思考, 才能保证有效的沟通. 作为一名技术人员, 尤其是在互联网行业, 多多少少都应该需要具备一定的产品思维.

不让 SIGPIPE signal 太嚣张

发表于 2018-04-15 | 分类于 iOS |

最近在看人工智能相关的知识,无意中发现了一个巨牛的 人工智能教程,分享一下给大家。

教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家。点 这里 可以直接看教程。

起因

由于项目迭代比较快, 大家还没有来得及做自我调整和总结, 就需要投入到新版本当中开发新功能了.

在最近的一次版本测试和体验过程中, 很多内测用户反馈进入或者退出 App 的聊天室, 会莫名其妙的崩溃掉(Crash).
对于 Crash 的问题, 我们开发同事绝对是零容忍, 于是就开始跟踪问题.

跟踪了很久, 发现这个 Crash 并不是那么的 ‘乖巧’, 很难复现!
既然用户已经反馈了并且后台也有 Crash 上报, 这个问题肯定存在, 所以我们不能放弃.

好吧, 继续加班搞…苦逼中…

最终, 我们发现一个规律, 在日志后台, 看到很多类似下面的日志:

1
Signal 13 was raised. SIGPIPE (_mh_execute_header + 420728)

很遗憾的是, 堆栈信息中没有提供给我们更有力的证据, 所以当时定位在 Signal 13 这个点上面.

也算是有了突破…这班没有白加…

排查问题

既然所有的罪证都指向了 Signal 13, 我们就需要去跟踪它, 去调查它, 去研究它.

在 signal.h 文件中, 可以发现其定义如下:

1
2
/* write on a pipe with no one to read it */
#define SIGPIPE 13

用通俗的话来讲, 就是管道破裂.

管道破裂,这个信号通常在进程间通信产生,比如采用 FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到 SIGPIPE 信号.
此外用 Socket 通信的两个进程,写进程在写 Socket 的时候,读进程已经终止.
另外, 在 send/write 时会引起管道破裂,关闭 Socket, 管道时也会出现管道破裂.
使用 Socket 一般都会收到这个 SIGPIPE 信号.

也就是说, 该信号是跟 Socket 的连接以及数据的读写相关联的.

这样的话,我们就知道为什么进退房间导致 Crash 了,我们的进退房间都和 Socket 有关联,这种 Crash 大都数是在用户网络不好的情况下发生的.

解决问题

类似 signal 13 这种错误是系统发出来的, 和内存使用异常和野指针一样,由于是系统级别崩溃,所以不能通过

1
2
3
4
5
6
@try {

}
@catch(NSException *exception) {

}

捕获到这类异常.

因此, try catch 是无法解决问题的.

目前有两个方案可用:

方案1. 忽略这类信号.

方案2. 修改源码, 在 IM 代码里面修改.

因为, IMSDK 我们是使用第三方的, 所以无法更改其源码, 所以采取了方案1: 忽略这类信号.

忽略的方案很简单, 在你连接或者初始化 IMSDK 之前, 只需要一行代码:

1
signal(SIGPIPE, SIG_IGN);

示例代码:

1
2
3
4
5
6
7
8
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
// ...

signal(SIGPIPE, SIG_IGN);

// ...
}

对于 方案2, 我查阅了一下 CocoaAsyncSocket 的源码:

1
2
3
4
// Prevent SIGPIPE signals

int nosigpipe = 1;
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));

所以对于 方案2, 就是增加上面的代码即可, 即设置 Socket 不发送 SIGPIPE 信号.

对于上面的两种方案, 苹果开发者文档 Avoiding Common Networking Mistakes 都有提及.

-w380

问题复现

既然这个问题很难复现, 我们就想办法让他很容易复现.

可以采用手动发送 signal 的方式, 来复现这个问题.

先看一下 kill(3) - Linux man page 函数:

The kill() function shall send a signal to a process or a group of processes specified by pid. The signal to be sent is specified by sig and is either one from the list given in <signal.h> or 0. If sig is 0 (the null signal), error checking is performed but no signal is actually sent. The null signal can be used to check the validity of pid.

kill 函数是可移植操作系统接口 POSIX(Portable Operating System Interface of UNIX) 定义的, 可以参考 维基百科.

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)viewDidLoad
{
[super viewDidLoad];

//获取进程 id
pid_t cur_pid = getpid();
printf("current process's id: %i\n", cur_pid);

//延时10s 为了让 Bugtags 有时间上报日志.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

wpcSendSignal(cur_pid);
});
}

///手动发送信号
int wpcSendSignal(int pid)
{
int ret = kill(pid, SIGPIPE);

return ret;
}

我们将 signal(SIGPIPE, SIG_IGN) 代码先注释.

运行 APP 到手机, 然后退出 Xcode 的连接.

注意: 这里如果连接 Xcode, 不会直接 Crash, 所以需要断开手机与 Xcode 的连接.

在后台可以看到类似的 Crash 日志:

-w500

除了上面使用 kill 函数外, 我们还可以使用 raise 函数来发送 signal.

关于 raise 函数, 可以查阅 手册.

或者直接问男人(man):

1
man raise

问题再次来临: 与 Bugtags 的结合

本以为采用上述方案就万事大吉了, 在第二次提测后, 还是有这种 Crash 的问题在后台上报.

排查了很久后才发现,问题的原因是 BugTags 也会控制这个开头,默认是不忽略,这样:

1
2
3
4
/**
* 是否忽略 PIPE Signal (SIGPIPE) 闪退,默认 NO
*/
@property(nonatomic, assign) BOOL ignorePIPESignalCrash;

这个默认设置为 NO, 即可以上报 PIPE Signal Crash 的问题.

这里也说明一个问题, 即使我们采用 方案1 解决 Crash 的问题了, Bugtags 还是会将这种 Crash 上报到后台.

示例代码:

1
2
3
4
5
bugtag.option.ignorePIPESignalCrash = YES;

// ...

signal(SIGPIPE, SIG_IGN);

对比一下测试的两张图, 第一张图到第二张图是增加了两次崩溃次数, 原因是故意设置了 bugtag.option.ignorePIPESignalCrash = NO, 也正好验证了我们的想法.

-w600
-w600

总结

  1. Xcode 连接真机或者模拟器, 运行出现异常断点, 可能就是隐患点.

  2. 学会使用后台日志找到规律, 继而去思考并解决问题.

  3. 对 Crash 进行更深入的分析和总结, 不要轻易放弃.

后续研究

  1. 是否可以忽略其他的 signal, 来避免不必要的 Crash?

  2. 自定义一套关于 signal 捕获的跨平台库, 在开发阶段可以直接看到完整的日志.

推荐

  1. Avoiding Common Networking Mistakes

  2. linux die

  3. Using Sockets and Socket Streams


扫码关注,你我就各多一个朋友~

这样学习你愿意吗?

发表于 2018-04-07 | 分类于 随笔 |

不得不承认, 学习确实是个「苦差事」, 这里说的学习不仅仅指伏案学习, 之乎者也等, 而是一种广泛的学习.

如何学习是始终要去探讨的一个话题, 时代在变, 环境在变, 学习当然也不能一成不变. 坚持学习是一辈子的事, 活到老学到老.

既然学习是一个漫长的马拉松, 那我们只有持之以恒的’跑’下去, 才能保持不掉队. 这样一来, 学习方法和学习技巧就是取胜的关键了.

世上没有绝对的学习妙招和学习宝典, 找到适合自己的就是最好的. 下面我结合自己的一些经历给大家分享在学习的道路上遇到的那些事.

不管怎样, 学习一定要带着目标, 结合理论再去实践, 事半功倍.


欢迎大家在评论区交流, 不喜勿喷~

回忆当天事

每晚躺在床上或者沙发上, 发发呆, 把当天做过的事情, 在脑海中统统的过一遍, 像放电影一样, 能有多详细就多详细, 也就是说尽量的回忆细节.

这种做法看似很简单, 其实很难坚持下去. 特别是中间几天, 很多人都会放弃, 再加上现在手机互联网的 “毒害”, 留给自己去真正学习的时间更少了. 但是只要你咬咬牙坚持一下, 把这个习惯养成, 就会终生受用.

我在初中时期已经养成了这个习惯, 直到大学都没有放弃. 大学里面每次期末考试, 看到很多人忙的不可开交, 我都是不慌不忙的, 悠闲自得, 室友问我怎么做到游刃有余, 我说你白天少睡点觉, 平时注意听讲, 晚上回忆一下就可以了, 从他迷茫的眼神中可以看出, 他当时并不完全相信我说的!

每次回忆结束, 有些东西还是比较模糊甚至一知半解, 没事, 你第二天去翻阅相关知识再去整理和理解一下就好了, 这种自我激励比任何鞭策都有用.

有些困难只是暂时的, 想办法 ‘挺’ 过去, 你就赢了. 很多时候不是困难吓到了我们, 而是我们放弃了自己.

多想多问

和你身边兴趣相投或者比你有能力的人多交流, 交流的过程也是你练习自我表达能力的过程, 把自己的问题能够清楚的描述给别人听, 也是一种能力.

不知道大家有没有这样一种感觉, 在跟别人交流的过程中, 虽然别人没有直接帮你解决问题, 但是你突然有了解决问题的办法. 为什么会这样呢? 很简单, 因为这个问题你之前思考过, 只是在交流的过程中, 你的大脑专注在此, 聚精会神的在想这件事情.

问问题有一套门道. 在请教别人问题之前, 最好自己先尽力想一想, 不要遇到一点问题就去问东问西, 即使别人给你讲了, 你也只是当时明白了, 下次再遇到此类问题, 你依然懵逼. 还有就是会让你形成一种依赖心理, 反正有人能解决, 我问就可以了, 干嘛还有费时费力的去想?! 久而久之, 就破罐子破摔了.

另外一个要说的就是在请教或者交流问题的时候, 要对对方谦虚.

我很感激高中时代, 班主任很信任我, 让我当了三年班长, 那三年让我学到了很多书本之外的东西, 其中沟通和管理让我在今后的工作中受益匪浅.

阅读和写作

这里说的写作不是指专业的作家级的写作, 泛指任何文字的记录, 日记也算.

每天最幸福的时刻就是在 23:00 点之前坐在电脑前, 可以写自己心中要表达的内容, 任思绪飘摇.

只要有闲暇的时间, 我就会整理和总结这段时间发生的事情, 既有工作上的也有生活上的.

小的时候, 家里比较穷, 交学费都是问题, 更别说是买书了. 我记得初中一年级, 我很想要一本英语书籍, 那种渴望至今难忘! 后来, 我每天从自己的生活费里抠出一点钱(生活费本来就少), 就这样积攒了几个星期, 去新华书店买了一本英语书籍. 高中时期, 有同学不要的或者已经不读的书籍, 我都会借过来看, 沉浸在自己的幸福当中.

参加工作之后, 我也有写博客的习惯, 把自己工作中遇到的问题以及解决问题的思路和方式写出来, 分享给其他人, 快乐无比. 特别是收到文章的评论后, 心理美滋滋的. 在互联网的海洋里, 大家素不相识, 通过键盘上敲击的文字互相交流知识和想法, 彼此成长着, 也别具一番乐趣.

阅读和写作可以培养自己的专注力, 这么好的习惯你千万不要拒绝. 写作可以倒逼着你去阅读, 去关心生活里面的点滴, 阅读和生活中的体验又可以激发你写作的欲望, 两者相辅相成.

推荐给大家一本书: Google 工作整理术

-w200

谷歌前CIO(首席信息官)分享风靡谷歌的信息整理术,帮助现代人彻底告别无序工作,学会利用数字工具为大脑减压!信息太多、时间太少,整理信息已成为现代人急需掌握的必备技能。谷歌作为引领全球的信息搜索与整合巨头,拥有能够有序整理信息的独特方式,而谷歌首席信息官便是主导这一“信息工厂”的总工程师。如今,曾担任“总工程师”的道格拉斯梅里尔亲自将风靡谷歌的信息整理方式分享给大众读者,帮助现代人克服无序工作的通病。《Google工作整理术》告诉你:不要花太多时间给信息归档,用的时候学会去搜索;在数字信息文档中加上关键词,方便日后检索;从前,知识就是力量,现在,共享知识才是力量;把工作和生活融为一体,而不是力图在二者之间求平衡。这些实用 Tips 都揭示了:信息整理才是高效工作的关键,信息整理已是现代人的工作必备技能!

OC-RunTime: 总结消息转发中用到的知识点

发表于 2018-04-05 | 分类于 iOS |

前言

OC-RunTime: 消息转发之实例方法的转发流程

OC-RunTime: 消息转发之实例方法的转发流程实例讲解

OC-RunTime: 消息转发之类方法的转发流程

在上面的几篇文章中我分享了关于消息转发相关的知识点,里面有很多细节没有阐述。

若在上面的文章中加入很多细节点的话,一是拉长了文章的内容, 二是对于刚接触 Runtime 的朋友来说不一定能接受, 于是就有了这篇文章的诞生.

RunTime 的定义及使用场景

苹果 开发文档 的这样解释 runtime 的:

1
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work

尽量将决定放到运行的时候,而不是在编译和链接过程中.

RunTime 的应用场景:

1.面向切面编程 AOP.
2.方法调配 method swizzling. 有些地方称之为”黑魔法”.
3.消息转发.
4.给分类添加属性(关联对象).
5.动态获取 class 和 slector.
6.KVO/KVC, 修改私有属性的值.

建议去阅读下面框架的源码,恕我没有加入链接:

Aspects(AOP必备,“取缔” baseVC,无侵入埋点)

MJExtension(JSON 转 model,一行代码实现 NSCoding 协议的自动归档和解档)

JSPatch(动态下发 JS 进行热修复)

NullSafe(防止因发 unrecognised messages 给 NSNull 导致的崩溃)

UITableView-FDTemplateLayoutCell(自动计算并缓存 table view 的 cell 高度)

UINavigationController+FDFullscreenPopGesture(全屏滑动返回)

思考问题

在前面的文章中,很多次看到 IMP、SEL、selector 以及 Method 等关键字,相信大家随着对 RunTime 的逐步了解,慢慢会逐渐熟悉它们的,只是时间问题。很多概念上面的东西理解起来没那么简单,需要动手去写写代码。

在看下面内容之前, 先抛出一个问题:

runtime 如何通过 selector 找到对应的 IMP 地址?

接下来分别说一下 IMP、SEL、selector 以及 Method.

IMP

IMP 保存的是 Method 的地址,本质是一个函数指针,由编译器生成。

IMP 在 objc.h 中的定义:

1
2
3
4
5
6
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif

向对象发送消息之后,是由这个函数指针 IMP 指定的, 即 IMP 函数指针就指向了方法的实现.

IMP 函数指针最少包含 id 和 SEL 类型的两个参数,后面其他的参数是对应方法需要的参数。其中 id 代表执行该方法的 target(对象), SEL 就是对应的方法, 通过 id 和 SEL 参数就能确定唯一的方法实现地址.

那么我们如何获取方法的 IMP 呢?很简单.

NSObject 提供了如下两个方法:

1
2
- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;

对应的实现(源码 NSObject.mm), 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (IMP)instanceMethodForSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return class_getMethodImplementation(self, sel);
}

+ (IMP)methodForSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return object_getMethodImplementation((id)self, sel);
}

- (IMP)methodForSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return object_getMethodImplementation(self, sel);
}

大家可以看到,对应的 methodForSelector 既有实例方法又有类方法,而 instanceMethodForSelector 只有类方法。

在使用 methodForSelector 方法时,向类发送消息,则 SEL 应该是类方法, 若向实例对象发送消息,则 SEL 应该为实例对象方法.

而 instanceMethodForSelector 仅仅允许类发送该消息, 从而获取实例方法的 IMP. 该方法无法获取类方法的 IMP, 如果想获取类方法的 IMP 可以使用 methodForSelector 来获取。

函数文档原文解释如下:

1
2
Use this method to ask the class object for the implementation of instance methods only. 
To ask the class for the implementation of a class method, send the methodForSelector: instance method to the class instead.

举个例子,或许更好理解。

下面两个方法, 一个是类方法(testClassMethod), 另一个是实例方法(testInstanceMethod).

1
2
3
4
5
+ (void)testClassMethod {
}

- (void)testInstanceMethod {
}

分别使用上面提到的方法来获取 IMP 的几个方法.

1
2
3
4
5
6
7
8
9
IMP imp = [[self class] instanceMethodForSelector:@selector(testClassMethod)];
IMP imp2 = [[self class] instanceMethodForSelector:@selector(testInstanceMethod)];

// 也可以改成 NSObject 调用的方式, 结果一样.
// IMP imp = [NSObject instanceMethodForSelector:@selector(testClassMethod)];
// IMP imp2 = [NSObject instanceMethodForSelector:@selector(testInstanceMethod)];

IMP imp3 = [[self class] methodForSelector:@selector(testClassMethod)];
IMP imp4 = [self methodForSelector:@selector(testInstanceMethod)];

调试器可以看出, 如下日志:

1
2
3
4
5
6
7
8
9
10
11
12
Printing description of imp:
(IMP) imp = 0x000000010d8455c0 (libobjc.A.dylib`_objc_msgForward)

Printing description of imp2:
(IMP) imp2 = 0x000000010cf19b90 (-[ViewController testInstanceMethod] at ViewController.m:94)

Printing description of imp3:
(IMP) imp3 = 0x000000010cf19b60 (+[ViewController testClassMethod] at ViewController.m:89)

Printing description of imp4:
(IMP) imp4 = 0x000000010cf19b90 (-[ViewController testInstanceMethod] at ViewController.m:94)
(lldb)

imp2、imp3、imp4 都是正常的,唯独 imp 不正常,也充分说明了 instanceMethodForSelector 无法获取类方法的 IMP.

Method

在源码 runtime.h 中, 定义 method, 其本质是一个结构体.

1
2
3
4
5
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

方法名 method_name 类型为 SEL.

method_types 方法类型, 是一个 char 指针,存储着方法的参数类型和返回值类型。

方法实现 method_imp 的类型为 IMP.

可以看出, 有 SEL 和 IMP, method_types 是对应的方法返回值和参数类型, 如 v@:,是一个字符串。

runtime.h 中有两个方法,可以根据 SEL 直接获取实例方法和类方法的 Method,如下:

1
2
3
Method class_getInstanceMethod(Class cls, SEL name);

Method class_getClassMethod(Class cls, SEL name);

SEL

selector, 称之为方法选择器,SEL 是 selector 的表示类型,也是方法的编号,是类成员方法的指针。

SEL 定义在源码 objc.h 中, 是一个结构体指针, 如下:

1
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

但是源码中查不到 objc_selector 具体的定义和实现.

获取 SEL 有三个方法:

1
2
3
SEL sel = @selector(play:);
SEL sel = sel_registerName("play:");
SEL sel = NSSelectorFromString(@"play");

SEL 表示一个 selector 的指针,无论什么类里,只要方法名相同,SEL 就相同,SEL 实际是根据方法名 hash 化了的字符串。而对于字符串的比较仅仅需要比较他们的地址就可以了,所以速度上非常快,SEL 的存在加快了查询方法的速度。

思考一个问题:为什么在同一个 OC 类中,不能存在同名的函数,即使参数类型不同也不行,换句话说 OC为什么没有重载?

答案已经在上面说了,SEL 表示一个 selector 的指针,无论什么类里,只要方法名相同,SEL 就相同,相同的函数名,编译器无法编译通过。

dispatch table 存放 SEL 和 IMP 的对应关系,SEL 最终会通过 dispatch table 寻找到对应的IMP。

总之,Selector、Method 和 IMP 三者之间的关系可以这么解释,在类的(实例和类方法)调度表(dispatch table)中的每一个实体代表一个方法 Method,其名字叫做选择器 SEL,并对应着一种方法实现称之为 IMP,有了 Method 就可以使用 SEL 找到对应的 IMP,SEL 就是为了查找方法的最终实现 IMP。

class_addMethod

查看源码 objc-runtime-new.mm 中该函数实现如下:

1
2
3
4
5
6
7
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;

rwlock_writer_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}

开发文档中这样描述该函数:

1
2
3
4
Adds a new method to a class with a given name and implementation.
class_addMethod will add an override of a superclass's implementation,
but will not replace an existing implementation in this class.
To change an existing implementation, use method_setImplementation.

解释一下,可以为类根据 SEL 和 IMP 动态添加一个新方法。class_addMethod 仅可以动态添加方法,不会替换。如果想达到方法替换的效果可使用 method_setImplementation 函数。

关于 method_setImplementation 和 method_exchangeImplementations 后面文章再做分析.

其实, method_exchangeImplementations 的内部实现相当于调用了 2 次 method_setImplementation 方法。

class_addMethod 不仅可以动态添加类方法, 也可以添加实例方法。

参数及返回值解释:

1
2
3
4
5
6
7
8
9
返回值: 返回 YES 表示方法添加成功, 否则添加失败。

参数 Class cls: 将要给添加方法的类, 即[类名 class]

参数 SEL name: 将要添加的方法 SEL, 即 @selector(方法名),如果已经存在,该方法返回失败,不存在就添加成功。

参数 IMP imp:实现这个方法的函数. 有两种写法即 C 和 OC 的写法. 一个 IMP 最少包括两个参数, 上面已经说过。

参数 const char *types: 实现方法的函数的返回和参数编码类型. 如 "v@:" 表示返回值为 void, 没有参数的一个函数, 其中 @和:分别代表 IMP 的默认两个参数即 id 和 sel.

关于 types,可以使用 method_getTypeEncoding 来获取。

更多关于 types 的内容可以参考开发者文档 Type Encodings.

解答问题

读到这里, 大家对 IMP, SEL 以及 Method 应该有初步的了解了, 那么来解答一下刚才提出的问题:

runtime 如何通过 selector 找到对应的 IMP 地址?

回答这个问题的关键是要知道消息调度表(dispatch table),另外一个要回答的要点是 IMP 的实现和获取以及和 Method 之间的关系。

类对象中有类方法和实例方法的分发表,表中记录着方法的名字、参数和实现,selector 本质就是方法名称,runtime 通过这个方法名称就可以在列表中找到该方法对应的实现.

系统为我们提供了获取 IMP 指针的函数,无论是类方法还是实例方法我们都可以获取对应的 IMP.

而 Method 将 Selector 和 IMP 联系起来,可从源码中看出:

1
2
3
4
5
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

IMP 是函数的指针,它是由编译器编译生成的。当发一个消息时,它会找到那段代码执行,IMP 指向了这个方法的具体的实现,得到这个函数的指针可以直接执行。

IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址,反之亦然。当发送消息给一个对象时,runTime 会在对象的类对象方法列表里查找,当我们发送一个消息给一个类时,这条消息会在类的 Meta Class 对象的方法列表里查找,直到超找到 NSObject 中为止。

消息传递的过程

1. 当消息被发送给一个对象,messaging function 跟随对象的 isa 指针找到它的 class structure,在 dispatch table 中寻找 method selector.

2. 如果没有找到 selector,objc_msgsend 跟随该类实例的 isa 找到父类,尝试在父类的 dispatch table 中寻找 selector.

3. 重复步骤 2,直到 isa 指向 NSObject Class 为止。

关于分发表和消息相关的知识可以参考开发文档 Messaging,讲得很清楚。

实际例子

说了这么多理论知识,是时候举栗子了,方便大家更好的理解上面的内容。

1. 动态添加实例方法

Student.m

除 init 外,Student 只有一个实例方法 studentWalkImp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@implementation Student

- (instancetype)init
{
self = [super init];

if (self) {

SEL proxySelector = NSSelectorFromString(@"studentWalkImp");
IMP impletor = class_getMethodImplementation([self class], proxySelector);

// 获取实例方法
Method method = class_getInstanceMethod([self class], proxySelector);
const char *types = method_getTypeEncoding(method);

SEL origSel = NSSelectorFromString(@"walk");
class_addMethod([self class], origSel, impletor, types);
}

return self;
}

- (void)studentWalkImp
{
NSLog(@"---veryitman--- Student studentWalkImp");
}

@end

调用测试一下.

1
2
3
4
5
6
7
- (void)viewDidLoad {

[super viewDidLoad];

Student *stud = [[Student alloc] init];
[stud performSelector:NSSelectorFromString(@"walk") withObject:nil];
}

这里 Student 并没有 walk 方法,故意为之,运行后控制台会打印:

1
---veryitman--- Student studentWalkImp

成功的为 Student 添加了一个实例方法 walk 的实现 studentWalkImp.

上面的例子是使用 OC 的 IMP 方式来实现的,可以改为 C 实现版本的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation Student

- (instancetype)init
{
self = [super init];

if (self) {
SEL origSel = NSSelectorFromString(@"walk");
class_addMethod([self class], origSel, (IMP)studentWalkImp, "v:@");
}

return self;
}

void studentWalkImp()
{
NSLog(@"---veryitman--- Student studentWalkImp");
}

2. 动态添加类方法

动态添加类方法,和动态添加实例方法稍微有点不同。下面是改造后的 Student.m.

Student.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@implementation Student

- (instancetype)init
{
self = [super init];

if (self) {

// 获取 MetaClass, 类方法不可以使用 [self class]
Class metaCls = objc_getMetaClass([NSStringFromClass([self class]) UTF8String]);

SEL proxySelector = NSSelectorFromString(@"clsImp");
IMP impletor = class_getMethodImplementation(metaCls, proxySelector);

// 获取类方法
Method method = class_getClassMethod([self class], proxySelector);

const char *types = method_getTypeEncoding(method);
SEL origSel = NSSelectorFromString(@"walk");

class_addMethod(metaCls, origSel, impletor, types);
}

return self;
}

+ (void)clsImp
{
NSLog(@"---veryitman--- Student clsImp");
}

注意:这里获取 Class 稍微不同的是使用了 objc_getMetaClass,这里关系到 Objective-C 中的类、Class、根类和元类的区别,可以参考 Class、isa、元类 这篇文章。

调用测试一下:

1
2
3
4
5
6
7
8
- (void)viewDidLoad {

[super viewDidLoad];

Student *stud = [[Student alloc] init];

[[stud class] performSelector:NSSelectorFromString(@"walk") withObject:nil];
}

控制台打印:

1
---veryitman--- Student clsImp

成功地为类动态的添加了一个类方法 clsImp.

参考文档

1. Objective-C 对象模型及应用

2. Apple RunTime 源码 objc4-723.tar.gz

3. Messaging

4. Objective-C 深入理解中的消息机制和方法调用

完整代码

点击下载文中完整的 Demo.


扫码关注,你我就各多一个朋友~

OC-RunTime: 消息转发之类方法的转发流程

发表于 2018-04-03 | 分类于 iOS |

本篇是「消息转发」系列的第三篇, 在 OC-RunTime: 消息转发之实例方法的转发流程 和 OC-RunTime: 消息转发之实例方法的转发流程[实例讲解] 中分享了实例方法的转发流程.

今天分享如何对类方法进行消息的转发.

resolveClassMethod

NSObject 提供了 resolveClassMethod 来让开发者在里面动态添加一个类方法.

类方法的转发流程和实例方法转发的流程大致一样, 唯独不同的是需要重写的方法(NSObject中)的不一样.

当时我在写 Demo, 以为只需要将 resolveInstanceMethod 改为 resolveClassMethod 就万事大吉了即重写下面几个方法就可以解决问题, 事实证明这样是不行的.

  1. +resolveClassMethod
  2. -forwardingTargetForSelector
  3. -methodSignatureForSelector
  4. -forwardInvocation
  5. -doesNotRecognizeSelector:

网上很多博文并没有深入的探讨关于类方法转发的流程, 只是在介绍实例方法转发的流程的同时, 一笔带过类方法转发机制和流程.

通过对 NSObject.mm 源码的查看, 可以看到对应上面的几个方法都有类方法. 如下:

1
2
3
4
5
1. +resolveClassMethod
2. +forwardingTargetForSelector
3. +methodSignatureForSelector
4. +forwardInvocation
5. +doesNotRecognizeSelector:

重新这几个方法才是解决问题的关键.

现在我们重写 resolveClassMethod, 如下.


ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import <objc/runtime.h>

static NSString * const sPerformClassMethodName = @"veryClassMethod";

+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"---veryitman--- 1--- +resolveClassMethod");

NSString *methodName = NSStringFromSelector(sel);

if ([sPerformClassMethodName isEqualToString:methodName]) {

// 获取 MetaClass
Class predicateMetaClass = objc_getMetaClass([NSStringFromClass(self) UTF8String]);
// 根据 metaClass 获取方法的实现
IMP impletor = class_getMethodImplementation(predicateMetaClass, @selector(proxyMethod));
// 获取类方法
Method predicateMethod = class_getClassMethod(predicateMetaClass, @selector(proxyMethod));
const char *encoding = method_getTypeEncoding(predicateMethod);

// 动态添加类方法
class_addMethod(predicateMetaClass, sel, impletor, encoding);

return YES;
}

return [super resolveClassMethod:sel];
}

+ (void)proxyMethod
{
NSLog(@"---veryitman--- +proxyMethod of class's method for OC.");
}

模拟调用

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad
{
[super viewDidLoad];

// 运行类方法
SEL selector = NSSelectorFromString(sPerformClassMethodName);
SuppressPerformSelectorLeakWarning(
[[self class] performSelector:selector withObject:nil];
);
}

关于 SuppressPerformSelectorLeakWarning 可以参考 OC-RunTime: 消息转发之实例方法的转发流程[实例讲解].

将动态添加的方法让 proxyMethod 来执行, 显示结果达到预期.

1
2
---veryitman--- 1--- +resolveClassMethod
---veryitman--- +proxyMethod of class's method for OC.

创建被转发者

MZTempObj.m

1
2
3
4
5
6
7
8
9
@implementation MZTempObj

/// 类方法
+ (void)veryClassMethod
{
NSLog(@"---veryitman--- veryClassMethod");
}

@end

这里有类方法的一个实现 veryClassMethod.

重写转发消息的函数

同理将 resolveClassMethod 修改一下, 为了保证流程继续.

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"---veryitman--- 1--- +resolveClassMethod. selector: %@", NSStringFromSelector(sel));

NSString *methodName = NSStringFromSelector(sel);

// 这里故意将 sPerformClassMethodName 改为 @"", 为了流程往下走
if ([@"" isEqualToString:methodName]) {

// 获取 MetaClass
Class predicateMetaClass = objc_getMetaClass([NSStringFromClass(self) UTF8String]);
// 根据 metaClass 获取方法的实现
IMP impletor = class_getMethodImplementation(predicateMetaClass, @selector(proxyMethod));
// 获取类方法
Method predicateMethod = class_getClassMethod(predicateMetaClass, @selector(proxyMethod));
const char *encoding = method_getTypeEncoding(predicateMethod);

// 动态添加类方法
class_addMethod(predicateMetaClass, sel, impletor, encoding);

return YES;
}

return [super resolveClassMethod:sel];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 2--- +forwardingTargetForSelector");

NSString *selectorName = NSStringFromSelector(aSelector);

if ([sPerformClassMethodName isEqualToString:selectorName]) {

// 注意1: 也可在此转发实例方法
#if 0
// 让 MZTempObj 去执行 aSelector, 实现消息的转发
MZTempObj *myobject = [[MZTempObj alloc] init];

return myobject;
#endif

// 转发类方法对应返回类对象
return [MZTempObj class];
}

id obj = [super forwardingTargetForSelector:aSelector];

return obj;
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 3--- +methodSignatureForSelector");

// 找出对应的 aSelector 签名
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];

// 注意2: 也可以在此获取实例方法的签名
#if 0
if (nil == signature) {

// 是否有 aSelector
if ([MZTempObj instancesRespondToSelector:aSelector]) {
signature = [MZTempObj instanceMethodSignatureForSelector:aSelector];
}
}

return signature;
#endif

if (nil == signature) {

// 是否有 aSelector
if ([MZTempObj respondsToSelector:aSelector]) {

//methodSignatureForSelector 可以获取类方法和实例方法的签名
//instanceMethodSignatureForSelector只能获取实例方法的签名
signature = [MZTempObj methodSignatureForSelector:aSelector];
}
}

return signature;
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"---veryitman--- 4--- +forwardInvocation");

// 注意3: 也可以调用实例方法
#if 0
if ([MZTempObj instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:[[MZTempObj alloc] init]];
}
else {
[super forwardInvocation:anInvocation];
}

return;
#endif

if ([MZTempObj respondsToSelector:anInvocation.selector]) {

// 这里转发的是 MZTempObj Class, 不是对象
[anInvocation invokeWithTarget:[MZTempObj class]];
}
else {
[super forwardInvocation:anInvocation];
}
}

+ (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 5--- +doesNotRecognizeSelector: %@", NSStringFromSelector(aSelector));
}

执行后, 控制台输出日志:

1
2
3
---veryitman--- 1--- +resolveClassMethod. selector: veryClassMethod
---veryitman--- 2--- +forwardingTargetForSelector
---veryitman--- veryClassMethod

这里注意一下

将代码中 注意1 注意2 等部分可以自行打开测试一下, 然后将 MZTempObj.m 中的类方法(+veryClassMethod)改为实例方法(-veryClassMethod), 也是可以的, 这样就达到了将类方法转发给实例方法的效果.

修改一下 forwardingTargetForSelector 中的实现, 可以看到 4, 5也会执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 2--- +forwardingTargetForSelector");

NSString *selectorName = NSStringFromSelector(aSelector);

if ([@"" isEqualToString:selectorName]) {

// 注意1: 也可在此转发实例方法
#if 0
// 让 MZTempObj 去执行 aSelector, 实现消息的转发
MZTempObj *myobject = [[MZTempObj alloc] init];

return myobject;
#endif

// 转发类方法对应返回类对象
return [MZTempObj class];
}

id obj = [super forwardingTargetForSelector:aSelector];

return obj;
}
1
2
3
4
5
6
---veryitman--- 1--- +resolveClassMethod. selector: veryClassMethod
---veryitman--- 2--- +forwardingTargetForSelector
---veryitman--- 3--- +methodSignatureForSelector
---veryitman--- 1--- +resolveClassMethod. selector: _forwardStackInvocation:
---veryitman--- 4--- +forwardInvocation
---veryitman--- veryClassMethod

同理我们可以得到类方法的消息转发流程图, 如下图所示:

1

推荐

OC-RunTime: 消息转发之实例方法的转发流程

OC-RunTime: 消息转发之实例方法的转发流程实例讲解

OC-RunTime: 总结消息转发中用到的知识点

点击下载文中完整的 Demo.


扫码关注,你我就各多一个朋友~

OC-RunTime: 消息转发之实例方法的转发流程[实例讲解]

发表于 2018-04-01 | 分类于 iOS |

OC-RunTime: 消息转发之实例方法的转发流程 分享了消息转发的流程, 本次结合实际例子继续分析一下消息转发流程.

发送不存在的消息

在 ViewController 的 viewDidLoad 中运行 veryTestMethod 方法.

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
static NSString * const sPerformInstanceMethodName = @"veryTestMethod";

- (void)viewDidLoad {

[super viewDidLoad];

// 运行实例方法
SEL selector = NSSelectorFromString(sPerformInstanceMethodName);

SuppressPerformSelectorLeakWarning(
[self performSelector:selector withObject:nil];
);
}

其中, SuppressPerformSelectorLeakWarning 是定义的一个宏.

1
2
3
4
5
6
7
#define SuppressPerformSelectorLeakWarning(Stuff) \
do { \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
Stuff; \
_Pragma("clang diagnostic pop") \
} while (0)

在 ViewController 中我并没有写 veryTestMethod 这个函数, 只是借助 performSelector 动态执行, 如果编译运行直接会 crash.

可以查看 NSObject.mm 源码, 里面关于消息转发的几个重要函数都写着 _objc_fatal, 可谓是招招毙命.

紧接着, 我们可以借助 resolveInstanceMethod 来完成消息转发给 ViewController.

resolveInstanceMethod 转发

重写 NSObject 中的 resolveInstanceMethod 函数.

+resolveInstanceMethod

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 记得导入 RunTime 头文件
#import <objc/runtime.h>

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"---veryitman--- 1--- +resolveInstanceMethod");

NSString *methodName = NSStringFromSelector(sel);

if ([sPerformInstanceMethodName isEqualToString:methodName]) {

SEL proxySelector = NSSelectorFromString(@"proxyMethod");
IMP impletor = class_getMethodImplementation(self, proxySelector);

// 获取实例方法
Method method = class_getInstanceMethod(self, proxySelector);
const char *types = method_getTypeEncoding(method);

// 添加 OC 的函数
class_addMethod([self class], sel, impletor, types);

return YES;
}

return [super resolveInstanceMethod:sel];
}

ViewController 中实现的 proxyMethod

1
2
3
4
5
// OC 实现
- (void)proxyMethod
{
NSLog(@"---veryitman--- -proxyMethod of instance's method for OC.");
}

在 resolveInstanceMethod 中动态添加了 veryTestMethod 方法, 并让 proxyMethod 函数来实现(IMP).

运行可以看到, 程序并没有 crash, 成功的执行了 proxyMethod.

1
2
---veryitman--- 1--- +resolveInstanceMethod
---veryitman--- -proxyMethod of instance's method for OC.

到此为止, 我们已经看到动态添加一个方法的实现并成功运行的完整例子.

接下来, 我们将转发给其他对象 MZTempObj 来执行.

自定义被转发的对象

MZTempObj.m

1
2
3
4
5
6
@implementation MZTempObj

- (void)veryTestMethod
{
NSLog(@"---veryitman--- veryTestMethod");
}

veryTestMethod 就是我们要转发对应的消息.

消息转发实践

接下来我们把向 ViewController 发送 veryTestMethod 的消息转发给 MZTempObj 的 veryTestMethod 方法.

继续重写下面函数, 不过 resolveInstanceMethod 要稍微改造一下, 才能达到我们实践的目的.

  1. +resolveInstanceMethod
  2. -forwardingTargetForSelector
  3. -methodSignatureForSelector
  4. -forwardInvocation
  5. -doesNotRecognizeSelector:

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 记得导入 RunTime 头文件
#import <objc/runtime.h>

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"---veryitman--- 1--- +resolveInstanceMethod: %@", NSStringFromSelector(sel));

NSString *methodName = NSStringFromSelector(sel);

// 这里故意为之, 让流程往下走
if ([@"" isEqualToString:methodName]) {

SEL proxySelector = NSSelectorFromString(@"proxyMethod");
IMP impletor = class_getMethodImplementation(self, proxySelector);

// 获取实例方法
Method method = class_getInstanceMethod(self, proxySelector);
const char *types = method_getTypeEncoding(method);

// 添加 OC 的函数
class_addMethod([self class], sel, impletor, types);

return YES;
}

return [super resolveInstanceMethod:sel];
}

/// 转发给对应的某个对象来执行 aSelector
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 2--- -forwardingTargetForSelector");

NSString *selectorName = NSStringFromSelector(aSelector);

if ([sPerformInstanceMethodName isEqualToString:selectorName]) {

// 让 MZTempObj 去执行 aSelector, 实现消息的转发
MZTempObj *myobject = [[MZTempObj alloc] init];

return myobject;
}

id obj = [super forwardingTargetForSelector:aSelector];

return obj;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 3--- -methodSignatureForSelector");

// 找出对应的 aSelector 签名
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];

if (nil == signature) {

// 是否有 aSelector
if ([MZTempObj instancesRespondToSelector:aSelector]) {
signature = [MZTempObj instanceMethodSignatureForSelector:aSelector];
}
}

return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"---veryitman--- 4--- -forwardInvocation");

if ([MZTempObj instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:[[MZTempObj alloc] init]];
}
else {
[super forwardInvocation:anInvocation];
}
}

- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 5--- -doesNotRecognizeSelector: %@", NSStringFromSelector(aSelector));
}

运行程序, 控制台打印结果如下:

1
2
3
---veryitman--- 1--- +resolveInstanceMethod
---veryitman--- 2--- -forwardingTargetForSelector
---veryitman--- veryTestMethod

这里对照之前的流程图是完全符合的, 那么怎么让其执行 3 和 4 呢? 很简单, 修改一下 forwardingTargetForSelector 里面的实现即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"---veryitman--- 2--- -forwardingTargetForSelector");

NSString *selectorName = NSStringFromSelector(aSelector);

// 故意为之
if ([@"" isEqualToString:selectorName]) {

// 让 MZTempObj 去执行 aSelector, 实现消息的转发
MZTempObj *myobject = [[MZTempObj alloc] init];

return myobject;
}

id obj = [super forwardingTargetForSelector:aSelector];

return obj;
}

再次执行看结果:

1
2
3
4
5
6
---veryitman--- 1--- +resolveInstanceMethod veryTestMethod
---veryitman--- 2--- -forwardingTargetForSelector
---veryitman--- 3--- -methodSignatureForSelector
---veryitman--- 1--- +resolveInstanceMethod: _forwardStackInvocation:
---veryitman--- 4--- -forwardInvocation
---veryitman--- veryTestMethod

注意: 这里在 3后面会多了一个 1--- resolveInstanceMethod 的打印, 是系统调用的, 此时对应的 sel 是 _forwardStackInvocation.

如果不去重写 methodSignatureForSelector 打印结果如下:

1
2
3
---veryitman--- 1--- +resolveInstanceMethod
---veryitman--- 2--- -forwardingTargetForSelector
---veryitman--- 5--- -doesNotRecognizeSelector: veryTestMethod

参考文档

1.Apple RunTime 源码 objc4-723.tar.gz

2.Message Forwarding

推荐

OC-RunTime: 消息转发之实例方法的转发流程

OC-RunTime: 消息转发之类方法的转发流程

OC-RunTime: 总结消息转发中用到的知识点

点击下载文中完整的 Demo.


扫码关注,你我就各多一个朋友~

OC-RunTime: 消息转发之实例方法的转发流程

发表于 2018-03-31 | 分类于 iOS |

最近在看人工智能相关的知识,无意中发现了一个巨牛的 人工智能教程,分享一下给大家。

教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家。点 这里 可以直接看教程。


本篇只是从系统函数的实现上, 来分析消息转发的流程.

下一篇结合本篇的理论和实际的例子来分析一下消息转发.

需要明确的几个问题

在往下看这篇博文之前, 有几个问题需要大家达成共识.

1.Objective-C 的特点
按照是否需要编译的原理来说, 编程语言一般可以分为静态编译类型和动态解释类型.

如 Java/C/C++ 是属于编译类型的语言, Php/Python/Ruby 属于解释类型的语言.

Objective-C 是基于 C 并具有自身特点的编译型语言, 再加上其 RunTime 机制, Objective-C 既是编译型又是动态的一门编程语言. 所谓的动态指的是在程序编译后运行中可以改变其结构.

2.函数调用
在 Objective-C 中调用函数, 被解释为向一个对象发送消息, 该对象可以是类对象也可以是实例对象.

例如:

1
[person play];

意思是向实例对象 person 发送一个 play 的消息.

还有一个比较有意思的是, 向一个空对象(nil) 发送消息不会 crash, 如果在 Java 中这个是不行的.例如:

1
2
person = nil;
[person play];

这个不会导致程序崩溃, 只是调用 play 函数没有任何反应罢了!

3.self
在 C++/Java 语言中, 有 this 指针的概念, 在 Objective-C 中, 也有类似的 this 指针即 self. self 既可以是实例对象也可以是类对象.

这里举个例子, 下面两个同名函数, 一个是类方法(+)一个是实例方法(-).
跟 Java 中的类似, 实例方法可以直接使用类方法, 但是类方法不可以直接使用实例方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (void)testClassMethod
{
[self sendMsg];
}

- (void)testInstanceMethod
{
[self sendMsg];
}

// 类方法
+ (void)sendMsg
{
NSLog(@"+ sendMsg. self: %@", self);
}

// 实例方法
- (void)sendMsg
{
NSLog(@"- sendMsg. self: %@", self);
}

在 ViewController 中分别调用两个 test 方法, 输出内容, 如下:

1
2
3
+ sendMsg. self: ViewController

- sendMsg. self: <ViewController: 0x7fc64740a5e0>

从结果可以看出类方法中的 self 代表的是该类, 实例方法中的 self 指的是类的一个实例即对象.

更多 self 的知识点,可以参考 OC: self 这篇文章。

关于 Message Forwarding

Message Forwarding 即消息转发.

关于消息转发, 官方文档 Message Forwarding 是这么解释的:

1
2
3
Sending a message to an object that does not handle that message is an error. However, 
before announcing the error,
the runtime system gives the receiving object a second chance to handle the message.

大概意思是这样的:

向一个对象发送消息, 该对象如果无法处理该消息, 系统就会报错, 但是在报错之前, 利用 Objective-C 提供的运行时机制可以防止报错的发生. 在 iOS 中类似这样的报错会导致程序直接 crash.
这里的对象, 可以是实例对象又可以是类对象.

在开发过程中, 大家一般都会遇到类似的 crash:

1
2
3
4
5
6
7
8
9
10
11
12
 *** Terminating app due to uncaught exception  
'NSInvalidArgumentException',
reason: '-[Controller play]: unrecognized selector sent to instance 0x7ff779f322a0'
*** First throw call stack:
(
CoreFoundation 0x000000010d89712b __exceptionPreprocess + 171
libobjc.A.dylib 0x000000010cf2bf41 objc_exception_throw + 48
CoreFoundation 0x000000010d918024 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
UIKit 0x000000010df48f51 -[UIResponder doesNotRecognizeSelector:] + 295
CoreFoundation 0x000000010d819f78 ___forwarding___ + 1432
CoreFoundation 0x000000010d819958 _CF_forwarding_prep_0 + 120
)

这个 crash 很明显, Controller 中调用了一个不存在的函数 play.

这里可以看到一个被调用的系统函数 doesNotRecognizeSelector, 今天我们剖析一下消息转发的流程.

实例方法(消息)转发的流程

在上面已经说过, OC 可以通过运行时来避免因为找不到方法而导致错误.

其实, 含义就是 OC 给了我们第二次机会来避免类似的错误.

我们重写 NSObject 中的 5 个方法, 分别是:

  1. +resolveInstanceMethod
  2. -forwardingTargetForSelector
  3. -methodSignatureForSelector
  4. -forwardInvocation
  5. -doesNotRecognizeSelector:

当无法找到对应的方法时, 调用的方法和顺序大致如下:

1
2
3
4
+resolveInstanceMethod
-forwardingTargetForSelector
-methodSignatureForSelector
-doesNotRecognizeSelector:

当找到对应的方法时, 调用方法和顺序大致如下:

1
2
3
4
+resolveInstanceMethod
-forwardingTargetForSelector
-methodSignatureForSelector
-forwardInvocation

根据实践结果, 画流程图如下:
1

下面分别解释一下这几个方法的作用和意义.

+ (BOOL)resolveInstanceMethod:(SEL)sel

解析对应的实例方法. 在该方法中允许增加一个方法的实现, 从而实现动态添加方法.

默认返回 NO.

在 NSObject.mm 中可以看到方法的实现:

1
2
3
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector

将对应的 selector 转发给指定的对象. 换句话说就是将之前没有实现的 selector 转交给另外一个可能实现了该 selector 的对象去处理.

默认返回 nil.

**- (NSMethodSignature )methodSignatureForSelector:(SEL)aSelector*

该方法返回对 selector 实现的方法签名.

默认实现如下:

1
2
3
4
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}

**- (void)forwardInvocation:(NSInvocation )anInvocation*

该方法是依赖第3个方法的, 如果第三个方法找到了对应的方法签名, 该方法就可以实现转向调用了.

- (void)doesNotRecognizeSelector:(SEL)sel

如果前面几个方法都没有处理好, 系统会调用该方法, 直接导致程序 crash, 也称之为终极死亡方法.

源码实现如下:

1
2
3
4
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}

总结, Objective-C 中给一个对象发送消息会经过如下几个步骤:

步骤1. 在类的调度表(dispatch table) 中找要执行的函数(消息), 如果找到了,到相应的函数 IMP 去执行.

每个类的结构体包含着两个必备的元素:
[1]. 指向父类的指针
[2]. class dispatch table(调度表). 调度表中包含了 method selectors 和特定 class 相应方法实现的地址.

步骤2. 如果没找到,运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试解析这个消息. 在该方法中, 允许动态添加一个方法的实现.

关于 resolveClassMethod 这个放在另一篇博客中讲解.

步骤3. 如果 resolveXX 返回 YES, 直接结束后面的流程, 执行解析得到的方法. 如果 resolveXX 返回 NO,运行时就发送 forwardingTargetForSelector 消息, 允许这个消息转发给另一个可能实现了对应 selector 的对象.

步骤4. 如果没有新的目标对象返回, 运行时就会发送methodSignatureForSelector 消息, 找到对应的方法签名.
如果找到了, 会接着调用 forwardInvocation, 如果没有找到即返回 nil, 那么直接调用 doesNotRecognizeSelector.

下一篇结合实际例子分析一下整个实例方法的转发流程.

参考文档

1.Apple 开发者文档 Message Forwarding

2.Apple RunTime 源码 objc4-723.tar.gz

推荐

OC-RunTime: 消息转发之实例方法的转发流程实例讲解

OC-RunTime: 消息转发之类方法的转发流程

OC-RunTime: 总结消息转发中用到的知识点

点击下载文中完整的 Demo.


扫码关注,你我就各多一个朋友~

<1…121314…20>

193 日志
16 分类
163 标签
© 2024 veryitman