玩转runtime的前世今生看我足矣

完善这篇文章的目的在于巩固一下自身,之前的并不完善,对有些问题的理解也不是很透彻,今儿重温了下,在之前的基础上,再次进行完善,也算自己对技术知识上的负责任吧。昨晚听了汉克老师的公开课,讲的挺好的,其实很多时候不管是学习上的还是生活中的某些事情,都会发现我们无法一直坚持下去,也许外在原因是一因素,但是很多时候还是在于自我的放松,这里不得不提一下汉克老师在直播说过这么一句话——反馈机制。

比如说:打游戏。有时候一下就是一上午,有时候天天都会去玩一把。为什么打游戏就能坚持,对于学习或者热点一直不减的减肥话题。大多数都是不了了知了呢。

分析原因:
上面有说过反馈机制,游戏里面会产生等级的提升、金币的上涨、英雄的购买,对此你可能产生了一个欲望,一种想要更多的欲望,于是有了有瘾的说法,站在产品的角度来看,这就是这一种及时性的反馈机制。而对于减肥以及学习始终不能坚持下去的根源在于什么呢?比方说学习,可能你学的东西,你用不上,减肥也是一个漫长的过程,今天重量也许比以往轻了点,结果每次看到好吃的,一下就绷不住了,往上飙了好几斤。由于反馈不及时,结果导致泄气,放弃。

引言

对于从事 iOS 开发人员来说,太多数人都会说「 runtime 是运行时 」
什么情况下用 runtime ?大部分人能说出「 给分类动态添加属性 || 交换方法」
runtime – 运行时(iOS的黑魔法!!)
runtime是OC的底层实现,可以静心一些非常底层的操作(OC无法办到的)

一.runtime简介
二.消息机制<了解/熟悉>
2.1消息机制原理
2.2消息调用流程
三.常用开发场景
3.1 UITextField占位文字的颜色以及字体大小
3.2 给分类动态添加属性
四.面试须知
4.1 动态添加方法(开发中几乎不用,但面试须知)
4.2 使用runtime实现自动归档和解档(之前傻逼式的一个一个写,昨天看了hanK的公开课,顿时打开眼界,原来还能这么搞,大写的服)
4.3 Method Swizzling方法交换(俗称黑魔法,也有说法叫方法欺骗)
4.4 runtime 字典转模型(这个本文不会讲到,因为解析都是用第三方框架了,成熟且实用便利。MJExtension字典转模型实现也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来)
五.利用runtime进行实用性封装
5.1 UIAlertView的封装
5.2 Method Swizzling方法交换封装
5.3 自动归档和解档定义成宏

一. runtime简介:

  • runtime简称运行时,OC就是运行机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
  • 对于C语言,函数的调用在编译的时候回决定调用哪个函数
  • 对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用

二.消息机制

2.1消息机制原理

方法调用本质:让对象发送消息

  • objc_msgSend只有对象才能发送消息,故以objc开头
  • 使用消息机制,必须导入#import<objc/message.h>
  • runtime都有一个前缀,谁的事情使用谁

解决提示步骤

  • 查找build setting -> 搜索msg ->设为NO

查看生成runtime代码,首先终端切换到该目录回车后输入clang -rewrite-objc main.m

2.2 消息调用流程
- 1.通过isa去对应的类中查找
- 2.注册方法编号
- 3.根据方法编号去查找对应方法
- 4.找到只是最终函数实现地址,根据地址去方法区调用对应函数
消息机制原理图
/*************ViewController.m******************/
-(void)setupobjc_msgSend{
/*
 objc_msgSend
<#id self#> :谁发送消息
 <#SEL op, ...#>:发送什么消息
 */
    //  id objc = objc_msgSend([NSObject class], @selector(alloc));
    //  objc = objc_msgSend(objc, @selector(init));

    //    OC: Person *p = [Person alloc];
    // 底层的实际写法
    Person  *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

   // 调用对象方法(本质:让对象发送消息)
    //  OC:  p = [p init];
    //runtime
    objc_msgSend(p, sel_registerName("init"));

    //OC:
    //    [p hungry];
    //    [p run:20];
    //runtime
    objc_msgSend(p, @selector(hungry));
    objc_msgSend(p, @selector(run:),20);
}

