iOS进阶之路(十八)总结

304 阅读17分钟

1.对象

1.alloc、init、new的区别

  • alloc:
1. _objc_rootAlloc 
2. callAlloc 
3. instanceSize(计算对象大小) 算法:(x + 7) & ~7  
4. calloc(申请开辟空间)
5. initInstanceIsa(指针关联对象)

image.png

  • init
+ (id)init { 
  return (id)self; 
}

init底层没有做什么操作,直接返回了obj。这样做是一种抽象工厂设计模式,让代码实现更加自由;我们可以在子类中重写init方法做一些初始化操作。

  • new
+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

new = alloc + init

2.内存对齐

什么是内存对齐

在计算机中,内存大小的基本单位是字节,理论上来讲,可以从任意地址访问某种基本数据类型。

但是实际上,计算机并非按照字节大小读写内存,而是以2、4、8的倍数的字节块来读写内存。因此,编译器会对基本数据类型的合法地址作出一些限制,即它的地址必须是2、4、8的倍数。那么就要求各种数据类型按照一定的规则在空间上排列,这就是对齐。

iOS编译器会自动的进行字节对齐,32位下采用4字节对齐,64位下采用8字节对齐

内存对齐原则

  • 数据成员对齐原则:结构(struc)(或联合(union))的数据成员,第一个数据成员放在offset为0的位置,以后每个数据成员M存储的起始位置要从成员M大小或者成员M的子成员大小(只要该成员有子成员,比如说是数组、结构体等)的整数倍开始(比如int为4的字节,则要从4的整数倍地址开始存储)。
  • 结构体作为成员:如果一个结构内有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct A内存有struct B,B里有char、int、double等元素,B应该从double也就是8的整数倍开始存储)
  • 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
struct StructA {
    int a;      // 4字节,0,1,2,3
    char b;     // 1字节,4, 补5,6,7
    double c;   // 8字节,8,9,10,11,12,13,14,15
    int *p;     // 8字节,16,17,18,19,20,21,22,23      大小24字节
} strA;

struct StructB {
    int a;      // 4字节,0,1,2,3, 补4,5,6,7
    int *p;     // 8字节,8,9,10,11,12,13,14,15
    char b;     // 1字节,16, 补17,18,19,20,21,22,23
    double c;   // 8字节,24,25,26,27,28,29,30,21,32  大小32字节
} strB;

struct StructC {
    int a;      // 4字节,0,1,2,3
    char b;     // 1字节,4, 补5,6,7
    double c;   // 8字节,8,9,10,11,12,13,14,15
    int *p;     // 8字节,16,17,18,19,20,21,22,23
    struct StructB b;    //32字节,sizeof(structB) = 32      大小56字节 
} strC;

内存对齐原因

  • 性能上的提升:从内存占用的角度讲,对齐后比未对齐有些情况反而增加了内存分配的开支,是为了什么呢?数据结构(尤其是栈)应该尽可能地在自然边界上对齐,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。最重要的是提高内存系统的性能。
  • 跨平台:有些硬件平台并不能访问任意地址上的任意数据的,只能处理特定类型的数据,否则会导致硬件层级的错误。有些CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。
八进制对齐算法:对象的属性
WORD_MASK = 2^3-1 = 7
1.(x + 7) & ~7 
2. (x + 7) >> 3 << 3 左移3 右移3
十六进制对齐算法:对象
WORD_MASK = 2^4-1 = 15
1.(x + 15) & ~15 
2. (x + 15) >> 4 << 4 左移4 右移4

3.对比class_getInstanceSize,malloc_size,sizeOf

  • class_getInstanceSize: 依赖于<objc/runtime.h>,获取实例对象中成员变量内存大小。
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;   // WORD_MASK = 7
}

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}
  • malloc_size: 依赖于<malloc/malloc.h>,系统为该对象实际开辟的内存大小
malloc_size = (instanceSize + 16 - 1) >> 4 << 4

对象占用内存大小:8字节对齐,最小16(class_getInstanceSize)。
系统实际分配的内存大小:16字节对齐(malloc_size)

  • sizeOf: sizeof是操作符,不是函数,它的作用对象是数据类型,主要作用于编译时,得到的结果是该数据类型占用空间大小。sizeof 只会计算类型所占用的内存大小,不会关心具体的对象的内存布局;

例如:在64位架构下,自定义一个NSObject对象,无论该对象有多少个成员变量,最后得到的内存大小都是8个字节。

  • 面试题:一个NSObject对象占用多少内存?

在64位架构下, 系统分配了16个字节给NSObject对象(通过malloc_size函数获得);

