iOS runtime 七: 方法查找与动态决议

2022-10-10  本文已影响0人  Trigger_o

快速查找

runtime将方法调用转换为objc_msgSend函数,尽管每个方法的返回值,参数可能不一样,
但是objc_msgSend可以做类型转换.
这个函数没有C++实现,直接是汇编代码实现.位于.s文件,不同的架构有不同的文件以及不同的汇编代码.
除了objc_msgSend还有几个相关的方法objc_msgSendSuper等.
以ENTRY为入口.

        ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    NilTest NORMAL
    GetIsaFast NORMAL       // r10 = self->isa
    // calls IMP on success
    CacheLookup NORMAL, CALL, _objc_msgSend
    NilTestReturnZero NORMAL
    GetIsaSupport NORMAL

// cache miss: go search the method lists
        LCacheMiss_objc_msgSend:
    // isa still in r10
    jmp __objc_msgSend_uncached
//...

这是x86_64的objc_msgSend,
NilTest,GetIsaFast,CacheLookup等等这些都是定义在当前文件中的其他代码段.
NilTest用于判断receive是否为空.
GetIsaFast获取isa
接下来CacheLookup就是快速查找IMP.
如果没找到,进入__objc_msgSend_uncached

ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    END_ENTRY _objc_msgSend

上面这段是arm64的_objc_msgSend.
x86_64没有SUPPORT_TAGGED_POINTERS的情况,arm64有.
首先也是查看receiver是否为空,如果是空,分成两种情况,一是tagged pointer,走LNilOrTagged,二是不支持tagged pointer,走LReturnZero.
LNilOrTagged和LReturnZero就在上面代码的最后部分.

ldr p13, [x0]
GetClassFromIsa_p16 p13, 1, x0
//p13拿到isa 通过isa拿到class放入p16寄存器

CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
//接下来走CacheLookup

    mov x15, x16            // stash the original isa
LLookupStart\Function:
    // p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    ldr p10, [x16, #CACHE]              // p10 = mask|buckets
    lsr p11, p10, #48           // p11 = mask
    and p10, p10, #0xffffffffffff   // p10 = buckets
    and w12, w1, w11            // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE]          // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
    tbnz    p11, #0, LLookupPreopt\Function
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
#else
    and p10, p11, #0x0000fffffffffffe   // p10 = buckets
    tbnz    p11, #0, LLookupPreopt\Function
#endif
    eor p12, p1, p1, LSR #7
    and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11           // p11 = mask = 0xffff >> p11
    and p12, p1, p11            // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

    add p13, p10, p12, LSL #(1+PTRSHIFT)
                        // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

                        // do {
1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel != _cmd) {
    b.ne    3f              //         scan more
                        //     } else {
2:  CacheHit \Mode              // hit:    call or return imp
                        //     }
3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10            // } while (bucket >= buckets)
    b.hs    1b

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                        // p13 = buckets + (mask << 1+PTRSHIFT)
                        // see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p13, p10, p11, LSL #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                        // p12 = first probed bucket

                        // do {
4:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel == _cmd)
    b.eq    2b              //         goto hit
    cmp p9, #0              // } while (sel != 0 &&
    ccmp    p13, p12, #0, ne        //     bucket > first_probed)
    b.hi    4b

这里有一个CACHE_MASK_STORAGE,

#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif

所以arm64真机是CACHE_MASK_STORAGE_HIGH_16

p1的类型是SEL,也就是objc_msgSend传进来的参数_cmd, p16是刚才的isa.