Person.m文件
-(void)run:(NSUInteger)m{
    NSLog(@"他跑了%zdM",m);
}
-(void)hungry{
NSLog(@"他饿了");
}

打印如下:


打印结果

三.常用开发场景

3.1 UITextField占位文字的颜色以及字体大小
    [_textField setValue:kFont(14) forKeyPath:@"_placeholderLabel.font"];
    [_textField setValue:kThemeColor forKeyPath:@"_placeholderLabel.textColor"];

3.2 给分类动态添加属性

注意:我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。
原理:给一个类声明属性,其本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

需求:给系统 NSObject 类动态添加属性 name 字符串。

#import "NSObject+Person.h"
#import <objc/runtime.h>
static char const * const kName = "kName";
@implementation NSObject (Person)
- (void)setName:(NSString *)name{
    /*
     objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
     
     <#id  _Nonnull object#>: 给哪个对象添加属性
     <#const void * _Nonnull key#>:属性名称
     <#id  _Nullable value#>:属性值
     <#objc_AssociationPolicy policy#>:保存策略
     **/
    objc_setAssociatedObject(self, kName, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
}
- (NSString *)name{
    //objc_getAssociatedObject:获取某个对象的值
    return objc_getAssociatedObject(self, kName);
}
/*************调用******************/
 NSObject *obj =[NSObject new];
    obj.name = @"flowerflower";
    NSLog(@"我是分类obj中的%@",obj.name);
/*************打印输出******************/
我是分类obj中的flowerflower

小结:
属性赋值的本质:就是让属性与一个对象产生关联,所以要给NSObject的分类的name属性赋值就是让name和NSObject产生关联,而runtime可以做到这一点。


四.面试须知

4.1 动态添加方法(开发中几乎不用,但面试须知)

需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。
案例代码

/*************UserModel.h******************/
- (void)eat;
- (void)run:(NSNumber *)m;

/*************UserModel.m******************/
@implementation UserModel
// 没有返回值,1个参数
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == NSSelectorFromString(@"run:")) {
        // 动态添加run方法
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法,即添加方法的方法编号
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
        // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
- (void)eat{
    
    NSLog(@"我吃了");
}
@end

/*************ViewController.m******************/
- (void)viewDidLoad {
    [super viewDidLoad];

    UserModel *user = [UserModel new];
    [user performSelector:@selector(run:) withObject:@121];
    [user eat];
    objc_msgSend(user,@selector(eat));
    objc_msgSend(user,@selector(run:),@1);
}
图片.png
4.2 使用runtime实现自动归档和解档(之前傻逼式的一个一个写,昨天看了hanK的公开课,顿时打开眼界,原来还能这么搞,大写的服)

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。


图片.png

假设这里有100个属性,那是不是我们也只能把100个属性都给写一遍。看着HanK老师的公开课之后,我会觉得自己挺傻乎乎的,不管模型中再多属性,轻轻松松50行搞定。

首先要看个小示例

/*************UserModel.h******************/
@interface UserModel : NSObject<NSCoding>

@property(nonatomic,copy)NSString *name;

@property(nonatomic,assign)NSInteger age;

/*************UserModel.m******************/
#import "UserModel.h"
#import <objc/runtime.h>
@interface UserModel()
@property (nonatomic,copy) NSString *userId;
@property (nonatomic,copy) NSString *phone;             //注册手机号
@end
@implementation UserModel
@end
/*************调用******************/
    unsigned int count = 0; //成员变量的个数
    
  Ivar *ivarList =  class_copyIvarList(UserModel.class, &count);
    
    Ivar ivar1 = ivarList[0];
    
    NSLog(@"%s",ivar_getName(ivar1)); //_name
    Ivar ivar2 = ivarList[1];
    NSLog(@"%s",ivar_getName(ivar2));//_age
    Ivar ivar3 = ivarList[2];
//  打印UserModel里面的成员属性的个数
    NSLog(@"%d",count);

图片.png

也就是说即使在.m声明的私有属性,也可以使用runtime获取到成员变量的属性

回归正题(使用runtime实现归档和接档,不管属性是10个,20个,甚至上100个)

/*************UserModel.h******************/
@interface UserModel : NSObject<NSCoding>
//定义一堆属性  假设这里有22个属性    这里就不全部展示了。就简单写了几个
@property (nonatomic,copy) NSString *userId;         //用户Id
@property (nonatomic,copy) NSString *phone;         //注册手机号
@property (nonatomic,copy) NSString *nickName;     //昵称
@property (nonatomic,copy) NSString *loginPSW;    //登陆的md5密码
@end

/*************UserModel.m******************/
#import "UserModel.h"
#import <objc/runtime.h>
@interface UserModel()     
@end
@implementation UserModel

- (void)encodeWithCoder:(NSCoder *)aCoder{
    /* 获取类中的ivar列表
     count:  count为ivar总数
     class_copyIvarList:  获取类的全部属性
     **/
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self.class, &count);
    for (int i  = 0; i< count; i++) {
        //取出Ivar
        Ivar ivar = ivarList[i];
        //属性名称  ivar_getName:获取类实例成员变量,只能取到本类的,父类的访问不到
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        //归档  通过KVC取的 就没有int类型了
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
    //但凡在C语言里面 看到New Creat Copy 都需要释放
    free(ivarList); //释放
}
- (instancetype)initWithCoder:(NSCoder *)coder{

    if ( self = [super init]) {
        unsigned int count = 0;
        Ivar *ivarList = class_copyIvarList(self.class, &count);
        
        for (int i  = 0; i< count; i++) {
            //取出Ivar
            Ivar ivar = ivarList[i];
            //属性名称
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            //解档    设置到成员变量上
            [self setValue: [coder decodeObjectForKey:key] forKey:key];
        }
        free(ivarList); //释放
    }
    return self;
}


/*************ViewController******************/
//宏
#define UserDataFilePath ([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"user.data"])

- (void)viewDidLoad {
    [super viewDidLoad];
    unsigned int count = 0; //成员变量的个数
  Ivar *ivarList =  class_copyIvarList(UserModel.class, &count);
    Ivar ivar1 = ivarList[0];
    NSLog(@"%s",ivar_getName(ivar1)); //_userId
    Ivar ivar2 = ivarList[1];
    NSLog(@"%s",ivar_getName(ivar2));//_phone
    Ivar ivar3 = ivarList[2];
//  打印UserModel里面的成员属性的个数
    NSLog(@"%d",count); //打印结果:22
}
- (IBAction)save:(id)sender{
     UserModel *model = [UserModel new];
        model.nickName = @"flowerflower";
        model.phone = @"1234567890";
        //归档
        [NSKeyedArchiver archiveRootObject:model toFile:UserDataFilePath];
}

- (IBAction)read:(id)sender{
  UserModel *model = [NSKeyedUnarchiver unarchiveObjectWithFile:UserDataFilePath];
  NSLog(@"%@----%@",model.nickName,model.phone);
}

图片.png
4.3 方法交换(俗称黑魔法,也有说法叫方法欺骗)
  • 系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能
    • 简单粗暴方式: 继承系统的类,重写方法.
    • 高端大气上档次的runtime:使用runtime,交换方法.
简单粗暴方式:
//Image继承于UIImage
//重写系统方法
+(UIImage *)imageNamed:(NSString *)name{

    UIImage *image =[super imageNamed:name];

    if (image) {
        NSLog(@"加载成功");
    }else{
        NSLog(@"加载失败");
    }
    return image;
}

//ViewController中导入头文件直接调用
//有图片则加载成功,无图片则加载失败。
UIImage *img = [Image imageNamed:@"brand_foreclock"];
高端大气上档次的runtime交换方法
UIImage+yhp_Image.m文件
#import "UIImage+yhp_Image.h"
#import <objc/message.h>

@implementation UIImage (yhp_Image)

// 把类加载进内存的时候调用,只会调用一次
+(void)load{
    //交换方法
    Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method yhp_imageNameMethod = class_getClassMethod(self, @selector(yhp_imageNamed:));

    //交换方法地址,相当于交换实现方法
    method_exchangeImplementations(imageNameMethod, yhp_imageNameMethod);

}
//不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super. 
//注意:不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉
+(UIImage *)yhp_imageNamed:(NSString *)name{
    // 图片
    UIImage *image = [UIImage yhp_imageNamed:name];

    if (image) {
        NSLog(@"加载成功");
    } else {
        NSLog(@"加载失败");
    }

    return image;
}

//ViewController不需要头文件
 UIImage *image = [UIImage imageNamed:@"1.peng"];

五.利用runtime进行实用性封装

5.1 UIAlertView的封装

正常写法

/**
使用步骤:
1.初始化
2.设置代理
3.实现代理方法
**/
@interface ViewController : UIViewController<UIAlertViewDelegate>  
  
@end  
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"AlertViewTest"  
                                                   message:@"message"  
                                                  delegate:self  
                                         cancelButtonTitle:@"取消"  
                                         otherButtonTitles:@"确定",nil];  
    [alert show];  
}
#pragma marks -- UIAlertViewDelegate --  
//根据被点击按钮的索引处理点击事件  
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex  {  
    NSLog(@"clickButtonAtIndex:%d",buttonIndex);  
}  

