iOS 底层 - 名词解析
目录
- 前言
- 名词解析
- OC消息传递和转发机制
- Runtime
- runtime动态创建类
- Runloop
- Method Swizzling黑魔法
- 自己动手写一个框架
- Category实现原理 和 Protocol
- 反射机制
- Json到Model的转化
- 快速归档
- 访问私有变量
- 。。。
名词解析
1. 源码 <objc/objc.h>
/<objc-private.h>
中的名词
① 实例对象 ===> id 和结构体 objc_object
/// A pointer to an instance of a class.
typedef struct objc_object *id;
源码指出,id 是指向 objc_object 的指针。而注释告诉我们,id 就是一个指向某个类的实例对象的指针。那 objc_object 就是某个类的实例对象,如下:
// <objc/objc.h> 源码中
// Objective-C 2.0 已废弃
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
// <objc-private.h> 源码中
// Objective-C 2.0 支持的定义
struct objc_object {
private:
isa_t isa;
public:
Class ISA();
Class getIsa();
void initIsa(Class cls /*nonpointer=false*/);
void initClassIsa(Class cls /*nonpointer=maybe*/);
void initProtocolIsa(Class cls /*nonpointer=maybe*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
...
}
objc_object 表示一个类的实例。objc_object 里面有一个 isa_t 类型的 isa 指针,这个指针指向了这个对象的类对象
② 类 ==== Class 和结构体 objc_class
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
从源码中可以看出,Class 是一个指向 objc_class 结构体的指针.注释告诉我们,Class 表示一个 Objective-C 类。那么 objc_class 又是什么?
// <objc/runtime.h> 源码中
// Objective-C 2.0 已经废弃
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
....
#endif
} OBJC2_UNAVAILABLE;
// <objc-runtime-new.h> 源码中
// Objective-C 2.0 支持
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
// 这个结构体里有所有的变量列表,方法列表等
}
...
}
我们可以从源码中看出来, objc_class 结构体继承自 objc_object. 说明 objc_class 也是个对象。那我们就很简单得推断出 objc_class 也有个 isa 指针。那么这个 isa 指针又指向的是什么呢?
其实这里仅仅从源码来看,我们并不知道这里的isa指向了哪里,反正不是父类,因为父类下面还有个 superclass 指向了它的父类
那 isa 指向了哪里呢? 只能通过网上的解答来解释了:
isa指向的是它的 metaclass(元类) , 元类也是 objc_class 的另一种对象
关于类和对象
我们常说的对象,一般指的是 实例对象。 但其实对象不仅仅是实例对象,还有其他的, 比如类对象. 我们上面有说到,objc_class 和 objc_object 几乎一致,那我们从 runtime 的源码下继续深入研究中看到
// <objc-runtime-new.h> 源码
struct objc_class : objc_object {
...
}
从这里看出,objc_class 是继承了 objc_object 的结构体。这里的源码我们其实可以先得出一个结论:
类也是一个对象
关于 metaclass 元类
在国外大牛的文章:What is a meta-class in Objective-C? 中对 metaclass 的解释总结过来有三个要点:
- metaclass 元类也是一个对象,是一个 obj_class 的一种,即是类对象的一种。这意味着, 你也可以对这些元类作为对象来发送消息。也因此每个Class都有其 metaclass 原类
- 所有的 metaclass 元类都使用同一个基础类的 元类。这意味着 几乎所有的metaclass 元类都是 ‘NSObject的元类’ 的派生类。
- 几乎所有的 metaclass元类 上的 isa 指针 指向的都是 ‘NSObject的元类’.元类也有个 isa 指针指向 root meteClass 根元,根元的 isa 指针指向自己
最后来一张经典图片收尾
③ 方法选择器 ==== SEL 和结构体 objc_selector
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
SEL 是 objc_selector 结构体的指针,表示了一个方法的选择器。结构体的内容在 runtime.h和obj.h 的源码中无法找到,所以我们无法得知 objc_selector 到底是什么。不过很多 Blog 和书籍中指出: " SEL 其实也是其中一种数据类型,用来定义类方法,是类成员方法的指针,本质上其实只是一串字符串。”(其实可以看做是类方法的identifier),接下来验证这种说法是否正确
// 首先,我们先定义一个 SEL, 定义 SEL 可以有多种方式
SEL aSelector = @selector(SEL selector);
SEL bSelector = SEL sel_registerName(const char *name);
// (NSSelectorFromString 方法就是通过 sel_registerName 方法来注册的 SEL)
SEL cSelector = NSSelectorFromString(NSString *cSelectorName);
// 其实从定义我们就可以看出来, SEL 和 字符串 可能存在某种转化。
// 除此之外, 我们输出下这个 SEL 发现:
// 尽管报了 warning ,但是你还是可以看到输出的就是个字符串,而没有报错
NSLog(@"%s",aSelector);
// 另外,我们还可以通过另一个方法来看看 SEL 其实也是个类似Key的东西
// 举例, dog 和 cat 是两个不同的实例对象,都有一个名为 description 的方法
SEL selector1 = @selector(description);
[dog performSelector:selector1]; // 调用 dog 的 description 方法
[cat performSelector:selector1]; // 调用 cat 的 description 方法
// 可以看出虽然 SEL 虽然相同, 但是调用的方法是不同的。
那么其实 我们是否可以大胆得得出一个结论:SEL 其实就只是 一个方法名 或者说 标识. 这一点其实从
后面的 objc_method 结构体中 SEL method_name
命名可以看出。
④ 方法实现 ==== IMP
#if !OBJC_OLD_DISPATCH_PROTOTYPES
// 指向C的函数
typedef void (*IMP)(void /* id, SEL, ... */ ); // 无返回值
#else
// OC的指向方法的指针
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); // 有返回值
#endif
IMP(Implementation)是一个指向 方法实现的地址 的指针,参数都包含 id 和 SEL 类型。通过 id (实例对象)里的 SEL (方法名) 获取到唯一的实现方法。
- 获取 IMP 的方式
IMP imp = class_getMethodImplementation(Class cls, SEL name)
IMP imp = method_getImplementation(Method _Nonnull m)
- 调用方式
// 无参无返回值的 IMP 调用。 (其实走的是C语言的指向函数)
imp();
// 对于有参数有返回值的 IMP 的调用比较麻烦。
// 这里给出一种常规操作: 强制类型转换 (例子中返回值和参数的类型都是 int)
int (*imp)(id, SEL, int) ;
imp = (int (*)(id, SEL, int))method_getImplementation(method); // 强制类型转换
Object *obj = [[Object alloc] init];
NSLog(@"%d", imp(obj, @selector(method_name:), 3));
其实,直接调用方法的 IMP指针效率比调用方法本身更高。但是也不要盲目得去用IMP来提高速率,毕竟可读性肯定没有那么高了。
⑤ 布尔值 ==== bool 和 BOOL 和 Boolean、true 和 false 、 YES 和 NO
- 关于 bool 和 true/false
// bool 的定义 在 <stdbool.h> 中 原代码较长,总结下来是这样的
// 如果是 C 语法
#define bool _Bool // 因为 C 语言 不具有 bool 值的,因此 C99 用 _Bool 来兼容 C++
#define true 1
#define false 0
// 如果是 C++ 语法
#define bool bool
#define false false
#define true true
- 这里注意, bool 不管在 C 还是在 C++, 都是只有 1个字节 。
- bool 值为 true/false, 对应的值只有 1/0 。
- 所有的 非0 数值, 强制转换成 bool 类型 都会转换成 1/true 。 如下:
NSLog(@"%d %d %d %d %d", (bool)1, (bool)-1, (bool)0, (bool)256, (bool)8);
//输出 1, 1, 0, 1, 1
- BOOL 和 YES/NO
// BOOL 的定义 在 <objc.h> 中
// OBJC_BOOL_IS_BOOL 的定义省略. 总结就如下
#if OBJC_BOOL_IS_BOOL // 当 64位iPhone 或 ARMv7K 时
typedef bool BOOL;
#else // 当是 32位iPhone 以及 非ARMv7K 时
typedef signed char BOOL;
#endif
// objc_bool 的 类型与 系统位数有关
#if __has_feature(objc_bool)
#define YES __objc_yes // __objc_yes = signed char 1 (32位) / true (64位)
#define NO __objc_no // __objc_no = signed char 0 (32位) / false (64位)
#else
#define YES ((BOOL)1)
#define NO ((BOOL)0)
#endif
- BOOL 不管是32位系统还是64位系统,都占了 1个字节
- 32 位系统 的类型是 signed char . 字符空间只有8位,超过8位的都只会取低8位的值。如 256 = 0b1 0000 0000 转换成 BOOL 会等于 0
- 所有的 非0 数值, 强制转换成 BOOL 类型,都不会转换成 1/true 。
NSLog(@"%d %d %d %d %d", (BOOL)1, (BOOL)-1, (BOOL)0, (BOOL)256, (BOOL)8); //输出 1 -1 0 0 8
注意: 这里不会转换成0/1 会导致一个问题。如果在条件判断语句中,使用 condition == YES,或 condition != YES这种写法,等式会不成立。比如:
BOOL condition = 123; BOOL condition = 123;
if (condition == YES) { if (condition) {
// 不会走这里 // 走这里, 正确
} else { ==用该方法改进==> } else {
// 走的是这里. 这是错误的
} }
- 总结来说
- 判断语句中不要用 == YES 的方式
- 尽量避免赋值给 BOOL 。 如果要赋值给 BOOL 也请注意取值范围 (因为超过8位会截取)
- Boolean
// Boolean 的定义在 <MacTypes.h> 中
typedef unsigned char Boolean;
Boolean 的注释中 有说到
Mac OS historic type, sizeof(Boolean)==1
说明Boolean只有一个字节
⑥ 区别 nil Nil NULL 和 NSNull
// 源码中的 nill Nil 在 <objc.h>
// Nil 定义
#ifndef Nil
# if __has_feature(cxx_nullptr)
# define Nil nullptr
# else
# define Nil __DARWIN_NULL
# endif
#endif
// nil 定义
#ifndef nil
# if __has_feature(cxx_nullptr)
# define nil nullptr
# else
# define nil __DARWIN_NULL
# endif
#endif
// NULL 在 C 语言中的定义
#undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif
// NSNull
@interface NSNull : NSObject <NSCopying, NSSecureCoding>
+ (NSNull *)null;
@end
-
适用场景:
- Nil 代表 类对象 指向一个 空指针
如: Class a = Nil
- nil 代表 OC对象 指向一个 空指针
如: NSString *a = nil
- NULL 一般用来使 基础类型 指向 空指针
如: int *pointerInt = NULL
- NSNull 表示一个 空的占位对象
//这里的 [NSNull null] 不能用 nil 替换。[NSNull null] 表示空对象 NSArray *arr = [NSArray arrayWithObjects:@"1",[NSNull null],@"2", nil]; NSLog(@"%@",arr); // 输出 (1,"<null>",2)
- Nil 代表 类对象 指向一个 空指针
-
注意事项:
-
对象很可能会被销毁后不置nil。(这个其实也是很常见。比如你命名一个对象时用了 assign 而不是 weak 时。对象被销毁后不会置nil, 所以指针指向的地址以及不存在,成为野指针)
这里有一点不置nil的对象 发送消息时会崩溃。 但是 置nil的对象 发送消息消息不会崩溃
所以,为了安全起见, 在调用方法之前判断一下是否为空是很有必要的NSString *str = nil; if (str != nil) { // do Something }
-
尽量不要用 NULL 去初始化 OC对象
-
iOS开发中 在向服务请求数据或者请求网页,对获得的数据解析时,如果遇到
(null) 或 <null>
时。因为这个数据不是 nil 而是 NSNull对象 ,所以在这个解析后的对象发送消息 会崩溃。崩溃消息:unrecognized selector sent to instance
解决的办法会在后面说到
-
2. 源码<objc/runtime.h>
/<objc-runtime-new.h>
中的名词
① 方法 ==== Method 和结构体 method_t / objc_method
// <runtime.h> 源码中 (Objective-C 2.0 已废弃)
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;
已经在 Objective-C 2.0 中废弃的,我们就不看了。重点关注下面的源码
// <objc-runtime-new.h> 源码中
typedef struct method_t *Method;
struct method_t {
SEL name; // 方法名
const char *types; // 返回值和参数
MethodListIMP imp; // 指向“方法的实现”的指针 注: using MethodListIMP = IMP;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
}; // 根据name的地址对方法进行排序的函数
};
这里我们可以知道 一个 Method 需要具备以下几个条件: 方法名、参数、返回值、实现方法
不同的类可以有相同的 Method 方法。查找 Method 的时候会在类的 Method 链表中进行查找。
定位到一个 Method 一般有下面两种:
// 获取实例方法
Method *method = class_getInstanceMethod([ClassName class], @selector(method_name))
// 获取类方法
Method *method = class_getClassMethod([ClassName class], @selector(method_name))
常用的方法
// 给类添加一个方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
// 替换 cls 类中的 name 方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 交换 m1, m2 方法
method_exchangeImplementations(Method m1, Method m2)
// 设置 指向方法的实现 指针。 返回 原来的指向方法实现的指针
IMP method_setImplementation(Method method, IMP imp)
② 成员变量 ==== Ivar 和结构体 ivar_t / objc_ivar
// <runtime.h> 源码中 (Objective-C 2.0 已废弃)
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name; //实例变量名
char *ivar_type; //实例变量类型类型
int ivar_offset; //基地址偏移字节
int space;
}
以上源码已经在Objective-C 2.0中被废弃
// <objc-private.h>
typedef struct ivar_t *Ivar;
// <objc-runtime-new.h> 源码中
struct ivar_t {
int32_t *offset; // 基地址 偏移字节
const char *name; // 实例变量名
const char *type; // 实例变量类型
uint32_t alignment_raw; //对齐
uint32_t size; //大小
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};
Ivar 是指向 对象的实例变量。Ivar 是指向 ivar_t 结构体 的指针,从结构体看出,Ivar 包括类型和名字等。
- 常用的方法:
// 1. 获取 Ivar 实例变量列表
unsigned int count;
Ivar *ivarList = class_copyIvarList([Class class], &count);
// 这里如果想要知道每一个实例变量。用个 for循环 就可以了
for (int i = 0; i < count; i ++) {
Ivar ivar = ivarList[i];
}
free(ivarList); // 结束后要释放
// 2. 获取 Ivar 实例变量
Ivar ivar = class_getInstanceVariable([Class class], ivar_name);
// 3. 获取 结构体对应的值
ivar_getName(Ivar ivar) // 获取 实例变量名
ivar_getTypeEncoding(Ivar ivar); // 获取 实例类型
ivar_getOffset(Ivar ivar); // 获取 实例偏移值
// 4.获取/设置 变量
object_setIvar(id obj, Ivar ivar, id value) // 设置某个对象的 实例变量的值
object_getIvar(id obj, Ivar ivar) // 获取某个对象的 实力变量的值
// 5.变量的添加 (无法在运行时添加)
class_addIvar(Class cls, const char *name, size_t size,alignment, const char *types)
这里有个注意点,很多人发现变量添加 会失败。因为 无法通过运行时添加ivar 或者说动态添加类的时,已经添加完成的类中,无法新增 Ivar
所以注释中有说 class_addIvar 需要 objc_allocateClassPair 和 objc_registerClassPair 之间。
- 这里有两篇文章 对 Ivar 的内存结构和 Ivar 的偏移量有详细的介绍:
③ 属性 objc_property_t 和结构体 property_t / objc_property
// <runtime.h> 源码中 (Objective-C 2.0 已废弃)
typedef struct objc_property *objc_property_t;
// <objc-private.h> 源码中的定义
typedef struct property_t *objc_property_t;
// <objc-runtime-new.h> 源码:
struct property_t {
const char *name; // 属性名
const char *attributes; // 属性的特性
};
- 常用方法:
// 1. 获取 属性 的列表
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([Class class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = proList[i];
}
free(propertyList);
// 2. 根据属性名 获取 objc_property_t
class_getProperty(Class cls, const char* name)
// 3. 获取结构体的内容
property_getName(objc_property_t property) // 获取属性名
property_getAttributes(objc_property_t property) // 获取属性的特性
// 4. 对属性进行修改或新增
class_replaceProperty(Class cls,
const char* name,
const objc_property_attribute_t* attributes,
unsigned int attributeCount) // 替换属性
class_addProperty(Class cls,
const char * name,
const objc_property_attribute_t* attributes,
unsigned int attributeCount) // 新增属性(同Ivar 一致)
// 5.属性 的特性 相关
// 获取属性的特性列表
property_copyAttributeList(objc_property_t property, unsigned int *outCount)
// 拷贝属性的特性的值
property_copyAttributeValue(objc_property_t property, const char *attributeName)
注意,这里的 新增属性方法 在动态添加类结束后 也会失败
- 关于 attributes 特性 和 objc_property_attribute_t 结构体 :
我们可以从结构体中看出 attributes 只是个字符串,但是其实从 replace 和 add 方法中可以看出 其实是个 objc_property_attribute_t 结构体的数组
我们定义属性的时候用到了 类似 nonatomic、strong、assign 来形容属性,这些形容的内容都会 体现在 attributes 这个字符串中。/// 定义一个 attribute typedef struct { const char * _Nonnull name; // attribute 的名字 const char * _Nonnull value; // attribute 的值 (经常是空的) } objc_property_attribute_t; // 新增 property 的时候, attributes 可以这么定义, 如: objc_property_attribute_t attr0 = { "T", "@\"NSString\"" }; objc_property_attribute_t attr1 = { "&", "" }; objc_property_attribute_t attr2 = { "V", "_newProperty" }; objc_property_attribute_t attrs[] = { attr0, attr1, attr2};
这里列出这里的 Name 所对应的意思:
name | attribute |
---|---|
T | 类型,如@"NSString" |
& | retain |
W | weak |
N | nonatomic |
R | readonly |
C | copy |
G(name) | getter=(name) |
S(name) | setter=(name) |
D | @dynamic |
V | 实例变量Ivar |
P | 用于垃圾回收机制 |
关于 Ivar(实例) 和 Property(属性) 的区别
这里有人会奇怪,Ivar (实例) 和 Property (属性) 有什么区别?
当你定义个一个新的属性时,@property NSString* newProperty;
你会发现多了一个实例变量_newProperty
和两个方法 setter 和 getter 方法
所以我们 其实可以看做Property = Ivar + setter + getter
④ 分类 ==== Category 和结构体 category_t 和 objc_category
// <runtime.h> 源码中 (Objective-C 2.0 已废弃)
typedef struct objc_category *Category;
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
上面的源码已经在 Objective-C 2.0 中废弃.
// <objc-private.h> 和 <objc-runtime-new.h>源码中
typedef struct category_t *Category;
struct category_t {
const char *name; // 分类名
classref_t cls; // 类
struct method_list_t *instanceMethods; // 实例方法 列表
struct method_list_t *classMethods; // 类方法 列表
struct protocol_list_t *protocols; // 协议 列表
struct property_list_t *instanceProperties; // 实例属性 列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties; // 类属性 列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
关于 category 的 实现原理,我们会在后面讲到。
小结
1. 关于博客中的源码内容
在查看 <objc/runtime.h>
的过程中,我发现头文件源码中很多都有用到宏定义 OBJC2_UNAVAILABLE
以及用 #if !__OBJC2__ endif
来区别是否是 Objective-C 2.0+ , 那么也就是说我们看到的这些方法其实很多都已经不适用于当前的 Objective-C 版本,最新的很多都看不到了。不过 runtime 的源代码苹果已经开源。我们可以从官网下载到源码 ->源码下载地址。在源码中我们可以看到这些对象里面都有些什么。
注意: 正如上面所说很多 <runtime.h>
和 <objc.h>
的大部分都已经在 Objective-C 2.0 的时候过期了。所以我们 Objective-C 2.0 相对应的 <objc-runtime-new.h>
和<objc-private.h>
文件等。 objc4源码 中也可以看到
2. 关于使用哪种方式来定义属性的类型
我注意到在源码 <objc/runtime.h>
的注释中有写到 /* Use Class' instead of 'struct objc_class *' */
, 可以推断,苹果其实都不建议直接用结构体指针来命名该类型类型的值。