iOS方法交换研究

1、Class/SEL/Method/IMP

Class+SEL=>Method=>IMP,...

2、class_

class_addMethod/class_replaceMethod/method_exchangeImplementations

class_addMethod//如果要添加的A方法已经存在则返回NO
A->B,B->B
method_exchangeImplementations
A->B,B->A
class_replaceMethod
A->B,B->B

3、消息处理机制

objc_msgSend/NSInvocation+NSMethodSignature/perfermSelector

1、更低层c语言实现
id objc_msgSend(id self, SEL op, ...)

2、oc的封装高级层
NSMethodSignature=返回值类型+参数(列)类型
SEL=函数名字
NSInvocation=target+SEL+NSMethodSignature
getArgumentTypeAtIndex:第0个参数是target,第1个参数是SEL,其它的参数(列)从第2,3,4,下标开始。

3、消息处理机制总结:

那么 objc_msgSend 到底是怎么工作的呢?

在Objective-C中,消息直到运行时才会绑定到方法的实现上。编译器会把代码中[target doSth]转换成 objc_msgSend消息函数,这个函数完成了动态绑定的所有事情。它的运行流程如下:

检查selector是否需要忽略。(ps: Mac开发中开启GC就会忽略retain,release方法。)

检查target是否为nil。如果为nil,直接cleanup,然后return。(这就是我们可以向nil发送消息的原因。)

然后在target的Class中根据Selector去找IMP

寻找IMP的过程:

先从当前class的cache方法列表(cache methodLists)里去找

找到了,跳到对应函数实现

没找到,就从class的方法列表(methodLists)里找

还找不到,就到super class的方法列表里找,直到找到基类(NSObject)为止

最后再找不到,就会进入动态方法解析和消息转发的机制。(这部分知识,下次再细谈)

0:对应SEL(对应参数)找到否 respondsToSelector
1:动态addmethod机会 实例方法调resolveInstanceMethod, 类方法调resolveClassMethod
2:重定向target机会 forwardingTargetForSelector
3:重定向NSInvocation机会 methodSignatureForSelector/forwardInvocation
4:无法识别异常退出 doesNotRecognizeSelector

4、容易混淆的类型


NSObject

@protocol NSObject

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}




Class
typedef struct objc_class *Class;
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//变量
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//方法名字+参数类型+imp函数指针
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//协议?
#endif

} OBJC2_UNAVAILABLE;



SEL
typedef  struct objc_selector *SEL
SEL本质是一个字符串,到底是结构体还是字符串


Method
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}       


IMP


id (*IMP)(id, SEL, ...)
typedef id (*IMP)(id,SEL,...) 要添加的方法


5、方法交换实践



//RAC的方法交换。selector和block
// 因为dealloc方法不能用selector,报错信息:ARC forbids use of 'dealloc' in a @selector
static const void *RACObjectCompoundDisposable = &RACObjectCompoundDisposable;

static NSMutableSet *swizzledClasses() {
    static dispatch_once_t onceToken;
    static NSMutableSet *swizzledClasses = nil;
    dispatch_once(&onceToken, ^{
        swizzledClasses = [[NSMutableSet alloc] init];
    });
    
    return swizzledClasses;
}

static void swizzleDeallocIfNeeded(Class classToSwizzle) {
    @synchronized (swizzledClasses()) {
        NSString *className = NSStringFromClass(classToSwizzle);
        if ([swizzledClasses() containsObject:className]) return;

        SEL deallocSelector = sel_registerName("dealloc");
        //__unsafe_unretained: 并不对其保持强引用,这一点和__weak修饰符的变量一样。当这块地址的内存被系统回收时,它仍然指向这个地址。weak会自动变为nil。 再次访问__unsafe_unretained释放了内存的地址,会产生奔溃。
        __block void (*originalDealloc)(__unsafe_unretained id, SEL) = NULL;

        id newDealloc = ^(__unsafe_unretained id self) {
            RACCompoundDisposable *compoundDisposable = objc_getAssociatedObject(self, RACObjectCompoundDisposable);
            [compoundDisposable dispose];

            if (originalDealloc == NULL) {
                struct objc_super superInfo = {
                    .receiver = self,
                    .super_class = class_getSuperclass(classToSwizzle)
                };

                void (*msgSend)(struct objc_super *, SEL) = (__typeof__(msgSend))objc_msgSendSuper;
                msgSend(&superInfo, deallocSelector);
            } else {
                originalDealloc(self, deallocSelector);
            }
        };
        
        IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);
        //如果当前类没有实现dealloc,则dealloc直接被替换为新的dealloc的block指针;如果实现了,则保存dealloc的block指针。
        if (! class_addMethod(classToSwizzle,
                            deallocSelector,
                            newDeallocIMP,
                            "v@:")) {
            // The class already contains a method implementation.
            Method deallocMethod = class_getInstanceMethod(classToSwizzle, deallocSelector);
            
            // We need to store original implementation before setting new implementation
            // in case method is called at the time of setting.
            originalDealloc = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
            
            // We need to store original implementation again, in case it just changed.
            originalDealloc = (__typeof__(originalDealloc))method_setImplementation(deallocMethod, newDeallocIMP);
        }

        [swizzledClasses() addObject:className];
    }
}


