IOS知识点汇总(二)

IOS知识点汇总(二)


苹果官方 OpenSource中下载源码

底层原理

1. Runtime

1.1.1 一个objc对象的isa的指针指向什么?有什么作用?
  • 指向他的类对象,从而可以找到对象上的方法
  • 下图很好的描述了对象,类,元类之间的关系:
    isa指针
  • 图中实线是 super_class指针,虚线是isa指针
  1. Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
  2. 每个Class都有一个isa指针指向唯一的Meta class
  3. Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
  4. 每个Meta class的isa指针都指向Root class (meta)。
1.1.2 一个 NSObject 对象占用多少内存空间?
  • 受限于内存分配的机制,一个 NSObject对象都会分配 16byte 的内存空间。

  • 但是实际上在 64位 下,只使用了 8byte;

  • 在32位下,只使用了 4byte

  • 一个 NSObject 实例对象成员变量所占的大小,实际上是 8 字节

#import <Objc/Runtime>
Class_getInstanceSize([NSObject Class])
  • 本质是
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
  • 获取 Obj-C 指针所指向的内存的大小,实际上是16 字节
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj); 
  • 对象在分配内存空间时,会进行内存对齐,所以在 iOS 中,分配内存空间都是 16字节 的倍数。
  • 可以通过以下网址 :openSource.apple.com/tarballs 来查看源代码。
1.1.3 一下对 class_rw_t 的理解?
  • rw代表可读可写。
  • ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中:
// 可读可写
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro; // 指向只读的结构体,存放类初始信息

    /*
     这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
     methods中,存储 method_list_t ----> method_t
     二维数组,method_list_t --> method_t
     这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
     */
    method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
    property_array_t properties; // 属性列表
    protocol_array_t protocols; //协议列表

    Class firstSubclass;
    Class nextSiblingClass;
    
    //...
    }
1.1.4 说一下对 class_ro_t 的理解?
  • 存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
struct class_ro_t {  
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;

    const uint8_t * ivarLayout;

    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};
1.1.5 说一下对 isa 指针的理解
  • 说一下对 isa 指针的理解, 对象的isa 指针指向哪里?isa 指针有哪两种类型?
  1. isa 等价于 is kind of
    实例对象 isa 指向类对象
    类对象指 isa 向元类对象
    元类对象的 isa 指向元类的基类
  2. isa 有两种类型
    纯指针,指向内存地址
    NON_POINTER_ISA,除了内存地址,还存有一些其他信息
  • 在Runtime源码查看isa_t是共用体。简化结构如下:
union isa_t 
{
    Class cls;
    uintptr_t bits;
    # if __arm64__ // arm64架构
#   define ISA_MASK        0x0000000ffffffff8ULL //用来取出33位内存地址使用(&)操作
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1; //0:代表普通指针,1:表示优化过的,可以存储更多信息。
        uintptr_t has_assoc         : 1; //是否设置过关联对象。如果没设置过,释放会更快
        uintptr_t has_cxx_dtor      : 1; //是否有C++的析构函数
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 内存地址值
        uintptr_t magic             : 6; //用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1; //是否有被弱引用指向过
        uintptr_t deallocating      : 1; //是否正在释放
        uintptr_t has_sidetable_rc  : 1; //引用计数器是否过大无法存储在ISA中。如果为1,那么引用计数会存储在一个叫做SideTable的类的属性中
        uintptr_t extra_rc          : 19; //里面存储的值是引用计数器减1

#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__ // arm86架构,模拟器是arm86
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        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;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };

# else
#   error unknown architecture for packed isa
# endif

}
1.1.6 说一下 Runtime 的方法缓存?存储的形式、数据结构以及查找的过程?

cache_t增量扩展的哈希表结构。哈希表内部存储的 bucket_t。
bucket_t 中存储的是 SEL 和 IMP的键值对。
如果是有序方法列表,采用二分查找
如果是无序方法列表,直接遍历查找

  • cache_t结构体
// 缓存曾经调用过的方法,提高查找速率
struct cache_t {
    struct bucket_t *_buckets; // 散列表
    mask_t _mask; //散列表的长度 - 1
    mask_t _occupied; // 已经缓存的方法数量,散列表的长度使大于已经缓存的数量的。
    //...
}
struct bucket_t {
    cache_key_t _key; //SEL作为Key @selector()
    IMP _imp; // 函数的内存地址
    //...
}
  • 散列表查找过程,在objc-cache.mm文件中
// 查询散列表,k
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0); // 断言

    bucket_t *b = buckets(); // 获取散列表
    mask_t m = mask(); // 散列表长度 - 1
    mask_t begin = cache_hash(k, m); // & 操作
    mask_t i = begin; // 索引值
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
    // i 的值最大等于mask,最小等于0。

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}
  • 上面是查询散列表函数,其中cache_hash(k, m)是静态内联方法,将传入的key和mask进行&操作返回uint32_t索引值。do-while循环查找过程,当发生冲突cache_next方法将索引值减1。
1.1.7 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
  • 无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。
  • 详情:
  1. 调用 -release :引用计数变为零
    对象正在被销毁,生命周期即将结束.
    不能再有新的 __weak 弱引用,否则将指向 nil.
    调用 [self dealloc]
  2. 父类调用 -dealloc
    继承关系中最直接继承的父类再调用 -dealloc
    如果是 MRC 代码 则会手动释放实例变量们(iVars)
    继承关系中每一层的父类 都再调用 -dealloc
  3. NSObject 调 -dealloc
    只做一件事:调用 Objective-C runtime 中object_dispose() 方法
  4. 调用 object_dispose()
    为 C++ 的实例变量们(iVars)调用 destructors
    为 ARC 状态下的 实例变量们(iVars) 调用 -release
    解除所有使用 runtime Associate方法关联的对象
    解除所有 __weak 引用
    调用 free()
1.1.8 实例对象的数据结构?
  • 具体可以参看 Runtime 源代码,在文件 objc-private.h 的第 127-232 行。
struct objc_object {
    isa_t isa;
    //...
}
  • 本质上 objc_object 的私有属性只有一个 isa 指针。指向 类对象 的内存地址。
1.1.9 什么是method swizzling(俗称黑魔法)
  • 简单说就是进行方法交换

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。

换方法的几种实现方式

  1. 利用 method_exchangeImplementations 交换两个方法的实现
  2. 利用 class_replaceMethod替换方法的实现
  3. 利用 method_setImplementation 来直接设置某个方法的IMP