但NSObject对象内部只使用了8个字节的空间(可以通过class_getInstanceSize函数获得)。

4.isa的结构

isa_t联合体有3个成员(Class cls、uintptr_t bits、位域ISA_BITFIELD),3个成员共同占用8字节的内存空间,通过ISA_BITFIELD里面的位域成员,可以对8字节空间的不同二进制位进行操作,达到节省内存空间的目的。

isa是一个联合体,8个字节,它的特性就是共用内存,或者说是互斥(比如说如果cls赋值了,再对bits进行赋值时会覆盖掉cls)。

union isa_t {
    isa_t() { }                                 // 初始化方法1
    isa_t(uintptr_t value) : bits(value) { }    // 初始化方法2

    Class cls;                                  // 成员1
    uintptr_t bits;                             // 成员2
#if defined(ISA_BITFIELD)
    struct {                                    // 成员3
        ISA_BITFIELD;  // defined in isa.h      // 位域宏定义
    };
#endif
};

在isa_t联合体内使用宏ISA_BITFIELD定义了位域,我们进入位域内查看源码。

# if __arm64__
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
  1. nonpointer:是否对isa指针开启指针优化
  • nonpointer = 0:不优化,纯isa指针,当访问isa指针时,直接通过isa.cls和类进行关联,返回其成员变量cls
  • nonpointer = 1:优化过的isa指针,指针内容不止是类对象地址,还会使用位域存放类信息、对象的引用计数,此时创建newisa并初始化后赋值给isa指针。 如果没有,则可以更快的释放对象。
  1. has_assoc:是否有关联对象,0没有,1存在。
  2. has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  3. shiftcls:存储类对象和元类对象的指针的值,在开启指针优化的情况下,在 arm64 架构中用 33 位用来存储类指针
newsisa.shiftcls = (uintptr_t)cls >> 3   // shiftcls >> 3 是因为他前面有3位,移动之后取的才是cls的值

5. magic:用于调试器判断当前对象是真的对象还是没有初始化的空间 6. weakly_referenced:对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放 7. deallocating:标志对象是否正在释放内存 8. has_sidetable_rc:当对象引用计数大于 10 时,则需要借用该变量存储进位(rc = retainCount) 9. extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1。例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下has_sidetable_rc

  1. 联合体所有属性共用内存,内存长度等于其最长成员的长度,使代码存储数据高效率的同时,有较强的可读性;而位域可以容纳更多类型。
  2. isa是联合体,最长成员的长度是位域的8字节(64位/8=8字节,1字节byte=8位bit),所以isa在内存中占用8字节。

5.isa走位

  • 1.对象:程序猿根据类实例化的。实例对象 和 类 通过isa关联。
  • 2.类:本质上也是对象,通过元类实例化出来,内存中只有一份,一般是编译期系统创建的。类对象 和 元类通过isa关联。
  • 3.元类:编译期系统创建的,便于方法的编译

image.png

  • isa走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
  • 继承关系(实线):子类 -> 父类 -> NSObject -> nil
  • 根元类的父类为NSObject。NSObject的父类是nil

6.对象的本质

clang编译Person对象:

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
    ...
}
  • 对象是一个结构体
  • 属性:_成员变量、setter、getter
  • 成员变量:底层编译不会生成相应的setter、getter

2.类

类的本质

struct objc_class : objc_object {
    // Class ISA;           // 8  
    Class superclass;       // 8
    cache_t cache;          // 16 不是8     // formerly cache pointer and vtable
    class_data_bits_t bits;                 // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    ...
};

objc_class继承于objc_object,存储着isa

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

类的本质是objc_class类型的结构体,objc_class继承于objc_object,所以满足万物皆对象

继承于objc_object就满足万物皆对象呢?我们来查看下NSObject源码

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

NSObject是OC版本的objc_object,和objc_object的定义是一样的,在底层会编译成objc_object

类的结构

通过objc_class源码可以得出,类有4个属性:isa、superclass、cache、bits。

  • Class ISA:实例对象通过isa关联类,类对象通过isa关联元类。Class是个指针,占用8字节。
  • Class superClass:类的父类,一般为NSObject。Class类型的结构体指针,占用8字节
typedef struct objc_class *Class
  • cache_t cache:cache主要是用来存储_buckets(方法缓存链表)和mask(分配用来缓存bucket的总数),还有当前实际占用的bucket个数。 cache是结构体,占用16字节。
