残影无伤

记录技术的脚印.Keep Moving

Objective-C的方法签名问题


最近在日常开发中经常会使用这样的代码处理流程:
1. 根据字串获取对应的Selector
2. 根据Selector使用instanceMethodSignatureForSelector方法获取指定方法签名
3. 根据方法签名从集合中获取方法实参,再使用NSInvocation的invoke方法触发指定方法调用

起初,按照笔者的理解是Objective-C所有方法都应该有自己的方法签名,但实际上这种理解却让笔者踩到了坑里。
为何呢?请看下面的例子:

SEL selector1 = @selector(initWithBytes:length:encoding:);    
NSLog(@"%@", [NSString instanceMethodSignatureForSelector:selector1]);    
NSLog(@"%@", [[NSString new] methodSignatureForSelector:selector1]); 

上面的代码会是怎样的输出呢?
答案出乎笔者之前的意料,两个输出都是 null .

系统提供的函数怎么会没有函数签名呢?!!平常写代码的时候我们是可以正常调用的呢。顿时一头雾水。。。

为了一探究竟,笔者又尝试打印了下NSString的实例方法respondsToSelector的返回值,结果是NO。意思就是NSString的实例不能响应这个消息。随即看了下respondsToSelector的官方文档,但却有这样一句话:

Note that if the receiver is able to forward aSelector messages to another object, it 
will be able to respond to the message, albeit indirectly, even though this method 
returns NO.

翻译一下:如果the receiver能通过消息转发的机制把自己无法响应的消息转给另外一个对象,那么我们仍然认为the receiver是可以响应该消息的,间接响应。但是,respondsToSelector依旧返回NO。

看到这里,笔者心里貌似有了思路了,难道initWithBytes:length:encoding:是通过消息转发实现的? 首先解释下什么是Message Forward。
Alt text

有1个函数值的我们注意: forwardInvocation: . 这里完整解释下OC消息处理的整个过程,如下:

消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择
子”(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。第二阶段涉及“完整的
消息转发机制”(full forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就
无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息
相关的方法调用。这又细分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把
消息转给那个对象,于是消息转发过程结束,一切如常。若没有“备援的接收者”(replacement receiver),则启
动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次
机会,令其设法解决当前还未处理的这条消息。如果还是不能处理该方法,最后会抛出大家很熟悉的异常
NSInvalidArgumentException,具体为unrecognized selector sent to instance xxxx.

那么上面提到的函数则是处于第二阶段第二步,那好办了,我们可以在NSString的forwardInvocation:方法上打一个符号断点,看看到底能不能断下来。

打符号断点的方法有很多种,笔者这里比较喜欢使用最简单的方法:

bmessage -[NSString forwardInvocation:]

执行完成上述lldb的命令后,会有如下输出:

Setting a breakpoint at -[NSObject forwardInvocation:] with condition
(void*)object_getClass((id)$rdi) == 0x000000010b377e90
Breakpoint 2: where = libobjc.A.dylib`-[NSObject forwardInvocation:], address = 
0x000000010b4da2ea

其实就跟我们使用XCode提供的Add Symbolic Exception添加一个条件断点没啥区别。

bmessage是啥呢?请参考Facebook开源的LLDB命令扩展 Chisel . 另外在objc.io的站点文章里上也有对bmessage更为详细的解释,具体可参考中文翻译文章与调试器共舞 - LLDB 的华尔兹.

此时,断点已搞好,重新运行文初的代码,结果。。。。没有断下来!!!. 纳尼?难道我们的判断出错了,难道不是走的NSString的消息转发机制?一时间有陷入了迷茫中。。。

我靠,就这么玩意儿,这么常用,Apple还搞得这么神奇,不厚道啊!

那咋办呢?不搞清除,心里难受啊。能猜的都猜了,只剩一个方法了:逆向. 不入虎穴焉得虎子,我们只有到系统的Framework里去看看才能真正知道它到底做了什么。

为了简单方便,笔者决定逆向iOS库的模拟器版本,开搞,执行命令查看Foundation.framework的位置:

cd /Applications/Xcode-Beta.app/

find . -name Foundation.framework

得到如下:

./Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/Foundation.framework

将Foundation.framework拷贝到外面的某个位置,打开Hopper,逆向之。

在Hopper中搜索符号initWithBytes:length:encoding:, 发现NSString根本没有这个方法,相反有个没见过的类NSPlaceholerString却实现了这个方法,如图: Alt text

NSPlaceholderString是个什么鬼? 反正不是一个公开的类。难道NSString的背后是它? 为了验证这个怀疑,笔者决定再打个符号断点,一探究竟。
常规方法:

bmessage -[NSPlaceholderString initWithBytes:length:encoding:]

输出如下:

Setting a breakpoint at -[NSPlaceholderString initWithBytes:length:encoding:] with 
condition (void*)object_getClass((id)$rdi) == 0x000000010bd48f08
Breakpoint 2: where = Foundation`-[NSPlaceholderString initWithBytes:length:encoding:], 
address = 0x000000010b9faf1c

断点设置有效。运行文初的代码看效果,果然断点被fire了。

结论:NSString方法initWithBytes:length:encoding:的幕后是NSPlaceholderString的同名方法。

那NSPlaceholderString到底是什么类呢?带着疑问,笔者去stackoverflow提了一个问题,如链接: What is the relationship between NSString and NSPlaceholderString

笔者也有幸得到了期望的答案,如链接: How can i implement the behaviour as in cluster pattern by apple (NSString and NSCFString)

看了上面的链接,总结一下,可以这么理解: NSString的类方法alloc真正返回的是一个NSPlaceholderString的实例。 alloc的开发文档里有这样的解释:

For historical reasons, alloc invokes allocWithZone:

调用alloc方法其实是真正调用了allocWithZone:. 那么我们就到Hopper里看看这正的实现。
来看伪代码:

void * +[NSString allocWithZone:](void * self, void * _cmd, struct _NSZone * arg2) {
            eax = self;
            if (**0x2e7260 != eax) {
                eax = _NSAllocateObject(0x0, arg_8);
            }
            else {
                eax = *__placeholder;
                if (eax == 0x0) {
                    [NSPlaceholderString class];
                    eax = _NSAllocateObject(0x0, 0x0);
                    *__placeholder = eax;
                }
            }
            return eax;
        }

很明显,一目了然。

我们再来看一段代码:

NSString *factory = [NSString alloc];
NSString *theInstance = [factory initWithString:@"I am constant"];
NSLog(@"factory class: %@, instance class: %@", [factory class], [theInstance class]);

看输出:

factory class: NSPlaceholderString, instance class: __NSCFConstantString
分析到这里,我们基本已确定了NSPlaceholderString就是背后的鬼.

可是苹果为啥要这么做呢? 这里提到了一个概念Class Cluster类簇. Apple对类簇有开发文档里有很详尽的解释。 请各位小伙伴仔细研究文档:Class Clusters.这里不再獒述.

Posted by 张超 ·  Jan 18th, 2015
原创文章,版权声明:自由转载-非商用-非衍生-保持署名