//  yykit 不支持协议方法的交换
BOOL swizzleInstanceMethod(Class aClass, SEL originalSel, SEL newSel) {
    //    SEL
    //    typedef  struct objc_selector *SEL
    //    SEL本质是一个字符串,到底是结构体还是字符串
    // Method
    //    struct objc_method {
    //        SEL method_name                                          OBJC2_UNAVAILABLE;
    //        char *method_types                                       OBJC2_UNAVAILABLE;
    //        IMP method_imp                                           OBJC2_UNAVAILABLE;
    //    }
    Method originalMethod = class_getInstanceMethod(aClass, originalSel);
    Method newMethod = class_getInstanceMethod(aClass, newSel);
    if (!originalMethod || !newMethod) return NO;
    // 动态添加方法
    //BOOL class_addMethod(Class cls, SEL name, IMP imp,  const char *types)
    class_addMethod(aClass,// 被添加的类
                    originalSel, // 方法名。如果存在则添加不了。如果不存在,可以添加。
                    
                    //        id (*IMP)(id, SEL, ...)
                    //        typedef id (*IMP)(id,SEL,...) 要添加的方法
                    class_getMethodImplementation(aClass, originalSel),
                    
                    method_getTypeEncoding(originalMethod));// 添加方法的返回类型和参数类型
    class_addMethod(aClass,// 被添加的类
                    // 指定一个类的方法(成员方法/类方法)
                    newSel,
                    // 把一个类的方法(成员方法/类方法),转化为c语言的函数指针。
                    class_getMethodImplementation(aClass, newSel),
                    // 参数:返回值类型,参数列表
                    method_getTypeEncoding(newMethod));
    
    method_exchangeImplementations(class_getInstanceMethod(aClass, originalSel),
                                   class_getInstanceMethod(aClass, newSel));
    return YES;
}

