Objective-C runtime运行时详解

最近看了一下runtime运行时方面的文章,总结一下,加上自己的一下理解,如果文章有未全和不足的地方,欢迎各位在下方留言补充和指正。

image.png

目录:

  1. runtime 消息机制
  2. runtime 添加属性
  3. runtime 交换方法
  4. runtime 动态添加方法
  5. runtime Class的常见方法

1.获取成员变量列表
2.获取属性列表
3.获取方法列表
4.获取协议列表
5.获得类方法
6.获得实例方法
7.添加方法
8.替换原方法实现
9.交换两个方法

  1. runtime method swizzling 黑魔法

runtime 消息机制

对于OC代码,调用方法的实质就是一个消息发送,OC底层通过runtime实现
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。

  • 首先必须要导入头文件 #import <objc/message.h>

  • 在使用objc_msgSend方法编译时可能出现报错的情况,对应的解决办法如下
    image.png
  • 新建一个Person类 Person.h中代码

/** 实例方法*/
// 无参数无返回值
- (void)run1;
// 无参数有返回值
- (NSString *)run2;
// 有参数无返回值
- (void)run3:(NSString *)string;
// 有参数有返回值
- (NSString *)run4:(NSString *)string;

/** 类方法*/
// 无参数无返回值
+ (void)run1;
// 无参数有返回值
+ (NSString *)run2;
// 有参数无返回值
+ (void)run3:(NSString *)string;
// 有参数有返回值
+ (NSString *)run4:(NSString *)string;
  • Person.m中代码
- (void)run1 {
    NSLog(@"实例方法 :run1");
}

- (NSString *)run2 {
    return @"实例方法 :run2";
}

- (void)run3:(NSString *)string {
    NSLog(@"run3 --- 参数:%@", string);
}

- (NSString *)run4:(NSString *)string {
    return [NSString stringWithFormat:@"run4 --- 参数:%@", string];
}

+ (void)run1 {
    NSLog(@"run1 classMethod");
}

+ (NSString *)run2 {
    return @"run2 classMethod";
}

+ (void)run3:(NSString *)string {
    NSLog(@"run3 classMethod --- 参数:%@", string);
}

+ (NSString *)run4:(NSString *)string {
    return [NSString stringWithFormat:@"run4 classMethod --- 参数:%@", string];
}
  • 利用objc_msgSend调用上面的这些方法