使用runtime对UIAlertView进行封装


/*************UIAlertView+HHAddition.h******************/
#import <UIKit/UIKit.h>
typedef void(^AlertCallBack)(NSInteger buttonIndex);

@interface UIAlertView (HHAddition)

-(void)showAlertWithHandler:(AlertCallBack)callback;

@end

/*************UIAlertView+HHAddition.m******************/
#import "UIAlertView+HHAddition.h"
#import <objc/runtime.h>

static char kUIAlertViewBlockAddress;

@implementation UIAlertView (HHAddition)

-(void)showAlertWithHandler:(void(^)(NSInteger))callback{
    
    self.delegate = self;
    //为某个对象设置关联对象的值
    //第一个参数是主对象,第二个参数是键,第三个参数是关联的对象,第四个参数是存储策略:是枚举,定义了内存管理语义
    objc_setAssociatedObject(self, &kUIAlertViewBlockAddress, callback, OBJC_ASSOCIATION_COPY);
    [self show];   
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex{
    //根据给定的键从某对象中获取相应的关联对象值
    AlertCallBack alertCallBack = objc_getAssociatedObject(self, &kUIAlertViewBlockAddress);
    if (alertCallBack) {
        alertCallBack(buttonIndex);
        objc_setAssociatedObject(self, &kUIAlertViewBlockAddress, nil, OBJC_ASSOCIATION_COPY);
    }
}
@end

/*************调用******************/
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"是否联系客服"  message:message delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"确定", nil];
    [alertView showAlertWithHandler:^(NSInteger buttonIndex) {
    NSLog(@"clickButtonAtIndex:%d",buttonIndex);  
    }];