ldr p11, [x16, #CACHE] // 这里p11 = mask|buckets,也就是bucket的首地址,

这里CONFIG_USE_PREOPT_CACHES是1,并且我们看带签名的部分,也就是__has_feature(ptrauth_calls)是true.

tbnz p11, #0, LLookupPreopt\Function //判断cache_t存在

and p10, p11, #0x0000fffffffffff //取出后48位,这部分是buckets,从前面的内容我们知道bucket是连续存储的.

eor p12, p1, p1, LSR #7 //eor异或,LSR #7 是右移7位,这就是前面说到的cache_hash函数在CONFIG_USE_PREOPT_CACHES为1的时候的算法,是通过这个hash算法获得SEL的hash值,也就是在buckets中的顺位.
上面说到p1是SEL,所以这个就是p12 = SEL ^ (SEL >> 7),在cache_hash函数里是这么写的value ^= value >> 7.
and p12, p12, p11, LSR #48 // 不过这里最后还要&上mask,最终得到下标.

add p13, p10, p12, LSL #(1+PTRSHIFT)// 刚才p12是下标,p10是bucket首地址,PTRSHIFT是3,这里是首地址加上下标左移4位.指向了一个bucket内存.

1:  ldp p17, p9, [x13], #-BUCKET_SIZE   
    cmp p9, p1          
    b.ne    3f              
2:  CacheHit \Mode          
3:  cbz p9, \MissLabelDynamic       
    cmp p13, p10            
    b.hs    1b

这一段是do while,
ldp p17, p9, [x13], #-BUCKET_SIZE //p13是刚才指向的bucket,-BUCKET_SIZE是一个bucket的大小,意思是指向前面一个bucket.放在p9,
这一步对标cache_next函数.

cmp p9, p1 //如果p9不是_cmd.
b.ne 3f //那么去执行3

CacheHit \Mode //p9就是_cmd,就执行CacheHit

cbz p9, \MissLabelDynamic //如果p9是空的,执行MissLabelDynamic函数.

cmp p13, p10 //如果p13大于p10,也就是说bucket不是第0个.
b.hs 1b //就执行1,向前移动.

add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) //如果向前移动到了首地址,还没匹配到,那就把p13移动到最后一个bucket

add p12, p10, p12, LSL #(1+PTRSHIFT) //刚才cmp p13, p10是从中间到首地址,现在修改p10,到刚才的p13后面一个,意思就是已经查看过的部分就不再查看了

4: ldp p17, p9, [x13], #-BUCKET_SIZE // 取出sel

        cmp p9, p1          
    b.eq    2b              
    cmp p9, #0          
    ccmp    p13, p12, #0, ne    
    b.hi    4b

还是do while,如果相等,就执行2, 否则就继续移动,直到走到新的首地址.


上面使用了和cache_hash和cache_next相同的算法来查找,也就是存的取的方法是一样的,只要存了,那一定取的到,
如果没存,那么在ldp p17, p9, [x13], #-BUCKET_SIZE这一步,p9就是空的,
然后cbz p9, \MissLabelDynamic就会跳转到MissLabelDynamic.
这个MissLabelDynamic是什么呢.

/*
 * CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant
*/
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

CacheLookup是这么定义的,MissLabelConstant是参数,是一个函数

回到前面看objc_msgSend调用cacheLookup的地方

GetClassFromIsa_p16 p13, 1, x0  // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

MissLabelConstant传的是objc_msgSend_uncached,所以我们去看这个函数

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
    STATIC_ENTRY __objc_msgLookup_uncached
    UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
    MethodTableLookup
    ret
    END_ENTRY __objc_msgLookup_uncached

里面执行的是MethodTableLookup

.macro MethodTableLookup
    
    SAVE_REGS MSGSEND
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward

    // IMP in x0
    mov x17, x0
    RESTORE_REGS MSGSEND
.endmacro

MethodTableLookup里面执行的是lookUpImpOrForward函数,
传参是lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER).
这个函数就是在快速查找失败的时候会执行的,它是C++实现.位于objc-runtime-new.mm


还是CacheLookup的定义

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x10, x1, x16    // authenticate imp and re-sign as IMP
    cmp x16, x15
    cinc    x16, x16, ne            // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
    ret             // return imp via x17
.else

mode有三种,NORMAL,GETIMP,和LOOKUP
objc_msgSend里面传的就是NORMAL.是验证并调用imp

当传入GETIMP时,就是_cache_getImp这个函数的实现,

IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil);

这个函数也是频繁的被使用.用于获取imp.

LOOKUP模式是验证并重签名imp,不会调用.

慢速查找

上面讲到,快速查找失败会执行lookUpImpOrForward函数,这个函数位于objc-runtime-new.mm

enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
};

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())) {
        behavior |= LOOKUP_NOCACHE;
    }

这个方法是在汇编中调用的时候,传参是这样的

// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward

obj是x0,sel是x1,cls是x2,最后的参数behavior是x3,传的是3.

首先判断类是否初始化isInitialized,这个条件一般不会进来,如果类没有初始化,behavior = 0011 | 1000 = 1011.

checkIsKnownClass(cls);
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    runtimeLock.assertLocked();
    curClass = cls;

判断类是否被添加到类表中,也就是存不存在这个类,如果不存在,就会触发断言.
然后调用realizeAndInitializeIfNeeded_locked初始化类,behavior & LOOKUP_INITIALIZE是0011 & 0001 或者1011 & 0001总之一定是true.

 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 {
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                imp = forward_imp;
                break;
            }
        }
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

