iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序文
在前面文章《iOS底层之类的加载》中探求了类的加载流程,本篇将对分类
展开探求,从分类的结构到分类的加载流程,来探求分类的实质。
Runtime
优化
在《WWDC 2020 关于Runtime的优化》中介绍了关于Runtime的优化内容,核心内容是对rw
扩展出rwe
,来优化整个运转时的功能。
咱们的应用程序装载到设备时,系统会为安装程序分配一段内存,这段内存是不可变的称为clean memory
也便是ro
,当程序运转启动时,系统会拓荒新的内存来运转程序,这段内存是可变化的称之为dirty memory
也便是rw
,由于系统内存有限,所以rw
这段内存是比较宝贵的。
但是在rw
中的数据很多是不会改变的,直接从ro
读取即可,需求改变的数据经过Runtime
运转时操作的数据,比如类的办法、特点、协议等,将这些数据存放在rwe
上,这样就能够达到对dirty memory
的优化。
分类
的意义便是要动态的给类添加办法等,那么分类的探求就从rwe
下手。
分类的结构
自界说类LGPerson
和分类LGPerson (Cat)
,然后经过clang
生成cpp
文件检查
@interface LGPerson : NSObject
{
NSString * name;
}
@property (nonatomic, copy) NSString * nickName;
- (void)instanceMethod;
+ (void)classMethod;
@end
@implementation LGPerson
- (void)instanceMethod {
NSLog(@"%s", __func__ );
}
+ (void)classMethod {
NSLog(@"%s", __func__ );
}
@interface LGPerson (Cat)
@property (nonatomic, copy) NSString * lg_nickName;
- (void)lg_categoryInstanceMethod;
+ (void)lg_categoryClassMethod;
@end
@implementation LGPerson (Cat)
- (void)lg_categoryInstanceMethod {
NSLog(@"%s", **__func__** );
}
+ (void)lg_categoryClassMethod {
NSLog(@"%s", **__func__** );
}
@end
主类的cpp
代码
在
LGPerson
的主类cpp
中有实例办法
、类办法
、以及特点的set
和get
办法;
分类的cpp
代码
到在分类中有
实例办法
和类办法
,并没有特点lg_nickName
的set
和get
办法
让LGPerson (Cat)
分类遵守协议NSObject
,从头生成cpp
分类的类型是_category_t
,经过category_t
在源码中能够找到分类
的结构界说
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
WrappedPtr<method_list_t, method_list_t::Ptrauth> 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);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
-
name
: 分类名称; -
cls
:主类; -
instanceMethods
:实例办法; -
classMethods
: 类办法; -
protocols
: 所遵守的协议; -
instanceProperties
:实例特点,并没有set
和get
办法; -
_classProperties
:类特点
经过
分类
的结构能够看出,分类
是没有元类
的,主类
的类办法是在元类
中,分类
的类办法是在classMethods
中。
分类的加载
rwe
是经过类中的extAllocIfNeeded
办法创建,假如已有值直接返回rwe
,假如没有则经过extAlloc
创建。
在源码中大局查找extAllocIfNeeded
,调用的办法有
attachCategories
分类、class_setVersion
设置版别、addMethods_finish
动态添加办法、class_addProtocol
添加协议、_class_addProperty
添加特点、objc_duplicateClass
类的复制、demangledName
修正类名
这些办法中有关分类的只需attachCategories
办法
attachCategories
办法探求
// 将办法列表、特点和协议从类别附加到类。假设cats中的类别都是按加载次序加载和排序的,最旧的类别优先。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
* 只需少数类在启动期间具有超过64个类别。这使用了一个小仓库,并避免了malloc。
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
* 类别必须以正确的次序添加,即从后到前。
* 为了完成分块,咱们早年到后迭代cats_list,向后构建本地缓冲区,
* 并对块调用attachList。attachLists预先准备好列表,
* 因而终究结果按预期次序排列。
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS); // 是否为元类
auto rwe = cls->data()->extAllocIfNeeded(); // 初始化rwe
// cats_count分类数量,循环处理分类
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
// 办法处理
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) { // 第一次mcount = 0
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__ );
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
//++mcount,将mlist放在mlists的最终
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
//++propcount,将proplist放在proplists的最终
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
// 向类中添加办法并排序
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__ );
// rwe中添加办法
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__ , [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
// rwe中添加特点
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
// rwe中添加协议
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
剖析一下办法
- 初始化
rwe
;- 经过分类数量
cats_count
,循环处理分类中的办法
、特点
、协议
;- 循环中按倒序刺进法,将一切的办法存在
mlists
,协议存放在protolists
,特点存放在proplists
;- 假如
mcount
大于0,说明有分类办法,经过prepareMethodLists
向类中添加办法并排序,然后rwe
中的methods
调用attachLists
,添加分类办法到rwe
;rwe
中的properties
调用attachLists
,添加分类特点到rwe
;rwe
中的protocols
调用attachLists
,添加分类协议到rwe
;
rwe
中的办法
、特点
和协议
都是调用的attachLists
刺进数据,是由于他们的界说都继承list_array_tt
,咱们看一下list_array_tt
中的attachLists
办法
attachLists
办法探求
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; // 总数据数量
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount)); // 拓荒新空间
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; i--) // 将旧值从后往前取出,刺进到新数组的后边
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++) // 将新值早年往后添加到新数组
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) { // 没有数据且刺进数据只需一条
// 0 lists -> 1 list
list = addedLists[0];// 直接刺进
validate();
}
else { // 没有数据且刺进多条数据
// 1 list -> many lists
Ptr<List> oldList = list; // 取出旧值
uint32_t oldCount = oldList ? 1 : 0; // 有旧值oldCount=1不然oldCount=0
uint32_t newCount = oldCount + addedCount;// 总数据数量
setArray((array_t *)malloc(array_t::byteSize(newCount)));// 拓荒新空间
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList; //将旧值存放在最终的方位
for (unsigned i = 0; i < addedCount; i++) // 早年往后循环刺进新值
array()->lists[i] = addedLists[i];
validate();
}
}
算法剖析:依据已有数据做不同状况处理
-
0 -> 1
:无数据且新刺进数据为1条
- 直接刺进到
list
-
1 list -> many lists
: 只需1条数据或刺进多条数据
- 取出旧值;
- 对
oldCount
赋值:有旧值oldCount=1
不然oldCount=0
;- 核算刺进后数据总量
newCount = oldCount + addedCount
;- 拓荒新空间;
- 将旧值存放在新空间最终;
- 依据
addedCount
早年往后刺进新值;
-
many lists -> many lists
: 已有多条数据
- 取出旧值;
- 核算刺进后数据总量
newCount = oldCount + addedCount
;- 拓荒新空间;
- 依据
oldCount
循环从后往前取出旧值,存放在新空间的后边方位;- 依据
addedCount
早年往后刺进新值;
依据hasArray
和setArray
判断,只需调用setArray
或array()->count
中的count
值大于0,hasArray
即为YES
。
一切分类加载后,rwe
的methods
结构应该为
分类加载实例探求
在类的加载探求中,咱们知道了类的加载机遇区分为懒加载类
和非懒加载类
,即是否完成+load
办法,分类的加载咱们相同按照懒加载
和非懒加载
的形式探求。
- 主类和分类都为懒加载;
- 主类非懒加载,分类懒加载;
- 主类懒加载,分类非懒加载;
- 主类和分类都非懒加载;
实例类为LGPerson
,就在attachCategories
办法中经过类名mangledName
比较LGPerson
来准确加断点、打印输出调试
1.主类和分类都为懒加载
主类LGPerson
声明实例办法sayHello
和sayByeBye
分类CatA
中声明实例办法sayHello_A
,以及重写主类办法sayHello
在main
函数中调用sayHello
并没有进入attachCategories
函数中,同时能够看到sayHello
是调用的分类
的办法,由此能够知道假如主类
和分类
都未完成+load
办法,分类的办法
等信息是在编译时
就和主类编译在一起了,在类的加载流程中验证
在main
函数中调用LGPerson
的alloc
办法开始加载类,这儿的method_list_t
是从ro
中读取的数据,输出检查
此时,list
中的办法数主类和分类的办法集合
,主类办法放在集合的最终方位,但这儿办法还没有经过排序处理,经过prepareMethodLists
会对list
进行排序修正
。
经过断点跟进,再输出一下经过fixupMethodList
处理后的list
处理后,主类的sayHello
办法排在了分类sayHello
后边。在调用sayHello
时,音讯查找流程在对排序好的list
进行二分法查找,并且会经过while
循环找到最前面的同名办法,这样分类办法
就覆盖了主类办法
。
2.主类非懒加载、分类懒加载
LGPerson
的主类完成+load
办法,分类不完成
在main
函数中调用sayHello
也没有进入attachCategories
函数中,sayHello
是调用的分类
的办法,由此能够知道假如主类
非懒加载和分类
懒加载,分类的办法
等信息也是在编译时
就和主类编译在一起了。
下面在类的加载流程中验证
这儿能够看出
-
LGPerson
是在程序启动时_read_images
完成加载的, - 分类的办法也是
编译时
就和主类办法编译在一起了,这儿是经过ro
获取的method_list_t
,分类办法也在ro
中。
3.主类懒加载、分类非懒加载
主类LGPerson
不完成+load
办法,分类完成
运转检查
也没有进入attachCategories
函数中,sayHello
是调用的分类
的办法,由此能够知道假如主类
懒加载和分类
非懒加载,分类的办法
等信息也是在编译时
就和主类编译在一起了。
在类的加载流程中验证
-
LGPerson
是在程序启动时_read_images
完成加载的,分类非懒加载
会导致主类
被迫成为非懒加载类
; - 分类的办法也是
编译时
就和主类办法编译在一起了,这儿是经过ro
获取的method_list_t
,分类办法也在ro
中。
4.主类和分类都为非懒加载
主类LGPerson
和分类CatA
都完成+load
办法,运转检查
经过输出打印能够看出,
主类的加载
和分类的加载
是在不同的流程中
主类加载
能够看出主类的加载是在
_read_images
流程,这儿从ro
读取的method_list_t
中只需主类的两个办法。
分类的加载
分类的加载流程是
load_images
->loadAllCategories
->load_categories_nolock
->attachCategories
,在attachCategories
中会创建rwe
,并调用prepareMethodLists
对分类办法进行排序处理,然后调用rwe
中methods
的attachLists
刺进分类的mlist
这儿的LGPerson
的分类有3个,只在分类CatA
中完成了+load
办法,咱们调用分类CatC
的办法
发现加载分类的时候,并没有输出分类CatC
的名字,也便是没有分类CatC
的加载,为什么能够调用sayHello_C
成功了呢?
猜测:分类
CatC
没有完成+load
,会不会是编译时和主类编译在一起了
再盯梢一下主类的加载流程
这儿看到,主类
ro
中只需主类自己完成的两个办法,所以猜测不成立。
再盯梢一下分类的加载流程
这儿看到,在加载分类
CatB
时,确有分类CatC
的办法,他们的共同点是都没有完成+load
,系统在编译时会把未完成+load
办法的分类合并成一个分类来处理,从而简化分类的加载流程。
咱们再添加一个分类CatD
做验证
能够看到,分类加载时把未完成+load
办法的CatB
、CatC
和CatD
一起加载的,验证了系统在编译时会把未完成+load
办法的分类合并成一个分类来处理。
总结
分类的加载原理
和类相同区分为是否为懒加载
,两者组合可分为4种状况
以上是对分类的加载流程
探求过程的总结,不免有不足和错误之处,如有疑问请在评论区留言吧