method swizzling

1.1.10 什么时候会报unrecognized selector的异常?

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。

1.1.11 如何给 Category 添加属性?关联对象以什么形式进行存储?

查看的是 关联对象 的知识点。
详细的说一下 关联对象。
关联对象 以哈希表的格式,存储在一个全局的单例中。

@interface NSObject (Extension)

@property (nonatomic,copy  ) NSString *name;

@end


@implementation NSObject (Extension)

- (void)setName:(NSString *)name {
    
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}


- (NSString *)name {
    
    return objc_getAssociatedObject(self,@selector(name));
}

@end
1.1.12 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
  • 不能向编译后得到的类中增加实例变量;

因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.所以不能向存在的类中添加实例变量。

  • 能向运行时创建的类中添加实例变量;

运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.

1.1.13 类对象的数据结构?
  • 具体可以参看 Runtime 源代码。类对象就是 objc_class。
struct objc_class : objc_object {
    // Class ISA;
    Class superclass; //父类指针
    cache_t cache;             // 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(); // &FAST_DATA_MASK 获取地址值
    }
  • 它的结构相对丰富一些。继承自objc_object结构体,所以包含isa指针

isa:指向元类
superClass: 指向父类
Cache: 方法的缓存列表
data: 顾名思义,就是数据。是一个被封装好的 class_rw_t 。

1.1.14 runtime如何通过selector找到对应的IMP地址?

每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.

1.1.15 如何实现weak变量的自动置nil?知道SideTable吗?

runtime 对注册的类会进行布局,对于 weak 修饰的对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

  • 更细一点的回答:
  1. 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
  2. 添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
  3. 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
  4. SideTable结构体是负责管理类的引用计数表和weak表
  • 详解:参考自《Objective-C高级编程》一书
  1. 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
{
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}
  • 当我们初始化一个weak变量时,runtime会调用 NSObject.mm 中的objc_initWeak函数。
// 编译器的模拟代码
 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用计数变为0,变量作用域结束*/
 objc_destroyWeak(&obj1);
  • 通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。
  1. 添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
  • objc_initWeak函数将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak函数。
obj1 = 0obj_storeWeak(&obj1, obj);
  • 也就是说:weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)
  • 然后obj_destroyWeak函数将0(nil)作为参数,调用objc_storeWeak函数。
objc_storeWeak(&obj1, 0);
  • 前面的源代码与下列源代码相同。
// 编译器的模拟代码
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用计数变为0,被置nil ... */
objc_storeWeak(&obj1, 0);
  • objc_storeWeak函数把第二个参数的赋值对象(obj)的内存地址作为键值,将第一个参数__weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从weak表中删除。
  • 由于一个对象可同时赋值给多个附有__weak修饰符的变量中,所以对于一个键值,可注册多个变量的地址。
  • 可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。
  1. 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
  • 当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?

当释放对象时,其基本流程如下:

  1. 调用objc_release
  2. 因为对象的引用计数为0,所以执行dealloc
  3. 在dealloc中,调用了_objc_rootDealloc函数
  4. 在_objc_rootDealloc中,调用了object_dispose函数
  5. 调用objc_destructInstance
  6. 最后调用objc_clear_deallocating
  • 对象被释放时调用的objc_clear_deallocating函数:
  1. 从weak表中获取废弃对象的地址为键值的记录
  2. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
  3. 将weak表中该记录删除
  4. 从引用计数表中删除废弃对象的地址为键值的记录
  • 其实Weak表是一个hash(哈希)表,Key是weak所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
1.1.16 objc中向一个nil对象发送消息将会发生什么?
  • 如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会崩溃。
  • 详解:
  1. 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);
  2. 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;
  3. 如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
  4. 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。
1.1.17 objc在向一个对象发送消息时,发生了什么?

objc在向一个对象发送消息时,runtime会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到 ,就去执行它的实现IMP

1.1.18 isKindOfClass 与 isMemberOfClass
  • 下面代码输出什么?
@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
        NSLog(@"%d %d %d %d", res1, res2, res3, res4);
    }
    return 0;
}

答案:1000

  • 详解:
    isa指针
  1. isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于meta classsuper class,不等再继续取super class,如此循环下去。
  2. [NSObject class]执行完之后调用isKindOfClass,第一次判断先判断NSObjectNSObjectmeta class是否相等,之前讲到meta class的时候放了一张很详细的图,从图上我们也可以看出,NSObjectmeta class与本身不等。接着第二次循环判断NSObjectmeta classsuperclass是否相等。还是从那张图上面我们可以看到:Root class(meta)superclass就是 Root
    class(class),也就是NSObject本身。所以第二次循环相等,于是第一行res1输出应该为YES。
  3. 同理,[Sark class]执行完之后调用isKindOfClass,第一次for循环,Sark的Meta Class[Sark class]不等,第二次for循环,Sark Meta Classsuper class 指向的是 NSObject Meta Class, 和Sark Class不相等。第三次for循环,NSObject Meta Class的super class指向的是NSObject Class,和 Sark Class 不相等。第四次循环,NSObject Classsuper class 指向 nil, 和 Sark Class不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO。
  4. isMemberOfClass的源码实现是拿到自己的isa指针和自己比较,是否相等。
  5. 第二行isa 指向 NSObjectMeta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都输出NO。
1.1.19 Category 在编译过后,是在什么时机与原有的类合并到一起的?
  1. 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init。
  2. 然后会 map_images。
  3. 接下来调用 map_images_nolock。
  4. 再然后就是 read_images,这个方法会读取所有的类的相关信息。
  5. 最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。
    在 reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传>6. 入 Class 和 Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体中。
1.1.20 Category 有哪些用途?

给系统类添加方法、属性(需要关联对象)。
对某个类大量的方法,可以实现按照不同的名称归类。

1.1.21 Category 的实现原理?
  • Category被添加在了 class_rw_t 的对应结构里。
  • Category 实际上是 Category_t 的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的Category,添加了同一个方法,执行的实际上是最后一个。
  • 拿方法列表举例,实际上是一个二维的数组。
  • Category 如果翻看源码的话就会知道实际上是一个 _catrgory_t 的结构体。
  • 例如我们在程序中写了一个 Nsobject+Tools 的分类,那么被编译为 C++ 之后,实际上是:
static struct _catrgory_t _OBJC_$_CATEGORY_NSObject_$_Tools __attribute__ ((used,section),("__DATA,__objc__const"))
{
    // name
    // class
    // instance method list
    // class method list
    // protocol list
    // properties
}
  • Category 在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 RuntimeCategory 和原来的类才会合并到一起。
  • mememovememcpy:这俩方法是位移、复制,简单理解就是原有的方法移动到最后,根根新开辟的控件,把前面的位置留给分类,然后分类中的方法,按照倒序依次插入,可以得出的结论:越晚参与编译的分类,里面的方法才是生效的那个。
1.1.22 _objc_msgForward函数是做什么的
  • _objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
  • 详解:

_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:

  1. List itemresolveInstanceMethod:方法 (或resolveClassMethod:)。
  2. List itemforwardingTargetForSelector:方法
  3. List itemmethodSignatureForSelector:方法
  4. List itemforwardInvocation:方法
  5. List itemdoesNotRecognizeSelector: 方法
1.1.23 [self class] 与 [super class]
  • 下面的代码输出什么?
@implementation Son : Father
   - (id)init
   {
       self = [super init];
       if (self) {
           NSLog(@"%@", NSStringFromClass([self class]));
           NSLog(@"%@", NSStringFromClass([super class]));
       }
       return self;
   }
   @end

输出结果:

NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son

  • 因为,Son 及 Father 都没有实现 -(Class)calss 方法,所以这里所有的调用最终都会找到基类 NSObject 中,并且在其中找到 -(Class)calss 方法。那我们需要了解的就是在 NSObject 中这个方法的实现了。
  • 在 NSObject.mm 中可以找到 -(Class)class 的实现:
- (Class)class {
    return object_getClass(self);
}
  • 在 objc_class.mm 中找到 object_getClass 的实现:
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
  • 上面的方法定义可以去苹果官方OpenSource中下载源码
  • 可以看到,最终这个方法返回的是,调用这个方法的 objc 的 isa 指针。那我们只需要知道在题干中的代码里面最终是谁在调用 -(Class)class 方法就可以找到答案了。
  • 接下来,我们利用 clang -rewrite-objc 命令,将题干的代码转化为如下代码:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8k_cgm28r0d0bz94xnnrr606rf40000gn_T_Car_3f2069_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8k_cgm28r0d0bz94xnnrr606rf40000gn_T_Car_3f2069_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Car"))}, sel_registerName("class"))));
  • 从上方可以得出,调用 [Father class] 的时候,本质是在调用
objc_msgSendSuper(struct objc_super *super, SEL op, ...)
  • struct objc_super 的定义如下:
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
  • 从定义可以得知:当利用 super 调用方法时,只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用,是去父类找实现,super 仅仅是一个编译指示器。但是消息的接收者 receiver 依然是self。最终在 NSObject 获取 isa 指针的时候,获取到的依旧是 self 的 isa,所以,我们得到的结果是:Son。

  • 深入解答:

这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。

  1. self 是类的隐藏参数,指向当前调用方法的这个类的实例;
  2. super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者。不同点在于:super 会告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
  3. 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。
  4. 在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
  • objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class
  • objc_msgSendSuper的工作原理应该是这样的:
  1. objc_super结构体指向的superClass父类的方法列表开始查找selector
  2. 找到后以objc->receiver去调用父类的这个selector
  3. 注意,最后的调用者是objc->receiver,而不是super_class
  • 那么objc_msgSendSuper最后就转变成:
// 注意这里是从父类开始msgSend,而不是从本类开始
objc_msgSend(objc_super->receiver, @selector(class))

/// Specifies an instance of a class.  这是类的一个实例
    __unsafe_unretained id receiver;   

// 由于是实例调用,所以是减号方法
- (Class)class {
    return object_getClass(self);
}
  • 由于找到了父类NSObject里面的class方法的IMP,又因为传入的入参objc_super->receiver = selfself就是son,调用class,所以父类的方法class执行IMP之后,输出还是son,最后输出两个都一样,都是输出son
1.1.24

2. Runloop

2.1.1 为什么 NSTimer 有时候不好使?

因为创建的 NSTimer 默认是被加入到了 defaultMode,所以当 Runloop 的 Mode 变化时,当前的 NSTimer 就不会工作了。

2.1.2 AFNetworking 中如何运用 Runloop?
  • AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
  • RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking[runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
  • 当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
2.1.3 autoreleasePool 在何时被释放?
  1. App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
  2. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  3. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
  4. 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
2.1.4 PerformSelector 的实现原理?

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

2.1.5 PerformSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?
  • PerformSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?

不起作用,子线程默认没有 Runloop,也就没有 Timer。
解决的办法是可以使用 GCD 来实现:Dispatch_after

2.1.6 RunLoop的Mode
  • 关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。
  • 当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的
  • 总共是有五种CFRunLoopMode:
模式说明
kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案
2.1.7 RunLoop的实现机制
  • 对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。
  • RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。
  • 即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop
  • Mach消息发送机制

大致逻辑为:

  1. 通知观察者 RunLoop 即将启动。
  2. 通知观察者即将要处理Timer事件。
  3. 观察者即将要处理source0事件。
  4. 处理source0事件。
  5. 如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
  6. 通知观察者线程即将进入休眠状态。
  7. 将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。
    一个基于 port 的Source1 的事件(图里应该是source0)。
    一个 Timer 到时间了。
    RunLoop 自身的超时时间到了。
    被其他调用者手动唤醒。
  8. 通知观察者线程将被唤醒。
  9. 处理唤醒时收到的事件。
    如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
    如果输入源启动,传递相应的消息。
    如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
  10. 通知观察者RunLoop结束。
2.1.8 RunLoop和线程
  • 线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
  • 自己创建的线程默认是没有开启RunLoop的
  • 怎么创建一个常驻线程?

1、为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop)
2、向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)
3、启动该RunLoop

 @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
  • 输出下边代码的执行顺序?
NSLog(@"1");

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
    NSLog(@"2");

    [self performSelector:@selector(test) withObject:nil afterDelay:10];
    
    NSLog(@"3");
});

NSLog(@"4");

- (void)test
{
    
    NSLog(@"5");
}
  1. 答案是1423,test方法并不会执行。
  2. 原因是如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的RunLoop中。也就是如果当前线程没有开启RunLoop,该方法会失效。
    那么我们改成:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"2");
        
        [self performSelector:@selector(test) withObject:nil afterDelay:10];
        
        [[NSRunLoop currentRunLoop] run];
  
        NSLog(@"3");
    });
  • 怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。