这是一个最多执行attempts次的for循环,或者说是无限循环,当(--attempts == 0)时会直接触发断言.
attempts是一个相当大的数,它是这么实现的

static unsigned unreasonableClassCount()
{
    runtimeLock.assertLocked();
    int base = NXCountMapTable(gdb_objc_realized_classes) +
    getPreoptimizedClassUnreasonableCount();
    return (base + 1) * 16;
}

它是以静态类表的大小加上dyld的类的个数为基础,乘上16得到的一个不真实的数字,因为那个for循环不需要真实的循环次数,只要足够就行.

isConstantOptimizedCache用于判断是否缓存优化.
CONFIG_USE_PREOPT_CACHES在arm64并且iOS时是1.
cache_getImp前面说到了是cahcelookup的getImp mode,用来查找imp.
如果满足这些条件,就会走快速查找,不过cahcelookup的getImp mode失败了不走lookUpImpOrForward,所以不会循环.

另一种情况则是去rw中找method_t, getMethodNoSuper_nolock函数是从类本身查找.
前面curClass首先指向传进来的cls.

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

从class获取methods(),这是一个list_array_tt,可能会有两层结构,内层结构用search_method_list_inline来迭代.
search_method_list_inline这个函数在上一篇有说明.

如果从类自己身上没找到的话,curClass = curClass->getSuperclass(),
curClass会指向父类,顺便判断如果没有父类了,就赋值imp为forward_imp,这个等下再说.

接下来是前面有提到的for循环最大执行次数.

然后就是从父类查找,如果找到了就跳到done,没有就继续循环.

    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;

如果for循环中找到了imp,就会跳转到done或者done_unlock,
如果没有找到imp,但是从for循环break了,就会走resolveMethod_locked.
但是它是有条件的,为什么要设置条件呢,因为resolveMethod_locked的流程中也会调用lookUpImpOrForward,
这行限制改变了behavior,第二次进来的时候就不满足条件了.

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

这个过程叫做动态方法决议.
做了两件事,
1.如果是元类就调用resolveClassMethod,如果是类对象就调用resolveInstanceMethod
2.调用lookUpImpOrForwardTryCache
lookUpImpOrForwardTryCache就是_lookUpImpTryCache,它里面干了两件事,
一是快速查找,二是慢速查找.
为什么要再来次,这是因为步骤1,也就是动态决议,给了程序一个临时添加方法的机会,添加之后,再走一次查找流程.

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        return;
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);
    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));
        }
    }
}

首先获取了一个SEL,resolveInstanceMethod,并且判断class是否实现了这个sel.
如果实现了,就给class发消息,调用resolveInstanceMethod这个方法.
然后调用lookUpImpOrNilTryCache查询一次sel是否存在了.

void newFunc(){
    NSLog(@"新添加的method6");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"未知的方法 %@",NSStringFromSelector(sel));
    if([NSStringFromSelector(sel) isEqualToString:@"myMethod6"]){
        class_addMethod(self, sel, (IMP)newFunc, "");
        return true;
    }
    return [super resolveInstanceMethod:sel];
}

//main.m
id myObj = [MyClass alloc];
MyClass *my = (MyClass *)[myObj init];
[my performSelector:NSSelectorFromString(@"myMethod6")];

写一个demo,可以通过在bool resolved = msg(cls, resolve_sel, sel);断点,
看到消息发送之后,输出了"未知的方法 myMethod6".
放开断点输出"新添加的method6".


在done的位置还调用了log_and_fill_cache函数

log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}

这个函数的作用是给cache_t添加数据.这部分的内容在上一篇.
上面的logMessageSend是输出方法调用的信息.

void instrumentObjcMessageSends(BOOL flag)

通过这个函数可以设置是否输出.


前面提到了一个forward_imp.

const IMP forward_imp = (IMP)_objc_msgForward_impcache;

_objc_msgForward_impcache没有c++的实现,但是可以在.s里找到汇编实现

STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward
``
里面就一句,执行__objc_msgForward,然后__objc_msgForward的实现紧接着在下面,
调用__objc_forward_handler,最终返回一个x17.

__objc_forward_handler又回到了c++代码

// Default forward handler halts the process.
attribute((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void _objc_forward_handler = (void)objc_defaultForwardHandler;

这就是转发的默认实现,会直接终止程序,错误信息unrecognized selector sent to instance ...
老朋友了属于是.

这个函数是可以修改的.

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;

if SUPPORT_STRET

_objc_forward_stret_handler = fwd_stret;

endif

}

上一篇下一篇

猜你喜欢

热点阅读