分类前情
分类里面可以写对象方法、类方法、协议、属性
不过这个属性不会生成_成员变量,只会生成属性的set和get方法的声明
打开源码搜索category,可以在objc-private.h里看到如下定义
#if __OBJC2__
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
#else
typedef struct old_method *Method;
typedef struct old_ivar *Ivar;
typedef struct old_category *Category;
typedef struct old_property *objc_property_t;
#endif
然后点击category_t查看
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);
};
看来看去发现这个分类结构体里就是没有ivars(成员变量),至少这个结构体说明分类是没法添加成员变量
下面通过转C++文件来查看下结构
新建MJPerson的分类
#import "MJPerson.h"
@interface MJPerson (Category)
@property (nonatomic , copy) NSString *myName;
@property (nonatomic , assign) int myNum;
-(void)catEat;
+(void)dogRun;
@end
#import "MJPerson+Category.h"
@implementation MJPerson (Category)
-(void)catEat{
NSLog(@"---catEat---");
}
+(void)dogRun{
NSLog(@"---dogRun---");
}
@end
然后通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Category.m -o MJPerson+Category.cpp将g分类的.m文件转C++文件来查看下结构
struct _class_t { //当前类信息
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
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;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)
static struct /*_method_list_t*/ { //分类里的对象方法信息
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"catEat", "v16@0:8", (void *)_I_MJPerson_Category_catEat}}
};
static struct /*_method_list_t*/ { //分类里的类方法信息
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"dogRun", "v16@0:8", (void *)_C_MJPerson_Category_dogRun}}
};
//如果有协议,也会生成分类里的协议信息
//...
static struct /*_prop_list_t*/ { //分类里的属性信息
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_MJPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"myName","T@\"NSString\",C,N"},
{"myNum","Ti,N"}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_MJPerson;
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = //编译器生成了category本身_OBJC_$_CATEGORY_MJPerson_$_Category,并用前面生成的列表来初始化category本身。
{
"MJPerson", //赋值给const char *name;
0, // &OBJC_CLASS_$_MJPerson, //赋值给struct _class_t *cls;
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Category, //赋值给const struct _method_list_t *instance_methods;
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Category, //赋值给const struct _method_list_t *class_methods;
0, //赋值给const struct _protocol_list_t *protocols;
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Category, //赋值给const struct _prop_list_t *properties;
};
通过上面可以看出,分类里的类方法和对象方法以及属性都有相应的生成,也是没有分类的成员变量的。(如果让分类继承<NSCopying>,在这里也是可以看到生成协议信息的)
分类底层结构里,是一个分类结构体,分类结构体里存以下东西(通过将文件转C++大致可以看出)(第一点这个结构体里没有_成员变量的存储列表;第二点成员变量(如果有的话)}的内存布局是在编译阶段就确定好的,而分类是通过运行时动态加载的。所以表面上就很多人认为分类不能添加属性 )
对象方法
类方法
属性
协议
所以通过源码,转C++,以及编译过程不同都可以知道分类不能添加成员变量了。
通过上面源码查看后可能会有如下的一个疑问
比如这个MJPerson类,如下调用分类里的方法
MJPerson *mj = [[MJPerson alloc]init];
[mj catEat];
[MJPerson dogRun];
上面两句调用,catEat是由实例对象mj的ISA指针指向MJPerson,然后调用MJPerson的catEat对象方法。dogRun是由MJPerson的isa指向元类对象,调用元类对象的dogRun类方法。但是上述的分类对象方法等是放在分类的结构体里的,怎么办 ?
开始源码分析
在objc-os.mm文件里找到程序运行时的入口函数(_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);
}
category被附加到类上面是在map_images的时候发生的(上面源码里是将map_images的地址传进去,所以可以点击map_images查看map_images具体做了什么事)
点击map_images看到如下
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
点击map_images_nolock看到如下
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
...//省略跟当前追踪无关代码
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
}
点击_read_images看到如下(read_images是读取模块的意思。上面几步流程简言之就是在new-ABI的标准下,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法)
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
...//省略跟当前追踪无关代码
// Discover categories.
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) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties) //分类如果有对象方法或者协议或者属性
{
addUnattachedCategoryForClass(cat, cls, hi); //利用addUnattachedCategoryForClass把类和category做一个关联映射
if (cls->isRealized()) { //Realized的翻译是已实现
remethodizeClass(cls); //remethodizeClass是真正去处理添加事宜的功臣,这里传的是类
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()); //remethodizeClass是真正去处理添加事宜的功臣,这里传的是元类了
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
}
点击remethodizeClass看到如下代码
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
//unattachedCategoriesForClass方法获取cls中未完成整合的所有分类
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/); //attachCategories接收类,分类数组等参数,attachCategories方法是将cats拼接到cls上
free(cats);
}
}
点击attachCategories看到如下代码
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists)); //二维数组
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count; //宿主的个数
bool fromBundle = NO;
while (i--) { //while循环,括号里面代码实现是倒序遍历,最先访问最后编译的分类。注意点:多个分类有同名函数时,最终执行哪个分类的函数,就看哪个分类最后参与编译。
auto& entry = cats->list[i]; //获取一个分类
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);//获取该分类的方法列表,即 装有方法的一维数组
if (mlist) {
mlists[mcount++] = mlist; //方法列表添加到二维数组里
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();//获取cls里的rw结构数据
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);//主要是针对分类中有关内存管理相关方法情况下 一些特殊处理
rw->methods.attachLists(mlists, mcount); //此时mlists已经是一个装有所有分类的所有实例方法的二维数组。rw->methods(拿到类对象里的方法列表数组)然后去调用attachLists,此时attachLists的作用是将含有mcount个元素的mlists拼接到rw的methods上
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);//这里是把所有category的属性列表拼成了一个大的属性列表数组,然后转交给了attachLists方法
free(proplists);
rw->protocols.attachLists(protolists, protocount);//这里是把所有category的协议列表拼成了一个大的协议列表数组,然后转交给了attachLists方法
free(protolists);
}
点击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; //拼接之后的元素总数
setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); //扩容,根据新总数重新分配内存
array()->count = newCount;//重新设置元素总数
/*内存移动,假设原来类有两个元素,分类共有三个元素,memmove后如下
[[],[],[],[原有的第一个元素],[原有的第二个元素]]
*/
memmove(array()->lists + addedCount, array()->lists,oldCount * sizeof(array()->lists[0])); // 以实例方法举例: array()->lists类对象原来的方法列表
/*内存拷贝,假设原来类有两个元素,分类共有三个元素,memcpy后如下
[
[addedLists里的第一个元素],
[addedLists里的第二个元素],
[addedLists里的第三个元素],
[原有的第一个元素],
[原有的第二个元素]
]
这也就是分类会“覆盖”宿主类的方法的原因
*/
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));// 以实例方法举例:addedLists所有分类的方法列表
//扩展:
/*memmove和memcpy都是C语言的库函数
void *memmove(void *__dst, const void *__src, size_t __len);
void *memcpy(void *__dst, const void *__src, size_t __n);
这两个函数都是将__src指向位置的__len/__n字节数据拷贝到__dst指向的位置,区别就在于关键字restrict, memcpy假定两块内存区域没有数据重叠,而memmove没有这个前提条件。如果复制的两个区域存在重叠时使用memcpy,其结果是不可预知的,有可能成功也有可能失败的,所以如果使用了memcpy,程序员自身必须确保两块内存没有重叠部分。
*/
}
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]));
}
}
上面一大堆底层代码,大致过程就是分类在编译的时候通过Runtime被装载;把所有Category的方法合并到一个大数组、属性合并到一个大数组、协议合并到一个大数组,合并到一个大数组中,然后通过memmove将类原来数据后移分类数组的长度,然后通过memcpy函数将合并后的分类数据(方法、 属性、协议),插入到类原来数据的前面
由此可以看出,以前所说的分类有跟原本类同名也就是相同的方法时,会调用分类的方法是有道理的,因为分类方法被装载在前面,原本类的同名方法被排在了后面,而运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休。如果两个分类有同样方法,先调哪个看编译顺序。
那么如何证明原本类相同的方法还在呢?
通过前几天写的那个运行时打印类的所有对象方法的函数,一验证就知道了,打印结果会有两个同名函数
//写一个运行时,用来打印一个类的对象方法
- (void)printMethods:(Class)cls //这个cls可以传元类,如果传元类,那么就打印类方法;传类对象就打印对象方法
{
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@", methodNames);
free(methods);
}
通过上述方法还可以验证新建一个分类后,当程序运行时,方法就已经被加载到方法列表里了,只是不引入分类文件不把这个这个分类方法暴露出来而已
比如
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@end
#import "MJPerson.h"
@implementation MJPerson
@end
新建MJPerson的分类
#import "MJPerson.h"
@interface MJPerson (Category)<NSCopying>
-(void)catEat;
@end
#import "MJPerson+Category.h"
@implementation MJPerson (Category)
-(void)catEat{
NSLog(@"--Category-catEat--");
}
@end
然后我们在控制器里通过[self printMethods:[MJPerson class]];打印,不引入分类文件也可以看到打印出
2019-11-26 10:18:28.315420+0800 Test[2014:55852] MJPerson - catEat
只不过不引入#import "MJPerson+Category.h"文件,系统不把分类的catEat暴露出来。
现在接着来验证类和分类有同名函数,不引入分类头文件,调用同名函数问题。
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
-(void)catEat;
+(void)animalEat;
@end
#import "MJPerson.h"
@implementation MJPerson
+(void)animalEat{
NSLog(@"--MJPerson-animalEat---");
}
-(void)catEat{
NSLog(@"--MJPerson-catEat---");
}
@end
新建MJPerson的分类
#import "MJPerson.h"
@interface MJPerson (Category)<NSCopying>
@property (nonatomic , copy) NSString *myName;
@property (nonatomic , assign) int myNum;
-(void)catEat;
+(void)animalEat;
@end
#import "MJPerson+Category.h"
@implementation MJPerson (Category)
+(void)animalEat{
NSLog(@"--Category-animalEat---");
}
-(void)catEat{
NSLog(@"--Category-catEat---");
}
@end
然后再控制器里不引入分类头文件,调用下列方法
MJPerson *person = [[MJPerson alloc]init];
[person catEat];
[MJPerson animalEat];
可以看到打印的是分类的方法
2019-11-26 10:25:56.605294+0800 Test[2114:59950] --Category-catEat---
2019-11-26 10:25:56.605347+0800 Test[2114:59950] --Category-animalEat---
由此也确实的验证了分类在编译的时候通过Runtime被装载,无论引不引入分类文件,只有类和分类有同名方法,调用这个同名方法,分类的方法都会“覆盖”类的方法。只不过不引入分类头文件,在没有同名函数的时候,分类方法是存在的,但不被直接暴露调用而已。
load方法
+load方法会在程序刚启动时,通过runtime加载类、分类时调用(给类、分类啊写load方法,并不使用这个类和分类,但是在程序启动的时候仍然可以看到类、分类的load方法被调用,可以看出+load方法会在runtime加载类、分类时调用。load一般是系统自动调用自动处理)
例:
MJPerson.m文件里实现load
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@end
#import "MJPerson.h"
@implementation MJPerson
+ (void)load {
NSLog(@"--MJPerson-load--");
}
@end
新建MJStudent继承MJPerson,然后MJStudent.m文件里实现load
#import "MJPerson.h"
@interface MJStudent : MJPerson
@end
#import "MJStudent.h"
@implementation MJStudent
+ (void)load {
NSLog(@"--MJStudent-load--");
}
@end
新建MJStudent的分类,然后MJStudent分类文件里实现load
#import "MJStudent.h"
@interface MJStudent (Category)
@end
#import "MJStudent+Category.h"
@implementation MJStudent (Category)
+ (void)load {
NSLog(@"--MJStudent (Category)-load--");
}
@end
然后在没有任何地方引入MJPerson、MJStudent、MJStudent分类的情况下启动项目,可以看到控制台打印如下
2019-11-26 10:35:20.617192+0800 Test[2303:65508] --MJPerson-load--
2019-11-26 10:35:20.617619+0800 Test[2303:65508] --MJStudent-load--
2019-11-26 10:35:20.617679+0800 Test[2303:65508] --MJStudent (Category)-load--
因此,说+load方法会在程序刚启动时,通过runtime加载类、分类时调用(给类、分类啊写load方法,并不使用这个类和分类,但是在程序启动的时候仍然可以看到类、分类的load方法被调用,可以看出+load方法会在runtime加载类、分类时调用)
每个类、分类的+load,在程序运行过程中只调用一次
load的调用顺序? (无论是类分类子类子类分类所有load方法只要重写都会打印)
1> 先调用类的load (如有继承,也是先调用父类即子类的load)
a) 先编译的类,优先调用load
b) 调用子类的load之前,会先调用父类的load
2> 再调用分类的load
a) 先编译的分类,优先调用load
通过代码可以验证书面结论
2019-11-26 10:43:21.962804+0800 Test[2458:71004] --MJPerson-load--
2019-11-26 10:43:21.963178+0800 Test[2458:71004] --MJStudent-load--
2019-11-26 10:43:21.963229+0800 Test[2458:71004] --MJStudent (Category1)-load-- //MJStudent的第二个分类
2019-11-26 10:43:21.963314+0800 Test[2458:71004] --MJStudent (Category)-load-- //MJStudent的第一个分类
2019-11-26 10:43:21.963376+0800 Test[2458:71004] --MJPerson (Category)-load--
initialize方法
initialize是类第一次接收到消息的时候(第一次使用这个类)调用,每一个类只会initialize一次
例:
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@end
#import "MJPerson.h"
@implementation MJPerson
+(void)initialize {
NSLog(@"--MJPerson-initialize--");
}
@end
然后在MJPerson的子类MJStudent以及分类都如上实现+(void)initialize方法。
然后运行程序,不使用类、分类,可以看到控制台没有打印。
但是当我们初始化或者调用类的方法时
MJPerson *p = [[MJPerson alloc]init];
就可以看到打印
2019-11-26 11:06:34.351296+0800 Test[3018:90114] --MJPerson (Category)-initialize--
然后我们初始化子类
MJStudent *p = [[MJStudent alloc]init];
就可以看到打印
2019-11-26 11:07:21.767492+0800 Test[3047:91570] --MJPerson (Category)-initialize--
2019-11-26 11:07:21.767591+0800 Test[3047:91570] --MJStudent (Category)-initialize--
因此可以得出下面initialize调用顺序结论
initialize调用顺序 (如果分类重写initialize方法会“覆盖”类的initialize)
1> 先初始化父类或者父类分类 (尽管只初始化子类,如果有继承,也是先调用父类或者父类分类的initialize)
2> 再初始化子类以及子类分类
话外:类,分类、子类、子类分类里都有+(void)animalRun;类方法时,调用子类的animalRun,只会打印子类分类的animalRun类方法。是因为分类会覆盖类,子类会覆盖父类。但是这个initialize是比较特殊的。
那为什么initialize方法会“覆盖”类的initialize,而load方法不会覆盖呢
是因为initialize是通过objc_msgSend调用(objc_msgSend消息发送机制本质就是通过ISA指针找对象),而load是根据类的函数地址直接调用(底层源码可以论证这些问题)
load、initialize方法的区别什么?
1.调用方式
1> load是根据函数地址直接调用
2> initialize是通过objc_msgSend调用 (objc_msgSend消息发送机制本质就是通过ISA指针找对象)
2.调用时刻
1> load是runtime加载类、分类的时候调用(只会调用1次)
2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次
通过load和initialize学习,回答下面两道题目。
1.Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?
有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
2.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
链接静态库有些方法找不到时
other linker flags设置成 -all_load
类扩展也叫匿名分类,跟分类没有关系。里面用来私有一些东西。
匿名分类(类扩展)的方法都是私有,那为什么还要写声明呢,直接写实现不行吗?
一般不用写的,有些写下声明是为了规范
网友评论