我们就可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI

[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
2.1.9 RunLoop的数据结构
  • NSRunLoop(Foundation)是CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
  • RunLoop 相关的主要涉及五个类:

CFRunLoop:RunLoop对象
CFRunLoopMode:运行模式
CFRunLoopSource:输入源/事件源
CFRunLoopTimer:定时源
CFRunLoopObserver:观察者

  • CFRunLoop

由pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成

  • CFRunLoopMode

由name、source0、source1、observers、timers构成

  • CFRunLoopSource

分为source0和source1两种

  1. source0:
    即非基于port的,也就是用户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到用户态
  2. source1:
    基于port的,包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。
    具备唤醒线程的能力
  • CFRunLoopTimer

基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

  • CFRunLoopObserver

监听以下时间点:CFRunLoopActivity

  1. kCFRunLoopEntry
    RunLoop准备启动
  2. kCFRunLoopBeforeTimers
    RunLoop将要处理一些Timer相关事件
  3. kCFRunLoopBeforeSources
    RunLoop将要处理一些Source事件
  4. kCFRunLoopBeforeWaiting
    RunLoop将要进行休眠状态,即将由用户态切换到内核态
  5. kCFRunLoopAfterWaiting
    RunLoop被唤醒,即从内核态切换到用户态后
  6. kCFRunLoopExit
    RunLoop退出
  7. kCFRunLoopAllActivities
    监听所有状态
  • 各数据结构之间的联系

线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

2.1.10 RunLoop概念
  • RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。

1、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
2、有消息需要处理时,立刻被唤醒,由内核态切换到用户态.

  • 为什么main函数不会退出?
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
  • UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环)
//无限循环代码模式(伪代码)
int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
        // ......
    } while (running);

    return 0;
}
  • UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。
2.1.11. RunLoop与NSTimer
  • 一个比较常见的问题:滑动tableView时,定时器还会生效吗?

默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。

  • 怎么去解决这个问题呢?

把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时添加到UITrackingRunLoopMode和kCFRunLoopDefaultMode上。
那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes了

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Timer就被添加到多个mode上,这样即使RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件

2.1.12 讲一下 Observer ?
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};
2.1.13 解释一下 NSTimer
  1. NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
  2. 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
  3. CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动 TableView 时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。
2.1.14 解释一下 事件响应 的过程?
  1. 苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
  2. 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
  3. _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
2.1.15 解释一下 手势识别 的过程?
  1. 当上面的 _UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
  2. 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer 的回调。
  3. 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
2.1.16 什么是异步绘制?
  • 异步绘制,就是可以在子线程把需要绘制的图形,提前在子线程处理好。将准备好的图像数据直接返给主线程使用,这样可以降低主线程的压力。
  • 异步绘制的过程:

要通过系统的 [view.delegate displayLayer:] 这个入口来实现异步绘制。

  1. 代理负责生成对应的 Bitmap
  2. 设置该 Bitmap 为 layer.contents 属性的值。
2.1.17 利用 runloop 解释一下页面的渲染的过程?
  1. 当我们调用 [UIView setNeedsDisplay] 时,这时会调用当前 View.layer 的 [view.layer setNeedsDisplay]方法。
  2. 这等于给当前的 layer 打上了一个脏标记,而此时并没有直接进行绘制工作。而是会到当前的 Runloop 即将休眠,也就是 beforeWaiting 时才会进行绘制工作。
  3. 紧接着会调用 [CALayer display],进入到真正绘制的工作。CALayer 层会判断自己的 delegate 有没有实现异步绘制的代理方法 displayer:,这个代理方法是异步绘制的入口,如果没有实现这个方法,那么会继续进行系统绘制的流程,然后绘制结束。
  4. CALayer 内部会创建一个 Backing Store,用来获取图形上下文。接下来会判断这个 layer 是否有 delegate。
  5. 如果有的话,会调用 [layer.delegate drawLayer:inContext:],并且会返回给我们 [UIView DrawRect:] 的回调,让我们在系统绘制的基础之上再做一些事情。
  6. 如果没有 delegate,那么会调用 [CALayer drawInContext:]。
  7. 以上两个分支,最终 CALayer 都会将位图提交到 Backing Store,最后提交给 GPU。
  8. 至此绘制的过程结束。

3. Block

3.1.1 Block的几种形式
  • Block的几种形式
  1. 分为全局Block(_NSConcreteGlobalBlock)、栈Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三种形式
  2. 其中栈Block存储在栈(stack)区,堆Block存储在堆(heap)区,全局Block存储在已初始化数据(.data)区
3.1.1.1 不使用外部变量的block是全局block
NSLog(@"%@",[^{
        NSLog(@"globalBlock");
    } class]);
输出:
__NSGlobalBlock__
3.1.1.2 使用外部变量并且未进行copy操作的block是栈block
NSInteger num = 10;
    NSLog(@"%@",[^{
        NSLog(@"stackBlock:%zd",num);
    } class]);
输出:
__NSStackBlock__
  • 日常开发常用于这种情况:
[self testWithBlock:^{
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();

    NSLog(@"%@",[block class]);
}
3.1.1.3 对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block
  • 比如堆1中的全局进行copy操作,即赋值:
void (^globalBlock)(void) = ^{
        NSLog(@"globalBlock");
    };

 NSLog(@"%@",[globalBlock class]);

输出:
__NSGlobalBlock__
  • 而对3.1.1.2中的栈block进行赋值操作:
NSInteger num = 10;

void (^mallocBlock)(void) = ^{

        NSLog(@"stackBlock:%zd",num);
    };

NSLog(@"%@",[mallocBlock class]);

输出:
__NSMallocBlock__
  • 对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block,例如:
[self testWithBlock:^{
    
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block
{
    block();
    
    dispatch_block_t tempBlock = block;
    
    NSLog(@"%@,%@",[block class],[tempBlock class]);
}

输出:

__NSStackBlock__,__NSMallocBlock__
  • 即如果对栈Block进行copy,将会copy到堆区,对堆Block进行copy,将会增加引用计数,对全局Block进行copy,因为是已经初始化的,所以什么也不做。
  • 另外,__block变量在copy时,由于__forwarding的存在,栈上的__forwarding指针会指向堆上的__forwarding变量,而堆上的__forwarding指针指向其自身,所以,如果对__block的修改,实际上是在修改堆上的__block变量。
  • 即__forwarding指针存在的意义就是,无论在任何内存位置,都可以顺利地访问同一个__block变量
  • 另外由于block捕获的__block修饰的变量会去持有变量,那么如果用__block修饰self,且self持有block,并且block内部使用到__block修饰的self时,就会造成多循环引用,即self持有block,block 持有__block变量,而__block变量持有self,造成内存泄漏。
    比如:
__block typeof(self) weakSelf = self;
    
    _testBlock = ^{
        
        NSLog(@"%@",weakSelf);
    };
    
    _testBlock();

如果要解决这种循环引用,可以主动断开__block变量对self的持有,即在block内部使用完weakself后,将其置为nil,但这种方式有个问题,如果block一直不被调用,那么循环引用将一直存在。
所以,我们最好还是用__weak来修饰self

3.1.2 Block变量截获
3.1.2.1 局部变量截获 是值截获
NSInteger num = 3;
    
    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
        
        return n*num;
    };
    
    num = 1;
    
    NSLog(@"%zd",block(2));

这里的输出是6而不是2,原因就是对局部变量num的截获是值截获。
同样,在block里如果修改变量num,也是无效的,甚至编译器会报错。

NSMutableArray * arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];
    
    void(^block)(void) = ^{
        
        NSLog(@"%@",arr);//局部变量
        
        [arr addObject:@"4"];
    };
    
    [arr addObject:@"3"];
    
    arr = nil;
    
    block();

打印为1,2,3
局部对象变量也是一样,截获的是值,而不是指针,在外部将其置为nil,对block没有影响,而该对象调用方法会影响.

3.1.2.2 局部静态变量截获 是指针截获
 static  NSInteger num = 3;
    
    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
        
        return n*num;
    };
    
    num = 1;
    
    NSLog(@"%zd",block(2));

输出为2,意味着num = 1这里的修改num值是有效的,即是指针截获。
同样,在block里去修改变量m,也是有效的。

3.1.2.3 全局变量,静态全局变量截获:不截获,直接取值
  • 我们同样用clang编译看下结果。
static NSInteger num3 = 300;

NSInteger num4 = 3000;

- (void)blockTest
{
    NSInteger num = 30;
    
    static NSInteger num2 = 3;
    
    __block NSInteger num5 = 30000;
    
    void(^block)(void) = ^{
        
        NSLog(@"%zd",num);//局部变量
        
        NSLog(@"%zd",num2);//静态变量
        
        NSLog(@"%zd",num3);//全局变量
        
        NSLog(@"%zd",num4);//全局静态变量
        
        NSLog(@"%zd",num5);//__block修饰变量
    };
    
    block();
}
  • 编译后
struct __WYTest__blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __WYTest__blockTest_block_desc_0* Desc;
  NSInteger num;//局部变量
  NSInteger *num2;//静态变量
  __Block_byref_num5_0 *num5; // by ref//__block修饰变量
  __WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • impl.isa = &_NSConcreteStackBlock;这里注意到这一句,即说明该block是栈block)
    可以看到局部变量被编译成值形式,而静态变量被编成指针形式,全局变量并未截获。而__block修饰的变量也是以指针形式截获的,并且生成了一个新的结构体对象:
struct __Block_byref_num5_0 {
  void *__isa;
__Block_byref_num5_0 *__forwarding;
 int __flags;
 int __size;
 NSInteger num5;
};

该对象有个属性:num5,即我们用__block修饰的变量。
这里__forwarding是指向自身的(栈block)。
一般情况下,如果我们要对block截获的局部变量进行赋值操作需添加__block
修饰符,而对全局变量,静态变量是不需要添加__block修饰符的。
另外,block里访问self或成员变量都会去截获self。

3.1.3 什么是Block?
  • 什么是Block?
    Block是将函数及其执行上下文封装起来的对象。

比如:

NSInteger num = 3;
    
    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
        
        return n*num;
    };
    
    block(2);
  • 通过clang -rewrite-objc WYTest.m命令编译该.m文件,发现该block被编译成这个形式:
 NSInteger num = 3;

    NSInteger(*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__WYTest__blockTest_block_impl_0((void *)__WYTest__blockTest_block_func_0, &__WYTest__blockTest_block_desc_0_DATA, num));

    ((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);
  • 其中WYTest是文件名,blockTest是方法名,这些可以忽略。
    其中__WYTest__blockTest_block_impl_0结构体为
struct __WYTest__blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __WYTest__blockTest_block_desc_0* Desc;
  NSInteger num;
  __WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • __block_impl结构体为
struct __block_impl {
  void *isa;//isa指针,所以说Block是对象
  int Flags;
  int Reserved;
  void *FuncPtr;//函数指针
};
  • block内部有isa指针,所以说其本质也是OC对象
    block内部则为:
static NSInteger __WYTest__blockTest_block_func_0(struct __WYTest__blockTest_block_impl_0 *__cself, NSInteger n) {
  NSInteger num = __cself->num; // bound by copy


        return n*num;
    }
  • 所以说 Block是将函数及其执行上下文封装起来的对象
    既然block内部封装了函数,那么它同样也有参数和返回值。

4. GCD

5. 多线程

5.1.1 进程、线程
  • 进程
  1. 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.
  2. 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.
  3. 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源
  • 线程
  1. 程序执行流的最小单元,线程是进程中的一个实体.
  2. 一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程
  • 进程和线程的关系
  1. 线程是进程的执行单元,进程的所有任务都在线程中执行
  2. 线程是 CPU 分配资源和调度的最小单位
  3. 一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
  4. 同一个进程内的线程共享进程资源
5.1.2 多进程、多线程
5.1.2.1 多进程
  • 打开mac的活动监视器,可以看到很多个进程同时运行
  1. 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。
  2. 进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身;所有由用户启动的进程都是用户进程。进程是操作系统进行资源分配的单位。
  3. 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。
5.1.2.2 多线程
  • 1.同一时间,CPU只能处理1条线程,只有1条线程在执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象

  • 2.如果线程非常非常多,CPU会在N多线程之间调度,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)

  • 3.多线程的优点:
    能适当提高程序的执行效率
    能适当提高资源利用率(CPU、内存利用率)

  • 4.多线程的缺点:
    开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
    线程越多,CPU在调度线程上的开销就越大
    程序设计更加复杂:比如线程之间的通信、多线程的数据共享

5.1.3 任务、队列
5.1.3.1 任务
  • 就是执行操作的意思,也就是在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)