//   支持协议方法的交换
BOOL rp_classMethodSwizzle(Class aClass, SEL originalSelector, SEL swizzleSelector, SEL nopSelector) {
    
    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method swizzleMethod = class_getInstanceMethod(aClass, swizzleSelector);
    
    BOOL didAddMethod = class_addMethod(aClass,
                                        originalSelector,
                                        method_getImplementation(swizzleMethod),
                                        method_getTypeEncoding(swizzleMethod));
    
    if (didAddMethod) {
        Method nopMehtod = class_getInstanceMethod(aClass, nopSelector);
        // 方法1--最妥
        class_replaceMethod(aClass,
                        swizzleSelector,
                        method_getImplementation(nopMehtod),
                        method_getTypeEncoding(nopMehtod));
        // 方法2--如果直接掉nopMethod会麻烦
        //method_exchangeImplementations(nopMehtod, swizzleMethod);
        
        // 方法3-- 不支持协议方法的交换
        //char *typeeconding=method_getTypeEncoding(originalMethod);
        //IMP imp1=method_getImplementation(originalMethod);
        //// 如果imp参数为nil则不能替换
        //class_replaceMethod(aClass, swizzleSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        // 交换method1和method2的
        // A->B,B->A
        method_exchangeImplementations(originalMethod, swizzleMethod);
        //这种不行。没有交换,只是仅仅替换。A->B,B->B
        //   class_replaceMethod(aClass, originalSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    }
    
    return YES;
}


 // 处理特点:强制转化对应的函数指针。根据参数的个数,强转对应参数个数类型的函数
 id objc_msgSend(id self, SEL op, ...)
 
 
 //  先把objc_msgSend函数指针强制转化为void*类型,在强制转化为“返回void类型(无返回参数),3个参数:id,SEL,int8_t”这个类型的函数的指针。再调用此函数。
 ((void (*)(id, SEL, int8_t))(void *) objc_msgSend)((id)model, meta->_setter, (int8_t)num.charValue);
 
 //  先把objc_msgSend函数指针强制转化为void*类型,在强制转化为“返回SEL类型,2个参数:id,SEL”这个类型的函数的指针。再调用此函数。
 SEL value = ((SEL (*)(id, SEL))(void *)objc_msgSend)((id)self, propertyMeta->_getter);
 
 //  先把objc_msgSend函数指针强制转化为void*类型,在强制转化为“返回double类型,2个参数:id,SEL”这个类型的函数的指针。再调用次此数。
 double num = ((double (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter);
 
 
 // 先把objc_msgSend函数指针强制转化为void*类型,在强制转化为“返回void类型(无返回参数),3个参数:id,SEL,double这个类型的函数的指针。再调用次此数。
 ((void (*)(id, SEL, double))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num);
 
 
 Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
 value = v ? NSStringFromClass(v) : nil;
 void *pointer = ((void* (*)(id, SEL))(void *) objc_msgSend)((id)model, property->_getter);
 propertyDesc = [NSString stringWithFormat:@"%p",pointer];





/**
 1、
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
 
 + (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
 
 2、
 NSInvocation和NSMethodSingature的target,sel都不匹配也能行。sel的名不一样也行。
 但是sel的参数个数不一致,会闪退。
 如果NSMethodSignature返回nil,NSInvocation构建会闪退。
 所以target,sel 最好是一致匹配的。
 为了保证NSMethodSignature不返回nil,NSMethodSingature的target和NSInvocation的target要不一样。
 
 3、
 target是0,sel是1,其它参数是2,3,4....
 
 
 总结:
 NSMethodSignature/NSInvocation  可以向任意的target发送sel消息,传递参数,获取返回值。
 NSMethodSignature:定义:Sel名,参数,返回值
 
 */
+ (void)testNSInvocation{
    //SimpleNSInvocationClassA *a =[SimpleNSInvocationClassA new];
    //NSMethodSignature*signature= [a methodSignatureForSelector:@selector(method_b1:)];
    
    
    
    {
        SimpleNSInvocationClassB*b=[SimpleNSInvocationClassB new];
        NSMethodSignature*signature = [b methodSignatureForSelector:@selector(method_b1:)];
        NSInvocation *invocation=[NSInvocation  invocationWithMethodSignature:signature];
        invocation.target=b;
        invocation.selector=@selector(method_b1:);
        NSString*arg1=@"helloworld";
        [invocation setArgument:&arg1 atIndex:2];
        [invocation invoke];
    }
    
    
    
    {
        SimpleNSInvocationClassB*b=[SimpleNSInvocationClassB new];
        NSMethodSignature*signature = [b methodSignatureForSelector:@selector(method_b4:str2:str3:)];
        NSInvocation *invocation=[NSInvocation  invocationWithMethodSignature:signature];
        invocation.target=b;
        invocation.selector=@selector(method_b4:str2:str3:);
        NSString*arg1=@"helloworld";
        [invocation setArgument:&arg1 atIndex:2];
        BOOL result;
        [invocation getReturnValue:&result];
        
        
        /*
__unsafe_unretained: 并不对其保持强引用,这一点和__weak修饰符的变量一样。当这块地址的内存被系统回收时,它仍然指向这个地址。weak会自动变为nil。
再次访问__unsafe_unretained释放了内存的地址,会产生奔溃。
         NSNumber __unsafe_unretained *tempResult;
         [invocation getReturnValue:&tempResult];
         NSNumber *result = tempResult;
         return result;
         
         
         void *tempResult = NULL;
         [invocation getReturnValue:&tempResult];
         NSNumber *result = (__bridge NSNumber *)tempResult;
         return result;
         */
        [invocation invoke];
    }
    
    
    
}




+ (void)testNSINovcationReWrite {

    SimpleNSInvocationClassC*a=[SimpleNSInvocationClassC new];
    a.nocrash=[NoSelDoObj new];
    [a method_a];
    
    {
        TestMethod*tmp= [[self class]new];
        // 因为SimpleNSInvocationClassC没有method_b方法,所以NSMethodSignature用自己类method_b方法来构建,简单。如果用signatureWithObjCTypes,则比较麻烦。
        NSMethodSignature*sig=[tmp methodSignatureForSelector:@selector(method_b)];
        NSInvocation*invocation=[NSInvocation invocationWithMethodSignature:sig];
        [invocation setTarget:a];
        [invocation setSelector:@selector(method_b)];
        [invocation invoke];
    }
    
    
    {
        
        [a performSelector:@selector(method_b)];
    }
    
    
    {
        ((void (*)(id, SEL)) (void *)objc_msgSend)((id)a, @selector(method_b) );
    }
}


-(void)method_b{
    
    
}



@interface NoSelDoObj : NSObject

@end
@interface SimpleNSInvocationClassC : NSObject
@property(nonatomic,strong)NoSelDoObj*nocrash;


-(void)method_a;
@end


/**
 执行顺序:
 1、如果有对应的SEL,则直接执行SEL方法
 2、如果不存在对应的SEL则:
 resolveInstanceMethod/resolveClassMethod->返回YES,并动态添加方法,重新发送消息至动态添加的方法。返回NO,或者未动态添加方法,跳转到forwardingTargetForSelector
 forwardingTargetForSelector->由哪个target来对应,如果此target有对应的sel,则立即执行,如果此target没有对应的sel,则crash。如果返回target为nil,则继续判断。
 methodSignatureForSelector->由哪个NSMethodSignature(sel,返回值类型,参数类型)来对应,如果对应NSMethodSignature为nil,则跳转到最后一步doesNotRecognizeSelector,到这里发生crash
 forwardInvocation->重定向到此执行invoke
 doesNotRecognizeSelector->如果以上都无法对应,跳转到这里crash
 */


@implementation SimpleNSInvocationClassC
- (void)method_a{
    NSLog(@"a");
    
}

- (IMP)methodForSelector:(SEL)aSelector {
    
    
    return [super methodForSelector:aSelector];
}

+ (IMP)instanceMethodForSelector:(SEL)aSelector{
    
    return [super instanceMethodForSelector:aSelector];
}

/**
 unrecognized selector sent to instance 0x60000000ce90
 找不到sel,会闪退
 */
- (void)doesNotRecognizeSelector:(SEL)aSelector{
    
    return [super doesNotRecognizeSelector:aSelector];
}

/**
 2: 重定向到其它消息接受者来执行。
 返回其它消息SEL接受者。检查是否有合适的target,如果不自定义,默认返回nil
 */
- (id)forwardingTargetForSelector:(SEL)aSelector{
    id r= [super forwardingTargetForSelector:aSelector];
    //    if (!r&&sel_isEqual(aSelector, @selector(method_b))) {
    //        return _nocrash;
    //    }
    //    if (!r) {
    //        return _nocrash;
    //    }
    //
    
    return r;
}


/**
 3:  NSInvocation
 重定向到其它消息接受者、并对应SEL方法
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    //An array of characters containing the type encodings for the method arguments.
    //NSMethodSignature只包括参数。返回类型,参数(列)类型.通过Class和SEL可以获取参数
    //NSMethodSignature*sig=  [self.nocrash methodSignatureForSelector:aSelector];
    //NSMethodSignature*sig=[super methodSignatureForSelector:aSelector];
    NSMethodSignature*sig=  [self.nocrash methodSignatureForSelector:@selector(method_c)];
    return sig;
}


/**
 methodSignatureForSelector -> forwardInvocation
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSInvocation *invocation=anInvocation;
      //NSInvocation *invocation=[NSInvocation invocationWithMethodSignature:anInvocation.methodSignature];
    // 消息接受者。
    invocation.target=_nocrash;
    // SEL函数名。默认是找不到的消息SEL,可以改
    //invocation.selector=anInvocation.selector;
    invocation.selector=@selector(method_c);
    [invocation invoke];
    //   [super forwardInvocation:invocation];
    //   [super forwardInvocation:anInvocation];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    BOOL r= [super resolveClassMethod:sel];
    return r;
}

/**
 1: 执行动态添加方法的机会class_addMethod
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    BOOL r= [super resolveInstanceMethod:sel];
    // 返回YES,并动态添加方法,重新发送消息至动态添加的方法。
    //    if (sel==@selector(method_b)) {
    //        //Class+SEL->IMP, Class+SEL->Method->TypeEncoding
    //        class_addMethod([self class], sel, class_getMethodImplementation([TestMethod class], sel), method_getTypeEncoding(class_getInstanceMethod([TestMethod class], sel)));
    //        return YES;
    //    }
    // 返回NO,或者未动态添加方法,跳转到forwardingTargetForSelector
    return r;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    BOOL r=[super respondsToSelector:aSelector];
    return r;
}
@end

@implementation NoSelDoObj

-(void)method_b{
    MyLog(@"");
}
-(void)method_c {
    MyLog(@"");
}
@end




- (id)performSelectorWithArgs:(SEL)sel, ...{

NSMethodSignature * sig = [self methodSignatureForSelector:sel];
if (!sig) {
    [self doesNotRecognizeSelector:sel];
    return ((void *)0);
}
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
if (!inv) {
    [self doesNotRecognizeSelector:sel];
    return ((void *)0);
}

[inv setTarget:self];

[inv setSelector:sel];

va_list args;

__builtin_va_start(args, sel);

[NSObject setInv:inv withSig:sig andArgs:args];

__builtin_va_end(args);;

[inv invoke];

return [NSObject getReturnFromInv:inv withSig:sig];


}





最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,692评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,188评论 0 7
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 755评论 0 1
  • 继上Runtime梳理(四) 通过前面的学习,我们了解到Objective-C的动态特性:Objective-C不...
    小名一峰阅读 744评论 0 3
  • 大学的图书馆也分淡季和旺季,平时上课期间就是淡季,图书馆鲜有人来,但是到了期末就是旺季,图书馆随时会发生占座风波,...
    了望台阅读 286评论 0 1