5.2 Method Swizzling方法交换封装
/*************NSObject+Swizzling.h******************/
#import <objc/runtime.h>
@interface NSObject (Swizzling) 

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                         bySwizzledSelector:(SEL)swizzledSelector;
@end

/*************NSObject+Swizzling.m******************/
#import "NSObject+Swizzling.h"
@implementation NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{
    Class class = [self class];
    //原有方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    //替换原有方法的新方法
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
/**
先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况.
class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。
也就是说如果class_addMethod返回YES,说明子类中没有方originalSelector,
通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。
*/
    BOOL didAddMethod = class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
        class_replaceMethod(class,swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end

5.3 自动归档和解档定义成宏

两句代码搞定

#import "UserModel.h"
#import <objc/runtime.h>

#define encodeRuntime(className) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\

#define initCoderRuntime(className) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\

@implementation UserModel

- (void)encodeWithCoder:(NSCoder *)encoder{

    encodeRuntime(UserModel)
}

- (id)initWithCoder:(NSCoder *)decoder{

    initCoderRuntime(UserModel)
}
@end

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

推荐阅读更多精彩内容

  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,700评论 7 64
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 一切都来的不可思议~ 突然间身边多了一个小东西,她天天哼哼唧唧哭闹找你的奶头,吮吸完会露出满足的微笑,然后还会“m...
    缘来袅袅兮兮阅读 300评论 0 0
  • 不知从何时起,我的信息获取来源主要就是微信公众号。记得大学那阵子,微信上还没多少联系人,微信也就是个阅读软件。 所...
    万旗k阅读 456评论 0 0
  • 作为理财小白,通过小学生就应该看而且看得懂的《小狗钱钱》入门,这几个关键词很有用: 梦想储蓄罐 钱是手段而不是目的...
    落落的萝卜糕阅读 301评论 0 2