同步(Sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。

异步(Async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务

5.1.3.2 队列
  • 队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
    在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

  • 串行队列(Serial Dispatch Queue):

同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的

  • 并发队列(Concurrent Dispatch Queue):

同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有在异步(dispatch_async)函数下才有效

5.1.4 iOS中的多线程
  • 主要有三种:NSThread、NSoperationQueue、GCD
5.1.4.1 NSThread:轻量级别的多线程技术
  • 是我们自己手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造器方式它就会自动启动。只要是我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是参数"];
    // 当使用初始化方法出来的主线程需要start启动
    [thread start];
    // 可以为开辟的子线程起名字
    thread.name = @"NSThread线程";
    // 调整Thread的权限 线程权限的范围值为0 ~ 1 。越大权限越高,先执行的概率就会越高,由于是概率,所以并不能很准确的的实现我们想要的执行顺序,默认值是0.5
    thread.threadPriority = 1;
    // 取消当前已经启动的线程
    [thread cancel];
    // 通过遍历构造器开辟子线程
    [NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方式"];
  • performSelector…只要是NSObject的子类或者对象都可以通过调用方法进入子线程和主线程,其实这些方法所开辟的子线程也是NSThread的另一种体现方式。
    在编译阶段并不会去检查方法是否有效存在,如果不存在只会给出警告
//在当前线程。延迟1s执行。响应了OC语言的动态性:延迟到运行时才绑定方法
        [self performSelector:@selector(aaa) withObject:nil afterDelay:1];
      // 回到主线程。waitUntilDone:是否将该回调方法执行完在执行后面的代码,如果为YES:就必须等回调方法执行完成之后才能执行后面的代码,说白了就是阻塞当前的线程;如果是NO:就是不等回调方法结束,不会阻塞当前线程
        [self performSelectorOnMainThread:@selector(aaa) withObject:nil waitUntilDone:YES];
      //开辟子线程
        [self performSelectorInBackground:@selector(aaa) withObject:nil];
      //在指定线程执行
        [self performSelector:@selector(aaa) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES]
  • 需要注意的是:如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的Runloop中。也就是如果当前线程没有开启runloop,该方法会失效。在子线程中,需要启动runloop(注意调用顺序)
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];
  • 而performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行.
5.1.4.2 GCD 对比 NSOprationQueue
  • 我们要明确NSOperationQueue与GCD之间的关系
    GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象。

1、GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便
2、GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
3、NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂
4、NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)

  • 实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选
    如果考虑异步操作之间的事务性,顺序行,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持
  • 不论是GCD还是NSOperationQueue,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销
5.1.5 GCD—队列

iOS中,有GCD、NSOperation、NSThread等几种多线程技术方案。

  • 而GCD共有三种队列类型:
    main queue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列。

  • global queue:全局队列是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列。调用dispath_get_global_queue并传入优先级来访问队列。

  • 自定义队列:通过函数dispatch_queue_create创建的队列。

5.1.6 死锁
  • 死锁就是队列引起的循环等待
  1. 一个比较常见的死锁例子:主队列同步
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"deallock");
    });
    // Do any additional setup after loading the view, typically from a nib.
}

在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。
同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。
而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。

解决方法:
想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。

  1. 下边的代码也会造成死锁
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
       
        dispatch_sync(serialQueue, ^{
            
            NSLog(@"deadlock");
        });
    });

外面的函数无论是同步还是异步都会造成死锁。
这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁

解决方法:
解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决

如下面代码是不死锁的:

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(serialQueue, ^{
       
        dispatch_sync(serialQueue2, ^{
            
            NSLog(@"deadlock");
        });
    });

这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。

5.1.7 GCD任务执行顺序
  1. 串行队列先异步后同步
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"1");
    
    dispatch_async(serialQueue, ^{
        
         NSLog(@"2");
    });
    
    NSLog(@"3");
    
    dispatch_sync(serialQueue, ^{
        
        NSLog(@"4");
    });
    
    NSLog(@"5");

打印顺序是:13245

原因是:

  1. 首先先打印1
  2. 接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3
  3. 然后是任务4,将任务4添加至串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5
  4. 所以最终顺序就是13245。
  5. 这里的任务4在主线程中执行,而任务2在子线程中执行。
    如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行(可以添加多个任务看效果)
  • performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        [self performSelector:@selector(test:) withObject:nil afterDelay:0];
    });
  • 这里的test方法是不会去执行的,原因在于
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

这个方法要创建提交任务到runloop上的,而gcd底层创建的线程是默认没有开启对应runloop的,所有这个方法就会失效。
而如果将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行(将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也是会去执行的)。

5.1.8 dispatch_barrier_async
  1. 怎么用GCD实现多读单写?
  1. 多读单写的意思就是:可以多个读者同时读取数据,而在读的时候,不能取写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的。
  2. 这里的写处理就是通过栅栏的形式去写。
  3. 就可以用dispatch_barrier_sync(栅栏函数)去实现
  1. dispatch_barrier_sync的用法:
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i = 0; i < 10; i++) {
        
        dispatch_sync(concurrentQueue, ^{
            
            NSLog(@"%zd",i);
        });
    }
    
    dispatch_barrier_sync(concurrentQueue, ^{
       
        NSLog(@"barrier");
    });
    
    for (NSInteger i = 10; i < 20; i++) {
        
        dispatch_sync(concurrentQueue, ^{
            
            NSLog(@"%zd",i);
        });
    }

这里的dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。
从打印上看,任务0-9和任务任务10-19因为是异步并发的原因,彼此是无序的。而由于栅栏函数的存在,导致顺序必然是先执行任务0-9,再执行栅栏函数,再去执行任务10-19。
dispatch_barrier_sync: Submits a barrier block object for execution and waits until that block completes.(提交一个栅栏函数在执行中,它会等待栅栏函数执行完)

dispatch_barrier_async: Submits a barrier block for asynchronous execution and returns immediately.(提交一个栅栏函数在异步执行中,它会立马返回)
而dispatch_barrier_sync和dispatch_barrier_async的区别也就在于会不会阻塞当前线程
比如,上述代码如果在dispatch_barrier_async后随便加一条打印,则会先去执行该打印,再去执行任务0-9和栅栏函数;而如果是dispatch_barrier_sync,则会在任务0-9和栅栏函数后去执行这条打印。

  1. 可以这样设计多读单写:
- (id)readDataForKey:(NSString *)key
{
    __block id result;
    
    dispatch_sync(_concurrentQueue, ^{
       
        result = [self valueForKey:key];
    });
    
    return result;
}

- (void)writeData:(id)data forKey:(NSString *)key
{
    dispatch_barrier_async(_concurrentQueue, ^{
       
        [self setValue:data forKey:key];
    });
}
5.1.9 dispatch_group_async
5.1.10 Dispatch Semaphore
5.1.11 延时函数(dispatch_after)
5.1.12 使用dispatch_once实现单例
5.1.13 NSOperationQueue的优点
5.1.14 NSOperation和NSOperationQueue
5.1.15 NSThread+runloop实现常驻线程
5.1.16 自旋锁与互斥锁

6. OC相关

6.1.1 KVC(Key-value coding)
6.1.2 KVO (Key-value observing)
6.1.3 分类、扩展、代理(Delegate)
6.1.4 属性关键字
6.1.5 通知(NSNotification)

7. 内存管理

7.1.1 在 Obj-C 中,如何检测内存泄漏?你知道哪些方式?
7.1.2 在 MRC 下如何重写属性的 Setter 和 Getter_
7.1.3 循环引用
7.1.4 说一下什么是 悬垂指针?什么是 野指针?
7.1.5 说一下对 retain,copy,assign,weak,_Unsafe_Unretain 关键字的理解
7.1.6 是否了解 深拷贝 和 浅拷贝 的概念,集合类深拷贝如何实现
7.1.7 使用自动引用计数应遵循的原则
7.1.8 能不能简述一下 Dealloc 的实现机制
7.1.9 内存中的5大区分别是什么?
7.1.10 内存管理默认的关键字是什么?
7.1.11 内存管理方案
7.1.12 内存布局
7.1.13 讲一下 iOS 内存管理的理解
7.1.14 讲一下 @dynamic 关键字?
7.1.15 简要说一下 @autoreleasePool 的数据结构?
7.1.16 访问 __weak 修饰的变量,是否已经被注册在了 @autoreleasePool 中?为什么?
7.1.17 retain、release 的实现机制?
7.1.18 MRC(手动引用计数)和ARC(自动引用计数)
7.1.19 BAD_ACCESS 在什么情况下出现?
7.1.20 autoReleasePool 什么时候释放?
7.1.21 ARC自动内存管理的原则
7.1.22 ARC 在运行时做了哪些工作?
7.1.1ARC 在编译时做了哪些工作
7.1.23 ARC 的 retainCount 怎么存储的?
7.1.24 __weak 属性修饰的变量,如何实现在变量没有强引用后自动置为 nil ?
7.1.25 __weak 和 _Unsafe_Unretain 的区别?

2. 网络知识

2.1.1 HTTP协议
2.1.2 HTTPS、对称加密、非对称加密
2.1.3 一个基于UDP的简单的聊天Demo
2.1.4 UDP的特点、UDP的报文结构及差错检测
2.1.5 TCP、三次握手、四次挥手、代码实现
2.1.6 可靠数据传输、流量控制(滑动窗口)、拥塞控制
2.1.7 DNS
2.1.8 Cookie和Session
2.1.9 IP协议、IP数据报分片、IPv4编址、网络地址转换(NAT)
2.1.10 IPv6、从IPv4到IPv6的迁移

3. UI 知识

3.1. UIView

3.2. CALayer

3.2.1.1 请说一下对 CALayer 的认识。

layer 层是涂层绘制、渲染、以及动画的完成者,它无法直接的处理触摸事件(也可以捕捉事件)
layer 包含的方面非常多,常见的属性有 Frame、Bounds、Position、AnchorPoint、Contents 等等。

3.2.1.2 CALayer 的 Contents 有几下几个主要的属性:

CALayer 的 Contents 有几下几个主要的属性:

  1. ContentsRect
    单位制(0 - 1),限制显示的范围区域
  2. ContentGravity
    类似于 ContentMode,不过不是枚举值,而是字符串
  3. ContentsScale
    决定了物理显示屏是 几@X屏
  4. ContentsCenter
    跟拉伸有关的属性

3.3 事件传递与视图响应链

3.4 图像显示原理

1.CPU:输出位图
2.GPU :图层渲染,纹理合成
3.把结果放到帧缓冲区(frame buffer)中
4.再由视频控制器根据vsync信号在指定时间之前去提取帧缓冲区的屏幕显示内容
5.显示到屏幕上
CPU工作
1.Layout: UI布局,文本计算
2.Display: 绘制
3.Prepare: 图片解码
4.Commit:提交位图
GPU渲染管线(OpenGL)
顶点着色,图元装配,光栅化,片段着色,片段处理

3.5 UI卡顿掉帧原因

iOS设备的硬件时钟会发出Vsync(垂直同步信号),然后App的CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染。随后,GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。也就是说,一帧的显示是由CPU和GPU共同决定的。
一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿。

3.6 滑动优化方案

CPU:

把以下操作放在子线程中
1.对象创建、调整、销毁
2.预排版(布局计算、文本计算、缓存高度等等)
3.预渲染(文本等异步绘制,图片解码等)

GPU:

纹理渲染,视图混合
一般遇到性能问题时,考虑以下问题:
是否受到CPU或者GPU的限制?
是否有不必要的CPU渲染?
是否有太多的离屏渲染操作?
是否有太多的图层混合操作?
是否有奇怪的图片格式或者尺寸?
是否涉及到昂贵的view或者效果?
view的层次结构是否合理?

3.7 UI绘制原理

异步绘制:
[self.layer.delegate displayLayer: ]
代理负责生成对应的bitmap
设置该bitmap作为该layer.contents属性的值

3.8 离屏渲染

On-Screen Rendering:当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
Off-Screen Rendering:离屏渲染,分为CPU离屏渲染和GPU离屏渲染两种形式。GPU离屏渲染指的是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作
应当尽量避免的则是GPU离屏渲染
GPU离屏渲染何时会触发呢?
圆角(当和maskToBounds一起使用时)、图层蒙版、阴影,设置

为什么要避免GPU离屏渲染?
GPU需要做额外的渲染操作。通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响。另外由于离屏渲染会增加GPU的工作量,可能会导致CPU+GPU的处理时间超出16.7ms,导致掉帧卡顿。所以可以的话应尽量减少offscreen-render的图层

3.9 动画

动画书籍

3.10 图片加载显示

3.11 视频