struct cache_t {
    struct bucket_t *_buckets; // 8 结构体指针
    mask_t _mask;              // 4
    mask_t _occupied;          // 4
    ...
};
  • class_data_bits_t bits:为我们提供了便捷方法用于返回其中的 class_rw_t * 指针;一个long类型的bits标志位,这个标志位存储了很多flags(比如:快速分配内存标志、是否有析构函数、是否有构造函数、是否有自定义控件等)
struct class_data_bits_t {

    uintptr_t bits;
private:
    bool getBit(uintptr_t bit)
    {
        return bits & bit;
    }
    ...
public:

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ...
};

类的属性和方法

存储位置
属性bit.data.rw.ro.property
成员变量bit.data.rw.ro.ivar
实例方法bit.data.rw.method
类方法实例方法的形式,存储在元类。元类又继承自NSObject,形成一个闭环
ro是编译其生成的,运行时copy一份生成rw,所以属性、成员变量存在ro

cache_t cache

struct cache_t {
    struct bucket_t *_buckets; // 结构体指针
    mask_t _mask; // 可以看出是一个无符号Int类型,在64位下为uint32_t
    mask_t _occupied; // 容积
    ...
};

struct bucket_t {
    MethodCacheIMP _imp;
    cache_key_t _key;
};

并不是掉用一个方法就缓存,而是有一个特殊缓存策略

cache_t的作用

Class中的Cache主要是为了在消息发送的过程中,进行方法的缓存,加快调用效率。其中使用了动态扩容的方法,当容量达到最大值的0.75时,开始2倍扩容,扩容时会完全抹除旧的buckets,并且创建新的buckets代替,之后把最近一次临界的imp和key缓存进来。

为什么在0.75的时候进行扩容

在哈希这种数据结构里面,有一个概念用来表示空位的多少叫做装载因子——装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降 负载因子是3/4的时候,空间利用率比较高,而且避免了相当多的Hash冲突,提升了空间效率

bucket与mask、capacity、sel、imp的关系

类cls拥有属性cache_t,cache_t中的buckets有多个bucket,bucket里存储着方法实现imp和方法编号sel强转成的key值cache_key_t

  • mask对于bucket,主要是用来在缓存查找时的哈希算法
  • capacity对于bucket,可以获取到cache_t中bucket的数量

cache_t什么时候调用?

 * Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp

cache_fill_nolock的调用时机,我们在源码中可以看到,是在cache_fill中进行了调用。cache_fill的调用时机其实是在method lookup的过程中调用的,而方法查找则要牵扯到objc_msgSend,也就是消息发送机制。所以我们姑且可以认为,在消息发送的过程中,先通过缓存查找imp,如果查找到就直接调用,如果没有,那么就进行缓存。

3.方法

方法查找测试

  1. 对象方法:
objc_msgSend(student, sel_registerName("student_instanceMethed"));
  • 对象的实例方法 - 自己有
  • 对象的实例方法 - 自己没有 - 找父类
  • 对象的实例方法 - 自己没有 - 父类没有 - 找父类的父类 - NSObject
  • 对象的实例方法 - 自己没有 - 父类没有 - 找父类的父类 - NSObject也没有 - 崩溃
  1. 类方法
objc_msgSend(objc_getClass("AKStudent"),  // objc_getClass获取元类
sel_registerName("student_ClassMethod"));
  • 类方法 - 自己的元类有
  • 类方法 - 自己的元类没有 - 找元类的父类
  • 类方法 - 自己的元类没有 - 元类的父类没有 - 元类的父类的父类 - 根元类 - NSObject也没有 - 奔溃
  • 类方法 - 自己的元类没有 - 元类的父类没有 - 元类的父类的父类 - 根元类 - NSObject也没有,但是有对象方法(类方法会以实例方法的形式存在元类中)

方法查找流程

objc_msgSend(obj, SEL)

快速查找:

cache_getImp

  • 如果cacheHit缓存命中,表示找到了sel对应的imp,直接返回imp。
  • 如果JumpMiss缓存未命中,进入慢速查找

慢速查找:

lookUpImpOrForward(id obj, SEL sel, Class cls)

  • id obj:方法接受者(实例方法->实例对象,类方法->元类);SEL sel:方法名;Class cls:类
  • 查找方法:二分查找
  • 当前class命中,cache_fill添加缓存,然后return imp;
  • 当前class未命中,便利cls->superclass,重复步骤2
  • NSObject也未找到,进入消息转发objc_msgForward

image.png