/** 对象方法/实例方法 */
    // 底层的实际写法
    Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    person = objc_msgSend(person, sel_registerName("init"));

    NSLog(@"无参数无返回值:");
    
    ((void (*) (id, SEL)) (void *)objc_msgSend)(person, sel_registerName("run1")); 
    
    objc_msgSend(person, @selector(run1));
    
    NSLog(@"无参数有返回值:");
    
    NSString *run2Return1 = ((NSString *(*) (id, SEL)) (void *)objc_msgSend)(person, sel_registerName("run2"));
    NSLog(@"%@", run2Return1);
    
    NSString *run2Return2 = objc_msgSend(person, @selector(run2));
    NSLog(@"%@", run2Return2);
    
    NSLog(@"有参数无返回值:");
    
    ((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run3:"), @"3333");

    
    NSLog(@"有参数有反回值:");
    
    NSString *run4Return1 = ((NSString *(*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run4:"), @"4444");
    NSLog(@"%@", run4Return1);
    
    NSLog(@"--------------------------");
    
    /** 类方法 */
    
    NSLog(@"classMethod - 无参数无返回值:");
    
    ((void (*) (id, SEL)) (void *)objc_msgSend)(Person.class, sel_registerName("run1"));
    
    objc_msgSend(Person.class, @selector(run1));
    
    NSLog(@"classMethod - 无参数有返回值:");
    
    NSString *run2Return1Class = ((NSString *(*) (id, SEL)) (void *)objc_msgSend)(Person.class, sel_registerName("run2"));
    NSLog(@"%@", run2Return1Class);
    
    NSString *run2Return2Class = objc_msgSend(Person.class, @selector(run2));
    NSLog(@"%@", run2Return2Class);
    
    NSLog(@"classMethod - 有参数无返回值:");
    
    ((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(Person.class, sel_registerName("run3:"), @"3333");
    
    NSLog(@"classMethod - 有参数有反回值:");
    
    NSString *run4Return1Class = ((NSString *(*) (id, SEL, NSString *)) (void *)objc_msgSend)(Person.class, sel_registerName("run4:"), @"4444");
    NSLog(@"%@", run4Return1Class);
  • 注:
/**
   错误写法(arm64崩溃偶尔发生)
   */
    objc_msgSend(person, sel_registerName("run3:"), @"12345678");
    /**
     标准写法
     */
    ((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run3:"), @"不能直接写objc_msgSend,会出现崩溃的现象(正常应该是可以的)");
  • 解释一下标准写法前面参数
((void (*) (id, SEL)) (void *)objc_msgSend)
1、第一个void代表是否有返回值
   如果返回值是NSString类型的 例:无参数有返回值的方法
   写法:((NSString *(*) (id, SEL)) (void *)objc_msgSend)
2、(id, SEL)
   如果方法有参数,参数类型是NSString 例:有参数无返回值
   写法:((void *(*) (id, SEL, NSString *)) (void *)objc_msgSend)
3、关于离objc_msgSend最近的void从互联网上还没找到具体含义,还望知道的好友留言或私信告知
应用与注意

注:使用objc_msgSend()创建对象不能自动释放,对象需要手动release。使用runtime执行初始化方法创建的对象的时候是不在ARC控制之下的,所以在该类销毁的时候需要手动release
应用:使用objc_msgSend()创建对象时好处,应用之一就是在一个控制器要跳转多个控制器的时候,不再需要每个控制器都单独写一遍初始化,也不再需要每一个控制器单独写release方法,使用runtime的话,使用一个或者根据情况使用几个回调,返回控制器的类名以及相应的参数就好了。

Class class = objc_getClass(controllerName.UTF8String); //或者 NSStringFromClass(<#Class  _Nonnull __unsafe_unretained aClass#>)
        
id viewController = ((id(*)(id,SEL))objc_msgSend)(class,NSSelectorFromString(@"new"));

 [self xpz_pushViewController:viewController];
// 导航控制器获得控制权后进行release即可
- (void)xpz_pushViewController:(__kindof UIViewController *)viewController
{
    [self pushViewController:viewController];
    
    //release
    ((void(*)(id,SEL))objc_msgSend)(viewController,NSSelectorFromString(@"release"));
}

category添加属性

1.面试中经常会被问到如何给category添加属性,在平时我们偶尔也会遇到想要在分类中添加属性的情况,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成,其实可以使用runtime动态添加属性方法
2.还有另外一种方法: use @dynamic or provide a method implementation in this category

1.使用runtime动态添加属性

// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
// object:给哪个对象添加属性
// key:属性名称
// value:属性值
// policy:保存策略
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
  • 需求:在UIView上添加一个播放器
  • objc_getAssociatedObject有两个参数,第一个参数为从该object中获取关联对象,第二个参数为想要获取关联对象的key;

对于第二个参数const void *key,有以下四种推荐的key值:

  1. 声明 static char kAssociatedObjectKey;,使用 &kAssociatedObjectKey 作为 key 值;
  2. 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey;,使用 kAssociatedObjectKey 作为key值;
  3. selector ,使用 getter 方法的名称作为key值;
  4. 而使用_cmd可以直接使用该@selector的名称,即hideButton,并且能保证改名称不重复。(与上一种方法相同)
static char playerViewKey;  // playerView
static void *playerLayerKey = &playerLayerKey; // playerLayer
/************************************************* playerView ********************************************************/
// getter
- (UIView *)playerView {
    UIView *_playerView = objc_getAssociatedObject(self, &playerViewKey);
    if (!_playerView) {
        objc_setAssociatedObject(self, &playerViewKey, _playerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _playerView;
}
// setter
- (void)setPlayerView:(UIView *)playerView {
    return objc_setAssociatedObject(self, &playerViewKey, playerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}




/************************************************* avplayer ********************************************************/
//getter
- (AVPlayer *)avPlayer {
    AVPlayer *_avPlayer = objc_getAssociatedObject(self, @selector(avPlayer));
    if (!_avPlayer) {
        objc_setAssociatedObject(self, @selector(avPlayer), _avPlayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _avPlayer;
}
//setter
- (void)setAvPlayer:(AVPlayer *)avPlayer {
    objc_setAssociatedObject(self, @selector(avPlayer), avPlayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}



/************************************************* playerLayer ********************************************************/
- (AVPlayerLayer *)playerLayer {
    return objc_getAssociatedObject(self, playerLayerKey);
}
- (void)setPlayerLayer:(AVPlayerLayer *)playerLayer {
    objc_setAssociatedObject(self, playerLayerKey, playerLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}



/************************************************* playerItem ********************************************************/
/**
 getter方法
 */
- (AVPlayerItem *)playerItem {
    return objc_getAssociatedObject(self, _cmd);
}
/**
 setter方法
 */
- (void)setPlayerItem:(AVPlayerItem *)playerItem {
    objc_setAssociatedObject(self, @selector(playerItem), playerItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

扩展:

objc_getAssociatedObject与objc_setAssociatedObject方法另一种用途,在tableViewCell中的btn点击事件中使用

在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;方法中使用:
if (self.GroupListArr.count > 0) {
            cell.hidden = NO;
            CB_ActivityGroupModel *model = self.GroupListArr[0];
            [cell setModel:model];
            [cell.joinGroupBtn addTarget:self action:@selector(joinGroupAction:) forControlEvents:(UIControlEventTouchUpInside)];
            objc_setAssociatedObject(cell.joinGroupBtn, &joinGroup, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
使用
- (void)joinGroupAction:(UIButton *)btn
{
    CB_ActivityGroupModel *model = objc_getAssociatedObject(btn, &joinGroup);
    CB_ActivityGroupDetailsVC *vc = [[CB_ActivityGroupDetailsVC alloc]init];
    vc.goodModel = self.detailsModel;
    vc.model = model;
    [self.navigationController pushViewController:vc animated:YES];
}

runtime 交换方法

当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

class_getInstanceMethod 得到类的实例方法
class_getClassMethod 得到类的类方法

  • 给系统的imageNamed添加额外功能
#import "UIImage+Image.h"
#import <objc/runtime.h>

@implementation UIImage (Image)
+ (void)load
{
    Class class = [self class];
    // 类方法
    // 1.获取 imageNamed方法地址
    Method originalMethod = class_getClassMethod(class, sel_registerName("imageNamed:"));
    // 2.获取 xpz_imageNamed方法地址
    Method swizzledMethod = class_getClassMethod(class, @selector(xpz_imageNamed:));
    // 交换 imageNamed:
    method_exchangeImplementations(originalMethod, swizzledMethod);
    
}
/**
 看清楚下面是不会有死循环的
 调用 imageNamed => xpz_imageNamed
 调用 xpz_imageNamed => imageNamed
 */
+ (nullable UIImage *)xpz_imageNamed:(NSString *)name{
    UIImage *xpz_image = [UIImage xpz_imageNamed:name];
    if (xpz_image) {
        NSLog(@"runtime添加额外功能--加载成功");
    } else {
        NSLog(@"runtime添加额外功能--加载失败");
    }
    return xpz_image;
}
@end

/**
 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
 所以第二步,我们要 自己实现一个带有扩展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {
 
 }
 */
  • 给UIViewController的viewWillAppear添加额外功能,就可以再控制台看到每次控制器的变化
#import "UIViewController+hook.h"
#import <objc/runtime.h>

@implementation UIViewController (hook)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(hook_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)hook_viewWillAppear:(BOOL)animated {
    [self hook_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

打印

[控制器:UIViewController+hook.m -- line: 53行]  viewWillAppear: <NavigationViewController: 0x1060bfc00>
[控制器:UIViewController+hook.m -- line: 53行]  viewWillAppear: <GroupViewController: 0x105a6c960>
[控制器:UIViewController+hook.m -- line: 53行]  viewWillAppear: <NavigationViewController: 0x10607e400>
  • 为什么要添加didAddMethod判断?

先尝试添加原SEL其实是为了做一层保护,因为如果这个类没有实现originalSelector,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法,这当然不是我们想要的。所以我们先尝试添加 orginalSelector,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。

大概的意思就是我们可以通过class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。

runtime动态地添加方法

float runtime_addMethod(id receiver, SEL sel, const void *arg1, int arg2)
{
    NSLog(@"方法名:%s, 参数1:%@, 参数2:%d", __FUNCTION__, [NSString stringWithUTF8String:arg1], arg2);
    return 1;
}

    Person_runtimeVC *vc = objc_msgSend(objc_getClass("Person_runtimeVC"), sel_registerName("alloc"));
    vc = objc_msgSend(vc, sel_registerName("init"));
    /** 添加方法*/
    // 动态添加run方法
    // class: 给哪个类添加方法
    // SEL: 添加哪个方法,即添加方法的方法编号
    // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
    // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
    class_addMethod([vc class], NSSelectorFromString(@"runtime_addMethod"), (IMP)runtime_addMethod, "f@:r^vd");
    int returnValue = ((float (*)(id, SEL, const void *, int))objc_msgSend)((id)vc, NSSelectorFromString(@"runtime_addMethod"), "参数1", 10086);
    NSLog(@"返回值:%d", returnValue);
    NSLog(@"%s", @encode(const void *));


/**打印结果*/
[控制器:ViewController.m -- line: 108行]    方法名:runtime_addMethod, 参数1:参数1, 参数2:10086
[控制器:ViewController.m -- line: 74行] 返回值:1
[控制器:ViewController.m -- line: 75行] r^v
  • 参数:"f@:r^vd"
  第1个字符:表示函数(方法)返回值类型,这里返回值类型是 `float` ,故为 `f`
  第2、3个字符:苹果解释是由于函数(方法)至少带有两个参数(self和_cmd)还记得之前的 (id,SEL) 么,所以第2、3个字符必须是 ‘@:’,其实我们当做固定写法就好了
  第4个字符根据NSLog(@"%s", @encode(const void *));打印结果可以看出来r^v是第三个参数的类型,第四个参数是int,所以是d

苹果官方其他类型对照表

runtime 常见方法

原著https://www.jianshu.com/p/46dd81402f63

  • 获取成员变量
/** 获取类中的所有成员变量*/
    Ivar *ivarList = class_copyIvarList([Person_runtimeVC class], &count);
    for(int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        NSLog(@"获取类成员变量:%@  ---  %@",ivarName,key);
    }
  • 获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
 for (unsigned int i=0; i<count; i++) {
     const char *propertyName = property_getName(propertyList[i]);
     NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
 }
  • 获取方法列表
   Method *methodList = class_copyMethodList([self class], &count);
   for (unsigned int i; i<count; i++) {
       Method method = methodList[i];
       NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
   }
  • 获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
   for (unsigned int i; i<count; i++) {
       Protocol *myProtocal = protocolList[i];
       const char *protocolName = protocol_getName(myProtocal);
       NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
   }

现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法

  • 获得类方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 获得实例方法
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
  • 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  • 交换两个方法
method_exchangeImplementations(oriMethod, cusMethod);

什么是 method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换
  • 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP


    image

selector --> 对应的IMP

runtime知识很多,后期会慢慢补充,欢迎广大简友补充学习;刚学习使用Markdown,排版上做的不是太好。
本篇笔记部分参考自以下:

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 本文转载自:http://southpeak.github.io/2014/10/25/objective-c-r...
    idiot_lin阅读 916评论 0 4
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 788评论 0 4
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,159评论 0 7
  • 作业1:好好回想一下,小时候呆呆看过什么。挑选一个印象或一幅画面写出来。 2. 尝试随时记录,确定自己更适合手机记...
    米果书阅读 161评论 3 0