3.6 OpenGL ES

4. 算法

5. 架构设计

6. 设计模式

6.1.1 如何设计一个时长统计框架?
6.1.2 如何设计一个图片缓存框架?
6.1.3 编程中的六大设计原则?

7. 项目相关

8. 性能优化

8.1.1 iOS 性能优化面试题
8.1.2 光栅化
8.1.3 日常如何检查内存泄露?
8.1.4 如何高性能的画一个圆角?
8.1.5 如何提升 tableview 的流畅度?
8.1.6 如何优化 APP 的电量?
8.1.7 如何有效降低 APP 包的大小?
8.1.8 什么是 离屏渲染?什么情况下会触发?该如何应对?
8.1.9 如何检测离屏渲染?
8.1.10 怎么检测图层混合?

9. 逆向

10. 安全防护

10.1.1 RSA非对称加密
10.1.2 简述 SSL 加密的过程用了哪些加密方法,为何这么作?

11. 其他

11.1 OC对象占用内存原理

  1. OC对象 最少占用 16 个字节内存.
  2. 当对象中包含属性, 会按属性占用内存开辟空间. 每一行 16 个字节中, 剩余内存如果可以放下剩余其中一个属性 (参考倒数第二张图) , 则会在行末存储 (注意: 并非一定是按照定义顺序来开辟空间, 放不下就开辟这样). 放不下时会重新开辟一行存储.
    最终满足 16 字节对齐标准.

11.1.1 初始OC对象占用内存

  • 创建一个 Command Line Tool 工程 , 打开 main.m 在 main 函数创建一个 NSObject.
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
    }
    return 0;
}
  • 打开终端/iTerm2 , 进入到 main.m 目录. 将其转换为 c++ 源码.
clang -rewrite-objc main.m -o main.cpp
  • 文件夹目录里多出一个 main.cpp 文件 , 打开. 看到7703行代码,不要慌.我们只需要关注 NSObject 即可. 搜索 NSObject_IMPL.
  • 这个就是 NSOject 对象对应的 C++ 结构体. 里面包含了一个 Class 指针. 搜索发现
typedef struct objc_class *Class;
  • 其实就是一个指向 struct objc_class 结构体类型的指针. 那么也就是说目前我们只发现 NSObject 对象对应的结构体只包含一个 isa 指针变量 , 一个指针变量在 64 位的机器上大小是 8 个字节.

  • 那是不是说一个 NSObject 对象就占用8个字节大小的内存呢?实际上不是的. 答案其实是: 所有的OC对象至少为16字节.

  • 我们先来验证一下. (有兴趣的可以去看看刚刚 main.cpp 中最下面 main 函数中 对象的创建源码)

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *lbobjc = [[NSObject alloc] init];
        
        NSLog(@"lbobjc对象实际需要的内存大小: %zd",class_getInstanceSize([lbobjc class]));
        NSLog(@"lbobjc对象实际分配的内存大小: %zd",malloc_size((__bridge const void *)(lbobjc)));
    }
    return 0;
}
  • 打印结果如下:

查看内存大小

  • 接下来,我们先来打个断点看看

断点查看内存

  • 接下来,我们通过断点来查看内存,这里先介绍一下查看内存的方法。
11.1.1.1 查看内存具体内容方法
  • 打开内存查看工具:

打开内存查看工具

  • 打开后会进入这个页面:
    内存查看工具

  • 地址栏中输入对象地址: NSObject的内存地址 0x102806240
    地址栏中输入对象地址

  • lldb下输入调式命令 p lbobjc 查看对象指针地址,看到地址:0x0000000102806240,然后输入x 0x0000000102806240
    查看指针地址

  • 两种方法都表明, 目前我们创建的对象 后面几个字节全部为 00 .

  • 我们可以通过阅读 objc4 的源码来找到答案。通过查看跟踪 obj4allocallocWithZone 两个函数的实现,会发现这个连个函数都会调用一个 instanceSize 的函数:

size_t instanceSize(size_t extraBytes) {
     size_t size = alignedInstanceSize() + extraBytes;
      // CF requires all objects be at least 16bytes.
      if (size < 16) size = 16;
      return size; 
}
  • 上面源码中我们看出了答案, 最少会开辟16个字节. 那么为什么非要用 16 个字节来存储 8 个字节的内容呢?
  • 这里简单解释一下 .
  • 其实这里主要是涉及到硬件问题, 因为不同厂商之间需要一套标准化方案来解决不同厂商之间规则不同导致内存读取使用出现不统一的情况.为了解决这种问题而产生的 字节对齐.
  • 讲到这里,我还想继续看下 当这个对象包含多个属性时使用内存情况. 以便我们彻底搞明白 OC 对象使用内存情况.
11.1.1.2 查看属性占用内存情况
  • 创建一个 LBPerson 类,继承与 NSObject , 其包含三个 int 属性
@interface KylPerson : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int height;
@property (nonatomic,assign) int row;
@end
  • 回到main.m文件的main()函数,修改代码如下:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
#import "KylPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        //NSObject *kylobjc = [[NSObject alloc] init];
        KylPerson *kp = [[KylPerson alloc] init];
        kp.age = 26;
        kp.height = 180;
        kp.row = 10;
        
        NSLog(@"kylobjc对象实际需要的内存大小: %zd",class_getInstanceSize([kp class]));
        NSLog(@"kylobjc对象实际分配的内存大小: %zd",malloc_size((__bridge const void *)(kp)));
    }
    return 0;
}

  • 打印结果为:
    KylPerson内存打印结果

  • 和上面步骤一样,打断点通过lldb命令查看内存
    person对象属性内存地址查看

由于原本结构体 isa 指针占用8个, 然后 age 属性占用4个, height 占用 4个,此时 一组16字节刚好被占满 , row 属性再占用4个. 再次字节对齐,不足 16 补 16. 答案是 32 个字节.

  • 接下来我们对上面的属性类型做一点修改,我们将height属性由原来的int类型改为double类型:
@interface KylPerson : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,assign) double height;
@property (nonatomic,assign) int row;
@end
  • 重复上面的步骤,继续断点查看内存变化:

修改height类型为double后的内存

  • 我们再来修改一下属性row的类型由原来的int改为double类型。
@interface KylPerson : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,assign) double height;
@property (nonatomic,assign) double row;
@end
  • 继续重复上面步骤,lldb断点查看内存变化如下:
    height和row都改为double类型后的内存
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值