动态方法决议

  1. _class_resolveMethod
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) { //判断类不是元类
        // 调用对象方法解析
        _class_resolveInstanceMethod(cls, sel, inst);
    } else {
        // 调用类方法解析
        _class_resolveClassMethod(cls, sel, inst);
        // 再次查找下方法,如果没有的话,就再转发一下resolveInstanceMethod方法
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

2. 实例方法_class_resolveInstanceMethod

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    
    //发送SEL_resolveInstanceMethod消息,
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    //再次查找类中是否sel方法,因为resolveInstanceMethod方法执行后可能动态进行添加了,resolver是不要进行消息转发了
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {


        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
  • lookUpImpOrNil:会调用lookupImpOrForward查找类、父类是否实现SEL_resolveInstanceMethod方法,并cache_fill
  • 再次lookUpImpOrNil,查找类中是否sel方法,因为resolveInstanceMethod方法执行后可能动态进行添加了,resolver是不要进行消息转发了
  • 结束动态方法决议,回到lookUpImpOrForward方法,将triedResolve = YES并goto retry重新查找缓存和方法列表
  1. 类方法 类方法与实例方法基本一致,不同的是在结束SEL_resolveClassMethod回到lookUpImpOrForward方法时,再次调用lookUpImpOrNil会查找sel的imp,若有imp则退出动态方法决议,若无则进入_class_resolveInstanceMethod
else {
        // 调用类方法解析
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }

4. 解决方法

  • 实例方法:重写+resolveInstanceMethod添加imp
  • 类方法:重写+resolveClassMethod往元类添加imp(实例方法存在类对象中,类方法存在元类对象中)

快速消息转发

forwardingTargetForSelector会返回一个对象,将自己处理不了的消息转发给该对象,询问该对象是否能够处理该消息。

- (id)forwardingTargetForSelector:(SEL)sel {

    if ([AKTeacher respondsToSelector:sel]) {
        return AKTeacher.class;
    }
    return nil;
}

慢速消息转发

如果快速转发还不能找到method,会来到慢速转发methodSignatureForSelector。

  1. -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
  • methodSignatureForSelector会返回一个方法签名NSMethodSignature
  • 根据NSMethodSignature创建NSInvocation对象。
  • NSInvocation对象作为参数传给forwardInvocation方法.
  1. -(void)forwardInvocation:(NSInvocation *)anInvocation
  • forwardInvocation方法类似于将消息当做事务堆放起来,方法内部将消息给能处理该消息的对象,就算不操作也不会崩溃,这里也是防崩溃的最后处理机会。
+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
  • 异常是在doesNotRecognizeSelector方法里面抛出的,所以我们重写forwardInvocation方法后,如果不在里面执行父类的方法,程序是不会崩溃的
// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

3. 慢速转发补救

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (NSSelectorFromString(@"saySomething") == aSelector) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 把不能够处理的方法,返回一个方法签名
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 给不同的方法,传递给不同的能够处理的类去处理
    SEL aSelector = [anInvocation selector];
    if ([[AKTeacher alloc] respondsToSelector:aSelector]) {
         // 此时 AKTeacher 能够处理 方法 aSelector
          [anInvocation invokeWithTarget:[AKTeacher alloc]];
    }
    else {
          [super forwardInvocation:anInvocation];
    }
}

消息转发总结

当开发者调用了未实现的方法,苹果提供了三个解决途径:

1.动态方法决议:Method resolution

  • runtime调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。
  • 如果你添加了函数并返回YES, 那运行时系统就会重启一次消息发送lookupimporforward的过程。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

2.快速转发:Fast forwarding

  • 如果目标对象实现了-forwardingTargetForSelector:,runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
  • 只要这个其他对象不是nii或者self,那运行时系统就会重启一次消息发送的过程。此时,发送消息的对象变成你返回的那个其他对象。
- (id)forwardingTargetForSelector:(SEL)aSelector;

这里叫fast,只是为了区别下一步的慢速转发。因为这一步不会创建新的对象,而Normal forwarding会创建一个NSInvocation对象。

3.慢速转发:Normal forwarding

这一步是runtime最后一次给你挽救的机会。

  • 先调用-methodSignatureForSelector返回一个方法签名NSMethodSignature
  • 根据NSMethodSignature创建NSInvocation对象。
  • NSInvocation对象作为参数传给-forwardInvocation方法,在其内部做消息处理。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

4.为什么_class_resolveInstanceMethod会调用调用两次?

methodSignatureForSelector之后,forwardInvocation之前 调用第二次_class_resolveInstanceMethod`

类方法会以实例方法的形式存在元类中。这里的cls是元类,ins是类对象。在_class_resolveInstanceMethod会沿着isa走位图((类方法 -> [元类 - 根元类 - NSObject]))遍历查找有没有以实例方法形式存储的类方法