前言

runtime的资料网上有很多了,部分有些晦涩难懂,我通过自己的学习方法总结一遍,主要讲一些常用的方法功能,以实用为主,我觉得用到印象才是最深刻的。
另外runtime的知识还有很多,想要了解更多可以一些翻译的官方文档(有点枯燥)

什么是runtime?

runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h> 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。
比如我们创建了一个对象 [[NSObject alloc]init],最终被转换为几万行代码,截取最关键的一句可以看到底层是通过runtime创建的对象,OC中一切都被设计成了对象,我们都知道一个类被初始化成一个实例,这个实例是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。如下:

//类在runtime中的表示
struct objc_class {
Class isa;//指针,顾名思义,表示是一个什么,
//实例的isa指向类对象,类对象的isa指向元类 #if !__OBJC2__
Class super_class; //指向父类
const char *name; //类名
long version;
long info;
long instance_size
struct objc_ivar_list *ivars //成员变量列表
struct objc_method_list **methodLists; //方法列表
struct objc_cache *cache;//缓存
//一种优化,调用过的方法存入缓存列表,下次调用先找缓存
struct objc_protocol_list *protocols //协议列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
.cpp 文件
删除掉一些强制转换语句,可以看到调用方法本质就是发消息,[[NSObject alloc]init]语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。利用这个功能我们可以探究底层,比如block的实现原理。
需要注意的是,使用objc_msgSend() sel_registerName()方法需要导入头文件<objc/message.h>

 

消息机制

另外利用runtime 可以做一些OC不容易实现的功能

动态交换两个方法的实现(特别是交换系统自带的方法)
动态添加对象的成员变量和成员方法
获得某个类的所有成员方法、所有成员变量

如何应用运行时?
1.将某些OC代码转为运行时代码,探究底层,比如block的实现原理;
2.拦截系统自带的方法调用(Swizzle 黑魔法),比如拦截imageNamed:、viewDidLoad、alloc;
3.实现分类也可以增加属性;
4.实现NSCoding的自动归档和自动解档;
5.实现字典和模型的自动转换。

方法简单的交换

创建一个Person类,类中实现以下两个类方法,并在.h 文件中声明

+ (void)eat{
NSLog(@"吃");
} + (void)study {
NSLog(@"学习");
}

控制器中调用,则先打印吃,后打印学习

[Person run];
[Person study];

下面通过runtime 实现方法交换,类方法用class_getClassMethod ,对象方法用class_getInstanceMethod

// 获取两个类的类方法
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
// 交换后,先打印学习,再打印吃!
[Person run];
[Person study];

可以看到输出的是2个交换过的信息

获取列表

有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的Key和模型对象的属性名字不匹配)。
我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)。

 unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=; 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)));
} //获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
} //获取协议列表
__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]);
}

在Xcode上看下输出吧,需要给你当前的类写几个属性,成员变量,方法和协议,不然获取的列表是没有东西的。

方法调用

让我们看一下方法调用在运行时的过程吧

如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。

  1. 首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
  2. 如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
  3. 如果没找到,去父类指针所指向的对象中执行1,2.
  4. 以此类推,如果一直到根类还没找到,转向拦截调用。
  5. 如果没有重写拦截调用的方法,程序报错。

以上的过程给我带来的启发:

  • 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
  • 如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程。

拦截调用

在方法调用中说到了,如果没有找到方法就会转向拦截调用。
那么什么是拦截调用呢。
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
  • 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
  • 第二个方法和第一个方法相似,只不过处理的是实例方法。
  • 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
  • 第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。

动态添加方法

重写了拦截调用的方法并且返回了YES,我们要怎么处理呢?
有一个办法是根据传进来的SEL类型的selector动态添加一个方法。

首先从外部隐式调用一个不存在的方法:

/隐式调用方法
[target performSelector:@selector(resolveAdd:) withObject:@"111"];

然后,在target对象内部重写拦截调用的方法,动态添加方法。

void runAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"add C IMP ", string);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{ //给本类动态添加一个方法
if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
}
return YES;
}

其中class_addMethod的四个参数分别是:

Class cls 给哪个类添加方法,本例中是self
SEL name 添加的方法,本例中是重写的拦截调用传进来的selector。
IMP imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
"v@:*" 方法的签名,代表有一个参数的方法。

  

在分类中设置属性,给任何一个对象设置属性

众所周知,分类中是无法设置属性的,如果在分类的声明中写@property 只能为其生成get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash,有人会想到使用全局变量呢?比如这样:

int _age;

- (int )age {
return _age;
} - (void)setAge:(int)age {
_age = age;
}

但是全局变量程序整个执行过程中内存中只有一份,我们创建多个对象修改其属性值都会修改同一个变量,这样就无法保证像属性一样每个对象都拥有其自己的属性值。这时我们就需要借助runtime为分类增加属性的功能了。

set方法,将值value 跟对象object 关联起来(将值value 存储到对象object 中)
参数 object:给哪个对象设置属性
参数 key: 一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节
参数 value: 给属性设置的值
参数 policy:存储策略 (assign 、copy 、 retain就是strong)

关联策略有以下几种:

enum {
OBJC_ASSOCIATION_ASSIGN = ,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = ,
OBJC_ASSOCIATION_COPY_NONATOMIC = ,
OBJC_ASSOCIATION_RETAIN = ,
OBJC_ASSOCIATION_COPY =
};

关联步骤:先在.h 中@property 声明出get 和 set 方法,方便点语法调用;

