OC底层源码/原理合集
建议先看下
IOS底层(十四): 消息流程(二)慢速查找
sel
: 方法编号, 可以理解成一本书的目录, 可通过对应名称找到页码imp
: 函数指针地址, 可以理解成书的页码, 方便找到具体实现的函数
慢速查找核心源码lookUpImpOrForward
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
书接上回
动态方法决议
慢速查找流程未找到后(即父类链, 元类链都没找到),会执行一次
动态方法决议
动态方法决议
可以理解成, 再给一次机会重新再来一次, 看看能不能找到imp
// 此次动态方法决议控制条件, 主要用于判断流程只走一次
if (slowpath(behavior & LOOKUP_RESOLVER)) {
// LOOKUP_RESOLVER = 2, 这里做的一个异或操作
// 如果a、b两个值不相同,则异或结果为1。
// 如果a、b两个值相同,异或结果为0。
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 当前方法没有未实现会走动态方法决议(可以理解成没找到再给次机会)
// 例如 1. performSelector:@selector(sayGunDan), 但是.h/.m sayGunDan方法并不存在
// 2. .h存在, .m没有实现方法, 也会走动态方法决议
// 判断当前类是否为元类
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
// 详细见下方: 源码分析一: resolveInstanceMethod 实例方法的动态方法决议
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
// 详细见下方: 源码分析二: resolveClassMethod 类方法的动态方法决议
resolveClassMethod(inst, sel, cls);
//
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
源码分析一: resolveInstanceMethod 实例方法的动态方法决议
如果当前类不是元类, 走resolveInstanceMethod
/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// lookUpImpOrNilTryCache 底层 lookUpImpTryCache,
// 判断方法是否有 resolveInstanceMethod 这个方法, 不存在直接return
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
// 发送一个消息, resolve_sel是上面的 SEL resolve_sel = @selector(resolveInstanceMethod:);
// 这个其实可以看我后面的例子, 其实是对resolveInstanceMethod这里面方法进行查找
// resolveInstanceMethod 可以重写, 自定义里面内容
// 如果 resolveInstanceMethod 写了方法并有imp , 则再次进行次后面的lookUpImpOrForward, 就能找到
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 无论结果如何只处理一次
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 调用lookUpImpOrNilTryCache, 再给次机会, 看下方法是否存在
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
// 这块处理一些报错内容
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
/***********************************************************************
* lookUpImpOrForward / lookUpImpOrForwardTryCache / lookUpImpOrNilTryCache
* The standard IMP lookup.
*
* The TryCache variant attempts a fast-path lookup in the IMP Cache.
* Most callers should use lookUpImpOrForwardTryCache with LOOKUP_INITIALIZE
*
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* With LOOKUP_NIL: returns nil on negative cache hits
*
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
// 快速查找判断类是否初始化
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
// 没有回去重新调用 lookUpImpOrForward(里面前部分有针对初始化处理)
return lookUpImpOrForward(inst, sel, cls, behavior);
}
// 已经初始化, 调用cache_getImp
IMP imp = cache_getImp(cls, sel);
// 如果 imp != null, 走done方法
if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
// 如果有缓存, 缓存查找
if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
}
#endif
// 快速查找, 如果 imp还是没找到
if (slowpath(imp == NULL)) {
// 调用 lookUpImpOrForward
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
// 底层还是调用 lookUpImpOrForward
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
流程图
总结: 实例方法动态方法决议
- 在
慢速查找
流程未找到方法实现时,会进行一次动态方法决议resolveMethod_locked
- 如果不是元类, 执行实例方法
resolveInstanceMethod
验证下动态方法决议 resolveInstanceMethod
建一个SAPerson
类, .h
里面写一个实例方法say666
, .m
中不给他实现
(用这个方法也行 performSelector:@selector()
, 主要是不给实现)
main
中初始化SAPerson
, 调用say666
方法
正常情况下会报方法找不到错误
SAPerson.m
方法里面重写下动态决议方法resolveInstanceMethod
, 运行我们再看一下
可看到虽然还是报错, 因为resolveInstanceMethod
中并没有给设置或者写什么imp
。接下来补全一下, 写一个sayHello
方法, 当say666
来了将其指向sayHello
, 运行可看到
(留意下只是大致模拟, 真正动态方法决议更复杂)
其实留意下会发现我上面的例子, 会有个疑问为什么没有指定方法, 动态方法决议走了2次? 而指定了方法( if (sel == @selector(say666)) ), 动态方法走了1次。
这块要等消息转发讲完之后才能解释
源码分析二: resolveClassMethod 类的动态方法决议
如果当是元类, 走resolveClassMethod
/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
//
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
可看到大致跟实例的类似, 当然我们也可以重写类
的动态方法决议留。留意下类方法的话, 要取元类, 方法用objc_getMetaClass()
由于类方法底层其实也是写成实例方法, 所以其实我们可以将动态方法决议正好到一起
注意: 不能整合之后不能放入SAPerson
中, 因为类方法在元类中以对象方法存在, 元类查询路线依次按 元类 → 根元类 → NSObject
, 而不会在当类查找, 所以不会走当前类的resolveInstanceMethod
。所以这块要写在NSObject 分类(Category )
方法里面(分类执行在类方法执行前面), 如下
这么重写
resolveInstanceMethod
, 主要处理找不到等情况, 当然重写了系统方法也会存在一些问题等。针对于系统方法被修改, 我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法, 例如: sa_model_xxxclick (前缀所在模块名称所做事物), 这样我们可以针对sa前缀进行hook, 可以在crash崩溃前pop到首页或者指定模块(封装界面 → 界面SDK上传),用于app线上防崩溃的处理等,提升体验。
总结
经过快速查找, 慢速查找都没有找到实现方法, 那么系统会给我一次机会, 即动态方法决议
-
判断类是否是
元类
- 如果是类,执行实例方法的动态方法决议
resolveInstanceMethod
- 如果是元类,执行类方法的动态方法决议
resolveClassMethod
,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
- 如果是类,执行实例方法的动态方法决议
如果动态方法决议中,如果找到, 则正常返回对应 imp
如果动态方法决议中,如果没有找到, 则进入消息转发
消息转发
针对于慢速查找imp
指针找不到, 动态方法决议也没有处理的方法, 接下来走消息转发。但是其实查看源码可看到并无消息转发的源码, 那么它到底是怎么执行的呢?
快速消息转发
lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
, 在logMessageSend
源码下方找到instrumentObjcMessageSends
的源码实现
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
// 留意下这里, objcMsgLogFD首次进入时候为-1, /tmp/msgSends是文件的写入目录
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
// 相当于开关, 调用是传YES, 用完传NO
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
- 建立一个main项目IOS创建个只有main.m工程, 里面建一个继承
NSObject
的类SATest
, .h里面有一个sayHello
, .m中不给他实现方法, main中调用
2.写入 instrumentObjcMessageSends
方法, 但是由于main中调用objc-class里面方法, 是不允许的, 所以需要借用extern
, extern void instrumentObjcMessageSends(BOOL flag);
这样保证正常调用
先运行一下依旧崩溃, 但是我们前往下/tmp/msgSends
(文件写入目录), 可看到里面有个msgSends-XXXXX
文件
打开msgSends
, 这里简单介绍下
-
resolveInstanceMethod
: 动态决议方法, 执行2次 -
forwardingTargetForSelector
: 消息快速转发方法, 执行2次, -
methodSignatureForSelector
: 消息慢速转发, 执行2次
那么我们可以模拟一下, SATest.m
中加入forwardingTargetForSelector
方法
可看到虽然crash但是, 的确走了forwardingTargetForSelector
方法, 说明我们探索方向是正确的, 接下来先看下cmd + shift + 0
, 查下forwardingTargetForSelector
解释
可看到 forwardingTargetForSelector
返回未识别消息应首先指向的对象, 通俗讲就是, 我们这个消息没人接收的话, 返回其第一继承人/接收者, 再通俗讲就是, 这个消息没人接收找个人接盘。
那么我们找个人来接收这个方法即可, 建立一个新类SATest1
(继承于NSObject
), 里面创建个sayHello
方法
可看到正常运行。留意下这里不能调用自身 [SATest alloc]
会循环引用。
慢速消息转发
快速消息转发 forwardingTargetForSelector
这里也没有找到, 则走慢速消息转发methodSignatureForSelector
。还是先看下定义, cmd + shift + 0
, 查下methodSignatureForSelector
。
最下边的
forwardInvocation
也留意下子类重写以将消息转发到其他对象。
返回一个NSMethodSignature对象(不能为空),该对象包含由Selector标识的方法。首先还是看一下该方法有没有走, SATest
中加methodSignatureForSelector
方法, 运行
虽然还是报错不过可看到, 的确在forwardingTargetForSelector
之后走了methodSignatureForSelector
方法, 那么接下来我们返回个签名即可
加一个forwardInvocation
, 可看到正常打印
接下来我们看一下NSInvocation
@interface NSInvocation : NSObject
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
@property (readonly, retain) NSMethodSignature *methodSignature;
- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;
@property (nullable, assign) id target;
@property SEL selector;
我们可以根据NSInvocation
打印获取部分信息
这块其实可以这样理解, 例如sayHello
方法, 最后大家都不接受, 那么就装到 Invocation
里面, Invocation
相当于一个漂流瓶(Invocation相当于保存), 漂流出去, 谁爱接收谁接收。当然我们也可以给他一个target
最后一段invoke
是触发开关, 什么时候调用都行。
总结
先看下 动态决议, 消息转发流程图
一起总结下 objc_msgSend
发送消息的流程
消息发送首先进行
快速查找流程
, 在类的缓存cache
中查找指定方法。快速查找流程
没有查找到走慢速查找流程
, 实例方法在当前类以及类继承链
中方法列表
查找, 类方法在元类以及元类继承链
中方法列表
查找。慢速查找流程
没有查找到会进行次动态方法决议
。动态方法决议
没有查找到会进行次消息转发
,快速消息转发
和慢速消息转发
2次补救机会。消息转发
也没找到的话,imp = forward_imp
, 走Crash崩溃报方法找不到异常: unrecognized selector sent to instance
附一
上面留下个动态方法决议走2次的问题
在源码resolveInstanceMethod
加断点, 每次走了say666
时在lldb
打bt
读一下内存段
在第二次中,我们可以看到是[NSObject(NSObject) methodSignatureForSelector:]
→ class_getInstanceMethod
再次进入动态方法决议, 看下他的源码
/***********************************************************************
* class_getInstanceMethod. Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
我们在class_getInstanceMethod
加个断点, 看一下
能看出的确方法走了这里, 而且在第二次打印
say666
之前走的。苹果在走到慢速查找签名invocation
之前,又再去查询查询一次,所以走到class_getInstanceMethod
这里,又去走了一遍lookUpImpOrForward
方法查询 say666
, 然后会再次走到动态方法决议。
接下来加forwardingTargetForSelector
, methodSignatureForSelector
, forwardInvocation
方法, 主要验证下是在慢速查找之前, 还是之后, 可看到
也可看到第二次动态方法决议
在慢速查找methodSignatureForSelector
和 forwardInvocation
方法之间。