iOS基础概念2 +load 调用时机: 加载镜像load_images中的类对象(class)和分类(category)的时候就会调用 void load_images(xxx) { xxxx //1. 查找并准备load方法,以供后面去调用 prepare_load_methods((const headerType *)mh); call_load_methods(); // 2.调用这些load方法 } prepare_load 递归方法把 类及其父类 add_class_to_loadable_list(cls); 同样按编译顺序添加分类 add_category_to_loadable_list(cat) 最终需要调用load方法的类就都添加到列表里了,等待被遍历获取 调用频率: 只会调用一次,不论在项目中有没有用到该类对象或者该分类,所有的类都会调 用一次 调用顺序: 1 先调用类对象(class)的+load方法: 1.1类对象的load调用顺序是按照 类文件的编译顺序 进行先后调用; 1.2调用子类+load之前会先调用父类的+load方法(递归寻找存储在列表 里) 2 再调用分类(category)的+load方法: 按照编译先后顺序调用(先编译的,先被调用) 在main函数开始之前调用load方法 调用机制: 通过函数指针load_method从loadable_class中获得+load方法的IMP作为其参 数,然后就直接对其进行调用(*load_method)(cls, SEL_load); load 方法不是通过消息机制调用的, 而是获取IMP直接调用的,因此不会存在方 法被覆盖问题 +initialize 调用时机: 在类第一次接收到消息的时候调用 调用机制: 通过objc_msgSend()进行调用的 通过消息机制调用的方法 1 可能被调用多次 ,2 可能被分类重名导致“覆盖”,初 始化分类最后编译的那个(如果实现了的话) 可使用dispath_once 保证执行一次 调用顺序: 1.先调用父类的,再调用子类的 2.如果子类没实现+initialize方法,会调用父类的+initialize。所以父类的 +initialize可能被调用多次 3.如果分类实现了+initialize,会覆盖类本身的+initialize 代码逻辑: void initializeNonMetaClass(Class cls) { ASSERT(!cls->isMetaClass()); Class supercls; bool reallyInitialize = NO; supercls = cls->superclass; if (supercls && !supercls->isInitialized()) { // 1. 递归调用,父类的initialize在子类之前调用 initializeNonMetaClass(supercls); } ...省略部分代码... { // 2. 调用initialize方法 callInitialize(cls); .... } ... } void callInitialize(Class cls) { ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize)); asm(""); } // 可以看到callInitialize中直接通过objc_msgSend方法调用+initialize方法。 这也就解释了为什么+initialize子类没实现,会去调用父类中的。category中的 会覆盖类本身的。 isa 结构 32位 旧版 isa 结构: typedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id; 对比: 32位和64位对比发现,新版isa不单单是指向对象的地址指针 64位isa结构: struct objc_object { private: isa_t isa; union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; #if SUPPORT_PACKED_ISA # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL struct { // 0,代表普通指针,存储着class、meta-class对象的内存地址;1,代 表优化过,使用位域存储更多信息 uintptr_t nonpointer : 1; // 是否设置过关联对象,如果没有,释放时会速度更快 uintptr_t has_assoc : 1; // 是否有C++的稀构函数,如果没有,释放时会更快 uintptr_t has_cxx_dtor : 1; // 这个部分存储的是真正的Class、Meta-Class对象的内存地址信息,因 此要通过 isa & ISA_MASK才能取出这里33位的值,得到对象的真正地址。 uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 // 用于在调试的时候分辨对象是否完成了初始化 uintptr_t magic : 6; // 是否被弱饮用指针指向过,如果没有,释放时会更快 uintptr_t weakly_referenced : 1; // 对象是否正在被释放 uintptr_t deallocating : 1; // 引用计数器是否过大无法存储在isa中,若果是,这里就为1,引用计数 就会被存储在一个叫SideTable的类的属性中。 uintptr_t has_sidetable_rc : 1; // 里面存储的值是 引用计数 - 1 uintptr_t extra_rc : 19; # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; # elif __x86_64__ 等等其它内核 MASK isa指针需要通过与一个叫ISA_MASK的值(掩码)进行二进制&运算,才能得到 真实的class/meta-class对象的地址 通过 isa & ISA_MASK 取出来的到底是哪几位上面的值。同时还可以发现一个小 细节,最终得出来的对象的地址值,会得到36个有效二进制位,而最后的四位, 只可能是 1000 或者 0000,也就是十六进制下的 8或0,因此对象的地址最后 一位(十六进制下),一定是8或0。 内存布局 代码段 :占用空间很小,一般存放在内存的低地址空间,我们平时编写的所有代 码,就是放在这个区域 数据段 :用来存放全局变量 堆区 :是动态分配内存的,用来存放我们代码中通过alloc生成的对象,动态分配 内存的特点是需要程序员申请内存和管理内存。例如OC中alloc生成的对象需要 调用releas方法释放【MRC下】,C中通过malloc生成的对象必须要通过free()去 释放。 栈区 :系统自动分配和销毁内存,用于存放函数内生成的局部变量