@property(nonatomic,copy)NSString *name;

在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值

char nameKey;

- (void)setName:(NSString *)name {
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
} - (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
}
 
 
 
 

iOS开发笔记之Runtime实用总结的更多相关文章

  1. 菜鸟手下的iOS开发笔记(swift)

    在阳春4月的一天晨会上,有一个老板和蔼的对他的一个菜鸟手下说:“你既然会Android,那你能不能开发iOS?” 不是说好的要外包的吗?内心跌宕,但是表面淡定的菜鸟手下弱弱的回道:“可以试试”. 第二 ...

  2. iOS开发之使用Runtime给Model类赋值

    本篇博客算是给网络缓存打个基础吧,本篇博客先给出简单也是最容易使用的把字典转成实体类的方法,然后在给出如何使用Runtime来给Model实体类赋值.本篇博客会介绍一部分,主要是字典的key与Mode ...

  3. iOS开发笔记7:Text、UI交互细节、两个动画效果等

    Text主要总结UILabel.UITextField.UITextView.UIMenuController以及UIWebView/WKWebView相关的一些问题. UI细节主要总结界面交互开发中 ...

  4. iOS开发笔记-两种单例模式的写法

    iOS开发笔记-两种单例模式的写法   单例模式是开发中最常用的写法之一,iOS的单例模式有两种官方写法,如下: 不使用GCD #import "ServiceManager.h" ...

  5. iOS开发笔记--什么时候调用layoutSubviews

    iOS开发笔记--什么时候调用layoutSubviews 分类: iOS2014-04-22 16:15 610人阅读 评论(0) 收藏 举报 今天在写程序时候遇见layoutSubviews触发时 ...

  6. IOS开发笔记(4)数据离线缓存与读取

    IOS开发笔记(4)数据离线缓存与读取 分类: IOS学习2012-12-06 16:30 7082人阅读 评论(0) 收藏 举报 iosiOSIOS 方法一:一般将服务器第一次返回的数据保存在沙盒里 ...

  7. IOS开发笔记 IOS如何访问通讯录

    IOS开发笔记  IOS如何访问通讯录 其实我是反对这类的需求,你说你读我的隐私,我肯定不愿意的. 幸好ios6.0 以后给了个权限控制.当打开app的时候你可以选择拒绝. 实现方法: [plain] ...

  8. 【Swift】iOS开发笔记(二)

    前言 这个系列主要是一些开发中遇到的坑记录分享,有助于初学者跨过这些坑,攒够 7 条发一篇. 声明  欢迎转载,但请保留文章原始出处:)  博客园:http://www.cnblogs.com 农民伯 ...

  9. iOS开发小技巧 - runtime适配字体

    iOS开发小技巧 - runtime适配字体 版权声明:本文为博主原创文章,未经博主允许不得转载,有问题可联系博主Email: liuyongjiesail@icloud.com 一个iOS开发项目无 ...

随机推荐

  1. oracle 邮件发送

    CREATE OR REPLACE PROCEDURE PRC_sendmail(p_receiver VARCHAR2, -- 邮件接收人                               ...

  2. 用gulp建立自动工具,完成软件的编译、测试、打包和发布流程

    gulp以task的形式组织任务. 在每一个任务中,从gulp.src()指定文件源头开始,经过一系列pipe管道处理, 最后结果保存到gulp.dest指定的目录中,(或输出到stream) 任务的 ...

  3. 诺基亚N900使用技巧

    一直都对 Linux 的掌上终端挺感冒的,最近从闲鱼上入后一台欧版的,开机进行评测和使用.以下经验仅供新手参考. 选择 N900 是有原因的,首先 N900 相对来说比较小巧(作为 MID,对比起手机 ...

  4. Oracle的登录方式

    1.本地登录 (1).用户名,密码登录: user:xxx password:xxx (2).指定用户名,密码登录: c:\>sqlplus   sys/qac123QAC as sysdba; ...

  5. clientWidth、offsetWidth和scrollWidth的区分

    scrollWidth:对象的实际内容的宽度,不包边线宽度,会随对象中内容超过可视区后而变大. clientWidth:对象内容的可视区的宽度,不包滚动条等边线,会随对象显示大小的变化而改变. off ...

  6. UITableView的创建及其一些常用方法

    UITableView,它的数据源继承于UITableViewDataSource,它的委托UITableViewDelegate. 一.UITableView的创建 1.代码方式: UITableV ...

  7. Sql总结之Sql--常用函数

    控制流函数 IFNULL(expr1,expr2) 如果expr1不是NULL,IFNULL()返回expr1,否则它返回expr2.IFNULL()返回一个数字或字符串值,取决于它被使用的上下文环境 ...

  8. jquery下拉菜单打开的同时,同行右边的图标变化

    1.用bootstrap的折叠面板时,右侧的三角图标随菜单下拉而旋转90°: html代码 <a class="advanced-option" data-toggle=&q ...

  9. 基于log4net的支持动态文件名、按日期和大小自动分割文件的日志组件

    最近处理一个日志功能,用log4net的配置不能完全满足要求,所以在其基础上简单封装了一下,支持以下功能: 1 零配置 内置默认配置,引用dll后不需要添加或修改任何配置文件也可以使用 2 动态指定文 ...

  10. 有关emoji表情以及utf-16编码

        昨日IOS组的同事遇到一个棘手的问题:当输入框内含有emoji表情时,如何获取文本框内的字符数(一个emoji表情算一个字符).       先从我最近接触的JAVA说起,JAVA中,在使用S ...