本文探索源码 objc4
( git开源地址 )c++
Category
?
Category
是 Object-C 2.0 以后添加的语言特性,Category
的主要做用是为已经存在的类添加方法.git
Extension
?
extension
一般咱们称之为扩展、延展、匿名分类。extension
看起来很像一个匿名的category
,可是extension
和category
几乎彻底是两个东西。和category
不一样的是extension
不但能够声明方法,还能够声明属性、成员变量。extension
通常用于声明私有方法,私有属性,私有成员变量。github
Category
和 Extension
有什么区别 ?让咱们从多个方面来回答这个问题。数组
Category
是一个 .h 和一个 .m.bash
Extension
是一个 .h . (固然,也能够在一个类的 .m 中伴生, 写法就是 @interface ***
/.../ @end
就很少说了.) 数据结构
Extension
是一个类的一部分, 它在 编译期 和头文件中@interface
, 实现文件中的@implement
一块儿造成一个完整类 ,Extension
伴随类的产生而产生 , 亦随之一块儿消亡.Extension
能够添加实例变量.Extension
通常用来隐藏类的私有信息 , 它没法直接为系统类提供扩展 , 但能够县建立系统类的子类 , 而后添加扩展.
举个🌰 app
p.objExtension = 28;
NSLog(@"%d",p.objExtension);
复制代码
如上使用, 发生崩溃.ide
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[LBPerson setObjExtension:]: unrecognized selector sent to instance 0x6000008ed4e0'
复制代码
将 NSObject
修改成 LBPerson
, 结果正常.函数
Category
是在运行时期决议. 这个时期对象的内存布局已经肯定了 , 若是再添加实例变量会破坏类的内部布局 , 这在编译型语言是灾难性的.布局
Category
能够给系统类添加分类.
Category
能够添加属性 , 可是并不会生成成员变量和对应的getter
以及setter
方法 .
一样 , 咱们实验一下 : 🌰
// LBPerson+Category.h
#import "LBPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LBPerson (Category)
@property (nonatomic,assign) int ageCategory;
@end
复制代码
使用:
LBPerson * p = [[LBPerson alloc] init];
p.ageCategory = 25; //分类添加属性错误
NSLog(@"%d",p.ageCategory);
复制代码
打印结果: 闪退.
-[LBPerson setAgeCategory:]: unrecognized selector sent to instance 0x6000021a72a0
固然, 咱们能够经过 runtime 设置关联对象 来解决这个问题 , 下面会仔细阐述.
Category
有什么用 ?Category
中framework
的私有方法公开 ( 在子类中经过引用 , 声明父类类别后 , 便可调用其未公开的私有方法)Tips:
请不要乱来:苹果官方会拒绝使用系统私有API的应用上架,所以即便学会了如何调用私有方法,在遇到调用其它类的私有方法时,要谨慎处理,尽可能用其它方法替代。
Category
底层原理解析说了这么多 , 终于要开始看看它究竟是个啥了. 打开终端/iterm2 , 编译转换成 c++.
clang -rewrite-objc LBPerson+Category.m
复制代码
找到编译后的文件, 打开, 搜索 _category_t
, 找到结构体定义.
struct _category_t {
const char *name; // 类名称
struct _class_t *cls; // 类对象, 在编译时没有值
const struct _method_list_t *instance_methods; //实例方法列表
const struct _method_list_t *class_methods; // 类方法列表
const struct _protocol_list_t *protocols; // 分类实现的协议列表
const struct _prop_list_t *properties; // 分类属性列表
};
复制代码
继续搜索 , 便可找到编译时 , 这个结构体存放的内容
static struct _category_t _OBJC_$_CATEGORY_LBPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"LBPerson",
0, // &OBJC_CLASS_$_LBPerson,
0,
0,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LBPerson_$_Category,
};
复制代码
搜索一下 _PROP_LIST_LBPerson_
会发现咱们定义的属性. 固然, 归纳一下: 定义的方法, 属性, 等都会在编译时存放在对应的字段中 , 编译后经过 section
段区分标识存放到生成的 Mach-O
可执行文件中.
同时 , 再往下看
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_LBPerson_$_Category,
};
复制代码
也就是说当在编译时 , 工程中全部的分类会被存储到 __DATA
数据段中的 __objc_catlist
这个 section
段中.
以上就是编译时分类所作的事情 , 分类结构体, 分类的方法 , 以及声明的属性 , 和存放 已经完成.
想要了解分类在运行时是如何加载和处理的. 咱们须要先知道一个概念.
什么是 dyld
?
dyld
是苹果的动态加载器 , 用来加载image
( 注意: 这里的image
不是指图片 , 而是Mach-O
格式的二进制文件 )当程序启动时 , 系统内核会首先加载
dyld
, 而dyld
会将咱们的 APP 所依赖的各类库加载到内存中 , 其中就包括libobjc
( OC 和 runtime ) , 这些工做是在 APP 的main
函数执行以前完成的.
_objc_init
是Object-C
-runtime
库的入口函数 , 在这里主要是读取Mach-O
文件 OC 对应的Segment section
, 并根据其中的数据代码信息 , 完成为 OC 的内存布局 , 以及初始化runtime
相关的数据结构.
那么也就是说 , 咱们要去 _objc_init
中一探究竟, 看看分类究竟是怎么加载 , 如何读取 , 又是如何释放的呢 ?
步骤 1️⃣: 直接打开 objc4
源码. 找到 _objc_init
入口函数 , 也能够经过添加符号断点找进来.
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
复制代码
前面一些初始化操做就很少说了 . 来看看这个 , 顺便提一句.
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
复制代码
map_images
: dyld
将 image
加载进内存时 , 会触发该函数.load_images
: dyld
初始化 image
会触发该方法. ( 咱们所熟知的 load
方法也是在此处调用 )unmap_image
: dyld
将 image
移除时 , 会触发该函数 .那么咱们就去研究研究 map_images
他加载时 , 咱们的分类到底处理了什么 .
步骤 2️⃣ : 点击进去 , 中间过渡方法我就省略了. 直接来到 _read_images
的方法实现.
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertLocked();
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
}
复制代码
因为方法过长 , 我就把一些读取其余的数据删除掉了, 只保留了读取分类部分 , 感兴趣的同窗能够去研究下其余数据的读取逻辑.
点击进去 _getObjc2ClassList
GETSECT(_getObjc2ClassList, classref_t, "__objc_classlist");
复制代码
这个就是咱们刚刚提到的 , 在编译时 , 分类被加载到这个 section
段中 , 咱们看到读取是这么读的 . 也顺便验证了咱们编译时的流程 .
那么也就是说 遍历全部的分类 , 而后一一添加设置 . 接下来 , 注意到在遍历中有一个方法
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
复制代码
看名字大概也猜获得, 往原类中添加 ( 注意是原类, 不是元类. 也就是原先的类 ).
步骤 3️⃣: 直接进去查看这个方法.
static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader) {
runtimeLock.assertLocked();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
复制代码
这里主要是 NXMapInsert(cats, cls, list);
这个方法, 其实简单说一下也就是把当前分类 , 和原类 创建起一个绑定关系 ( 其实就是经过数据结构映射起来 ). 为下面的事情作准备.
步骤 4️⃣ : 回到遍历方法中 , 创建起了绑定关系以后 , 下面还有一个方法 , 这个方法是遍历中最后的方法了 , 那么它必然是要将分类中的方法添加到原类中的 . 固然这是咱们的猜测 , 咱们带着这个目的去分析代码 .
remethodizeClass(cls->ISA());
复制代码
prepareMethodLists
中 , 把分类等要添加要元类的数据放进去.
步骤 5️⃣ : 重点 attachCategories
方法前半段咱们已经简单概述了一下 , 那么接下来来到
rw->methods.attachLists(mlists, mcount);
复制代码
点击进去 :
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
/** 拷贝的操做 void *memmove(void *__dst, const void *__src, size_t __len); */
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
复制代码
以上方法就是分类添加方法的核心逻辑 . 简单来讲就是经过 memmove
和 memcpy
这两个函数 , 以及本来类的方法列表和新分类的方法列表合并 , 添加到原方法的方法列表中.
并且要注意的是:
分类的方法是添加在方法列表数组前面的位置的 .
Mach-O
可执行文件 , 加载其中的 image
资源时 ( 也就是 map_images
方法中的 _read_images
) , 去读取编译时所存储到 __objc_catlist
的 section
段中的数据结构 , 并存储到 category_t
类型的一个临时变量中.catlist
和原先类 cls
进行映射remethodizeClass
修改 method_list
结构 , 将分类的内容添加到本来类中.至于属性和协议, 跟方法的流程是同样的, 只是存放在不一样的
section
段中. 这里就很少赘述了 , 参考如下 :
最后有一个疑问: ❓
首先咱们知道 OC 查找方法流程中 , 当查找类方法的方法列表时 , 是采用了一个二分查找的方式的 . 那么咱们类方法的扩展方法是添加到了原类的方法列表中前面位置的 . 那么它如何保证外部调用方法时 , 是必定会调用到类方法中的呢 ?
答:
看以下图:
当方法遍历二分查找时 , 后面的方法查找到 , 一样会往前查找一遍看看有没有同名 ( 方法编号 ) 方法 , 若是有 , 则返回的是前面的方法 . 以此来保证了其优先级顺序 , 也就是说 方法列表中前面的方法会有高优先级执行权限 .
从而也就保证了分类实现的目的.
Category
关联属性众所周知 , Category
中声明属性 , 但并不会在 method_list
中生成对应的 setter
和 getter
方法以及对应的实例变量 , 编译时会有警告.
那么解决方法你们也都知道 , 就是手动设置关联属性 , 能够理解成手动补上 setter
和 getter
方法 . 具体写法以下:
#import "LBPerson+Category.h"
#import <objc/runtime.h>
static NSString * ageCategoryKey = @"ageCategoryKey";
@implementation LBPerson (Category)
- (NSString *)ageCategory {
return objc_getAssociatedObject(self, &ageCategoryKey);
}
- (void)setAgeCategory:(NSString *)age {
objc_setAssociatedObject(self, &ageCategoryKey, age, OBJC_ASSOCIATION_COPY);
}
复制代码
看了上面分类结构体的源码以后 , 其实咱们就很清楚了 . 由于咱们并无看到像类的结构体中的实例变量列表 , 也就是咱们所说的 ivar_list
, 所以也就不会有编译器像类中自动帮咱们作 @synthesize
生成实例变量 ivar
和自动生成setter
和 getter
方法了
那么咱们就来深刻探讨一下 , 关联属性究竟是如何实现属性以及究竟是如何存储 , 又是如何销毁的呢 ?
注意 : 一样是刚刚的代码
objc4
步骤 1. 直接点击进入 objc_setAssociatedObject
方法. 过渡方法跳过.
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
//进行内存管理!!!
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
//初始化 HashMap
AssociationsHashMap &associations(manager.associations());
//当前对象的地址按位取反(key)
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
//<fisrt : disguised_object , second : ObjectAssociationMap>
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
//<fisrt : 标识(自定义的) , second : ObjcAssociation>
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
//ObjcAssociation
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
复制代码
具体总结一下这个方法: ( 其实咱们也能大概猜出来这个方法要作什么 , 无非是和自动生成的 setter
方法相似的操做 )
先根据内存管理语义作对应的引用计数等其余的操做. (
acquireValue(value, policy)
方法 )建立了一个管理者
AssociationsManager
, 如何建立了一个AssociationsHashMap
, 给这个map
对象赋值 : ( 将当前对象的地址 按位取反做为 key , 建立ObjectAssociationMap
对象做为 value)给
ObjectAssociationMap
赋值 : ( 将用户指定 , 传进来的key
做为 key , 建立ObjcAssociation
对象做为 value)
ObjcAssociation
对象中存放了用户指定的值 以及内存管理策略语义 . ( ObjcAssociation(policy, new_value) )给当前原类的
isa
添加标识 , 以便销毁时识别是否须要释放关联对象.(object->setHasAssociatedObjects()
)
步骤 2. 搜索 delloc
, 点击依次进入, 找到:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
复制代码
那咱们看到析构函数中根据 isa
判断了当有关联对象或者其余标识时 , 会调用 object_dispose
方法. 再点击 , 依次进入
void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
复制代码
依次释放关联属性等等. 那么也就是说, 咱们的关联属性的生命周期 是跟随原对象走的.
至此 , 关联对象的原理咱们已经解析完毕 , 总结一下:
Category
关联属性总结:
关联属性经过本身定义一个新的数据结构
ObjcAssociation
容器来保存用户设置的内容 以及读取用户设置的内容 . 以此达到属性那种经过方法访问实例变量的效果.分类关联属性的生命周期同原先类 . 经过在
isa
中标识是否有关联对象来在dealloc
中实现销毁操做.
下篇会继续
load
方法的探索. 欢迎关注交流.