iOS笔记梳理

App启动

1.有storyboard

1.main函数;2.创建UIApplication对象;3.根据Info.plist获取Main.storyboard加载;4.创建UIWindow;

2.无storyboard

1.main函数 2.创建UiApplication对象; 3.创建UIApplicationd的delegate对象;4.delegate对象开始处理系统事件,调用代理;5.执行到application:didFinishLaunchingWithOptions;6.创建UIWindow;7.设置RootViewcontroller; 8.显示窗口;

启动流程

  • 区分冷热启动,这里主要讲冷启动
1.main函数执行前

1.加载可执行文件。(App里的所有.o文件)
2.加载动态链接库,进行rebase指针调整和bind符号绑定
3.objc的runtime初始化 包括:objc相关Class的注册、category注册、selector唯一性检查等
4.初始化。 包括:执行+load()方法、用C++静态构造器 attribute((constructor))修饰的函数的调用、创建C++静态全局变量等

简单来说,
App启动后,首先,系统内核(Kernel)创建一个进程。其次,加载可执行文件。(可执行文件是指Mach-O格式的文件,也就是App中所有.o文件的集合体)这时,能获取到dyld(dyld是苹果的动态链接器)的路径。
然后,加载dyld,主要分为4步:
 1 . load dylibs:这一阶段dyld会分析应用依赖的dylib,找到其mach-o文件,打开和读取这些文件并验证其有效性,接着会找到代码签名注册到内核,最后对dylib的每一个segment调用mmap()。
 2 . rebase/bind:进行rebase指针调整和bind符号绑定。
 3 . ObjC setup:runtime运行时初始化。包括ObjC相关Class的注册、category注册、selector唯一性检查等。
 4 . Initializers:调用每个ObjC类与分类的+load方法,调用attribute((constructor))修饰的函数、创建C++静态全局变量。

2.main函数执行后

这个阶段主要完成的是首屏的一些工作,包括首屏初始化的配置文件读取,列表数据的读取,渲染

总的来讲:main函数通知UIApplicationMain()去创建UIApplication(包括:代理,创建主RunLoop),然后读取plist文件,需不需要storyboard,在didFinish代理回调里设置Window,以及rootController

3.首屏渲染完成后

首屏渲染完成后的阶段,指的是:didFinishLaunchingWithOptions方法作用域内执行首屏渲染后的所有方法执行。即从设置self.window.rootViewController到didFinishLaunchWithOptions方法作用域结束。

启动优化

  • 设置EnvionmentVariables :DYLD_PRINT_STATISTICS 设置为1(用于打印main函数调用之前,启动的各方面耗时)。DYLD_PRINT_STATISTICS_DETAILS : 可以打印得更详细
  •  1.代码内减少使用+load方法,尽量将在+load内执行的内容放到渲染之后,或者使用+initialize()
     2.动态库合并,苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持6个非系统动态库合并为一个。
     3.优化类、方法、全局变量,减少加载启动后不会去使用的类或方法;少用C++全局变量
     4.优化首屏渲染前的功能初始化,就是尽量不要把初始化放在didFinish

可执行文件(Mach-o)

  • 相当于window的exe文件
  • 在ipa包内体积最大那个就是了
  • 可以使用MachoOView分析文件

动态链接库

  库的链接分为静态和动态两种,iOS中直接影响到的就是包体积大小。1. 静态库,在我们开发完成之后会被打包到可执行文件内,包体积就大。2.动态库,比如UIKit系统库,存在于iOS系统内部,不被打包到我们的可执行文件内,只有在包安装到iOS上,启动时候会被dyld(动态链接器)链接起来,app就可以调用系统函数

Class原理

  Class在iOS中实际上就是指向objc_class结构体的指针 | class、metaclass对象本质都是struct objc_class

  • 1.实例对象(id):对类对象alloc或者new操作时创建,这个过程中会拷贝实例所属类的成员变量,但并不拷贝类定义的方法。实力对象在内存中存储的信息包括isa指针以及其他成员变量
  • 2.类对象(class):使用class和object_getClass获取的对象都是类对象;每个类在内存中只有一份;类对象在内存中包括:isa指针、superclass指针,类的属性(property)、类的对象方法信息(instancemethod)、类的协议信息(protocol)、类的成员变量信息(ivar)
  • 3.元类对象(metaclass):object_getClass([NSObject class])获取到的就是元类对象;每个类的内存有且只有一个meta-class对象;类方法存在于元类对象内;元类对象内包括:isa指针、superclass、类方法
  • 4.[[NSObject class] class] 获取的是类对象不是元类对象
    1. runtime的class_isMetaClass可以查看是否元类对象
  • 6.实例对象的isa指向类对象,类对象的isa指向元类对象
  • 7.调用实例对象的对象方法时候,通过isa找到类对象,再调用类对象内的对象方法
  • 8.调用类对象的类方法的时候,通过类对象的isa找到元类对象,再调用元类对象内的类方法
  • 9.实例对象没有superclass
  • 10.类对象的superclass指针指向父类的类对象
  • 11.类对象superclass指针最终指向NSObject的类对象
  • 12.实例对象要调用父类的对象方法时,需要使用isa找到自身的类对象,在用类对象的superclass指针找到父类的类对象,从而找到父类的对象方法。(找类方法差不多,就是找metaclass)
  • 13.没有父类,superclas指针为nil
  • 14.64bit开始,利用isa指针查找,需要&ISA_MASK 进行位运算才能计算真实的地址
  • 15.class、metaclass对象本质都是struct objc_class

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//1. 弃用的Class指针结构体
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

//2.现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();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
...
}

KVO

  1. 全称 Key-Value Observing(‘键值监听’),可以用于监听某个对象属性值的改变
  2. KVO时,将被监听的对象isa指针动态修改成新类NSKVONotifying_Person
  3. 同时修改superclass和class两个实现,隐藏了NSKVONotifying_Person的存在,使用object_getClass可以找出来
  4. 通过set方法为属性赋值触发监听,也可以手动触发willcchange和didchange,直接修改成员变量不会触发监听
    图片
    图片
1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}

// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}

KVC

  1. KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
  • (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
  • (void)setValue:(id)value forKey:(NSString *)key;
  • (id)valueForKeyPath:(NSString *)keyPath;
  • (id)valueForKey:(NSString *)key;

图片

Category

  1. 为已经存在的类添加方法、属性,无法添加成员变量;添加属性,只会生成set和get方法 (无法生成成员变量)
  2. Category 中的方法和类中原有方法同名,category 中的方法会覆盖掉类中原有的方法 (最后加载的留下)
  3. 使用关联变相给Category添加成员变量 设置:objc_setAssociatedObject,获取:objc_getAssociatedObject(self, &nameKey); - 分类添加属性
  4. 在Objective-C提供的runtime函数中,确实有一个lass_addIvar(),这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
  5. 实例对象在创建的时候就规定了isa指针以及成员变量,也就是实例变量的那块内存布局,如果动态添加了成员变量,就破坏了实例对象内存,这个实例对象就会变成了无效的对象。方法不存在实例对象内!
1
2
3
4
5
6
7
8
9
10
//分类的源代码
typedef struct category_t *Category;
struct category_t {
const char *name; //category名称
classref_t cls; //要拓展的类
struct method_list_t *instanceMethods; //给类添加的实例方法的列表
struct method_list_t *classMethods; //给类添加的类方法的列表
struct protocol_list_t *protocols; //给类添加的协议的列表
struct property_list_t *instanceProperties; //给类添加的属性的列表
};

AutoReleasePool

  • 自动释放池 :一般理解就是自动帮OC对象添加release操作,从而合理管理引用计数
  • 哨兵对象:NSAutoReleasePool 类的 push() 标记 一个哨兵对象 的内存地址,以NSAutoReleasePool成员变量的方式保存。AutoReleasePool代码块里面 创建 的OC对象,都会标记到哨兵对象之后的内存块中。(其实就是类似Array添加对象,然后在释放后把Array里面的所有对象都给释放掉一样),调用 [pool drain] 的时候,实际是调用NSAutoReleasePool 的pop方法,然后把添加在哨兵对象之后的OC对象全部释放
1
2
3
4
5
6
//AutoReleasePoolPage的源码大概是这样的流程
NSAutoReleasePool *pool = [[NSAutoReleasePool alloc] init]; ------> id context = AutoreleasePoolPage.push();
[obj1 autorelease]; -------> getHotPage().add(obj1); 添加到哨兵对象之后
[obj2 autorelease]; -------> getHotPage().add(obj2);
[pool drain]; -------> AutoreleasePoolPage.pop(context);

+load方法

  1. 每个类、分类的+load,在程序运行过程中只调用一次
  2. 调用顺序:1先调用类的+load( 按照编译先后顺序调用,先编译,先调用;调用子类的+load之前会先调用父类的+load),2再调用分类的+load(按照编译先后顺序调用,先编译,先调用)
  3. +load方法不经过msgSend调用,而是直接使用函数地址

+load()与+initialize()两者的区别?
+load()方法会在main()函数调用前就调用,而+initialize()是在类第一次使用时才会调用。
+load方法的调用优先级: 父类 > 子类 > 分类,并且不会被覆盖,均会调用。
+load方法是在main() 函数之前调用,所有的类文件都会加载,包括分类也会加载。
+initialize方法的调用优先级:分类 > 子类,父类 > 子类。(父类的分类重写了+initialize方法会覆盖父类的+initialize方法)

+initialize方法

  1. 在类第一次接收到消息时调用
  2. 先调用父类的+initialize,再调用子类的+initialize (先初始化父类,再初始化子类,每个类只会初始化1次)
  3. initialize是通过objc_msgSend进行调用的
  4. 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
  5. 如果分类实现了+initialize,就覆盖类本身的+initialize调用

符号表

ScrollView

  1. autolayout下系统会在viewDidAppear之前重新根据subView计算contentsize,所以在viewdidiload里面手动设置contentsize会被覆盖;希望在viewdidload下设置contentsize的话需要去掉autolayout选项,或者自己设置subView的constraint,或者在viewdidappear手动设置contentsize

View、UIWindow、CALayer

  1. View - UIResponder;CALayer - NSObject;
  2. 都是容器,View能响应事件,CALayer不能,UIWindow传递

frame 和 bounds

  1. frame指的是View在父View中的位置大小,参照物是父View的坐标系
  2. bounds指的是自身在坐标系中的位置大小,参照物是本身

关联对象ASSOCIATION

  • 我们所说的关联对象的使用环境,或者说面试时候的关联对象,通常以给分类添加属性时候的使用,因为分类无法添加成员变量
  1. 设置: objc_setAssociatedObject(self, @”name”,name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 获取: objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); 移除: objc_removeAssociatedObjects(self);
  2. 关联对象并不是存储在被关联对象本身内存中
  3. 关联对象存储在全局的统一的一个AssociationsManager中
  4. 策略:
1
2
3
4
5
6
7
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
  1. 如果设置关联对象为nil,就相当于是移除关联对象
  2. weak修饰的属性,在对象销毁时候指针就置空了,但是哈希表内对应的值还在,当我们使用这个指针去释放的时候,这个指针地址已经为空了

关联对象原理

Block

  1. block本质上也是一个OC对象,它内部也有个isa指针
  2. block是封装了函数调用以及函数调用环境的OC对象
  3. 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
  4. 定义block之后修改局部变量的值,在block调用的时候无法生效,局部变量传入block之后,内调用变量是在block内部的变量,所以外部修改局部变量无法生效
  5. block内部存在一个isa指针,表示这个结构体是一个oc对象
  6. 构造函数中传入的参数都存在了__main_block_impl_0这个结构体中,最后又将这个结构体的指针赋值给了block
  7. block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址
  8. Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//clang编译器可以将oc代码装换成c++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
//oc代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void(^block)(int ,int) = ^(int a, int b){
NSLog(@"this is block,a = %d,b = %d",a,b);
NSLog(@"this is block,age = %d",age);
};
block(3,5);
}
return 0;
}

// 定义block变量代码
void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

//__block_impl结构体
struct __block_impl{
void *isa; //&_NSConcreteStackBlockd地址,block就是_NSConcreteStackBlock类型
int Flags; //
int Reserved;
void *FuncPtr; //block代码块中的代码被封装成__main_block_func_0函数存储在FuncPtr中
}

// 执行block内部的代码,很明确的能看到我们在使用block内部代码的时候是通过FuncPtr
//这里使用的是__block_imple类型的block,因为__block_impl是__main_block_impl_0的第一个参数,所以起始的内存地址是一致的,可以进行强制类型装换,并且通过这个block找到FuncPtr
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);

/******8 传入参数 */
//__main_block_func_0存储着我们在block中写入的代码
static void __main_block_func_0(...){
int age = _cself->age
nslog(...)
nslog(...)
}

//&__main_block_desc_0_DATA
static struct __main_block_desc_0 {
size_t reserved //0
size_t Blick_size //传入了 __main_block_impl_0 大小
} __main_block_desc_0_DATA = {0,sizefo(struct __main_block_impl_0)}

//自定义的局部变量,因为在block块中使用带了age这个局部变量,所以将这个age当参数传入了block
age

/*************/



_main_block_imp_0结构体
block示意图

Block捕获

  • Block内部可以正常访问外部变量,有一个捕获的机制

  • self同样被block捕获,不论对象方法还是类方法都会默认将self作为参数传递给方法内部

  • 即使block中使用的是实例对象的属性,block中捕获的仍然是实例对象,并通过实例对象通过不同的方式去获取使用到的属性(self.name : 捕获self 通过name的get方法)

    局部变量

  • auto变量

    auto自动变量,离开作用域就销毁,通常局部变量前面自动添加auto关键字。
    自动变量会被捕获到block内部,也就是说block内部会专门新增加一个参数来存储变量的值
    auto只存在于局部变量中,访问方式为值传递,内存拷贝,深拷贝

  • static变量

    static 修饰的变量为指针传递,同样会被block捕获
    捕获的内部是 int *age 的指针形式

  • 自动变量和静态变量的区别来自于销毁的机制:自动变量是入栈,函数结束等都可能被销毁;静态变量,内存中有且只有一份,不会被销毁。所以自动变量需要保存值,而静态变量保存指针地址就行

  • 所以外部修改自动变量不影响block内部,修改静态变量就会影响

全局变量

  • 全局变量哪里都能访问,所以__main_block_imp_0没有添加任何变量,不捕获任何变量
  • 局部变量因为跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获

Block类型

  • ARC环境下会对栈区的block进行一次copy操作,将block提升到堆区
  • block继承于NSBlock,NSBlock继承于NSObject
  • block的三种类型:1. NSGlobalBlock ( _NSConcreteGlobalBlock ),2.NSStackBlock ( _NSConcreteStackBlock ),3.NSMallocBlock ( _NSConcreteMallocBlock )
  • NSGlobalBlock :不访问自动变量,多数情况下使用不到,通常全局变量在哪里都能访问,所以使用函数去完成相应的处理就行了
  • NSStackBlock:访问自动变量,使用到最多的情况,捕获自动变量进栈
  • NSMallocBlock:栈区的block在函数作用域结束时候跟着一起销毁,使用copy方式将block提升到堆区,防止内存被回收

block示意图
block在内存中的分配区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 验证代码:MRC环境下验证Block到底是进数据段还是堆还是栈
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Global:没有访问auto变量:__NSGlobalBlock__
void (^block1)(void) = ^{
NSLog(@"block1---------");
};
// Stack:访问了auto变量: __NSStackBlock__
int a = 10;
void (^block2)(void) = ^{
NSLog(@"block2---------%d", a);
};
NSLog(@"%@ %@", [block1 class], [block2 class]);
// __NSStackBlock__调用copy : __NSMallocBlock__
NSLog(@"%@", [[block2 copy] class]);
}
return 0;
}


// __NSStackBlock__ 调用copy 转化为__NSMallocBlock__ 防止内存作用域过后内存被回收
int age = 10;
block = [^{
NSLog(@"block---------%d", age);
} copy];
[block release];



上述代码验证代码验证各个情况block的内存类型情况,验证结果

类型 环境 内存区域
NSGlobalBlock 没有访问auto变量 数据段
NSStackBlock 访问oauto变量 栈区
NSMallocBlock __NSStackBlock__调用copy 堆区

各个类型的Block调用copy对类型的影响

类型 内存 copy
NSGlobalBlock 数据段 不改变类型,没反应
NSStackBlock 栈区 由栈入堆,类型变NSMallocBlock
NSMallocBlock 堆去 增加引用计数,不改变类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//将block赋值给__strong指针时
//block被强指针引用时,RAC也会自动对block进行一次copy操作
int main(int argc, const char * argv[]) {
@autoreleasepool {
// block内没有访问auto变量
Block block = ^{
NSLog(@"block---------");
};
NSLog(@"%@",[block class]);
int a = 10;
// block内访问了auto变量,但没有赋值给__strong指针
NSLog(@"%@",[^{
NSLog(@"block1---------%d", a);
} class]);
// block赋值给__strong指针
Block block2 = ^{
NSLog(@"block2---------%d", a);
};
NSLog(@"%@",[block1 class]);
}
return 0;
}
//打印结果:__NSGlobalBlock__;__NSStackBlock__;__NSMallocBlock__

_block修饰

  • __block可以用于解决block内部无法修改auto变量值的问题,因为编译器会将_block包装成一个对象(_Block_object_assign),
  • __block不能修饰全局变量、静态变量(static)
  • __block变量从堆上移除会调用__block变量内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放指向的对象(release)
  • 当__block变量在栈上时,不会对指向的对象产生强引用
    _ __block变量被copy到堆时会调用__block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)

_block修饰变量的实际表现:
block示意图

Runtime

  • 方法替换的实现,本质是类对象内rw_t内 methodlist方法数组内的方法对象的IMP交换掉
  • 当使用method_exchange进行了函数交换,会清空缓存
  • 替换方法时候需要重新将自生方法调用

在项目中的使用:
1.关联对象
2.遍历所有成员变量
3.归档解档
4.交换方法实现
5.利用消息转发修改报错,减少奔溃

Runloop

  • 程序运行时候循环做事情

  • 打个比方:runloop相当于流水线(线程)上的管家,等待我们需要的时候去做相应的操作,没活的时候,流水线改休息的休息(线程休眠),有活的时候就指挥流水线干活,合理分配流水线的工作(合理分配资源)

  • Runloop和线程的关系
    每条线程都有唯一的一个与之对应的runloop对象
    Runloop保存在一个全局的字典里面,线程为key,runloop为value
    线程刚创建的时候没有runloop对象,Runloop会在第一次获取它时候创建
    Runloop会在线程销毁的时候销毁
    主线程的Runloop已经自动获取(创建),子线程默认不会开启runloop

  • 同一时间只能使用一个mode,

  • iOS 中使用了两套API控制Runloop : NSRunLoop 和 CFRunLoopRef

  • CFRunLoopRef是开源的,https://opensource.apple.com/tarballs/CF/

  • 使用OC和C两种方式获取同一个Runloop的内存地址不同,原因是NSRunloop包装CFRunloopRef

  • CFRunLoopModeRef
    CFRunLoopModeRef代表RunLoop的运行模式
    一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
    RunLoop启动时只能选择其中一个Mode,作为currentMode
    如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
    不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
    如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

1
2
3
4
5
6
7
8
9
10
//获取runloop对象

//OC获取方法
[NSRunLoop currentRunLoop];获取当前线程Runloop
[NSRunLoop mainRunLoop];获取主线程Runloop

//C语言获取方法
CFRunLoopGetCurrent();获取当前线程
CFRunLoopGetMain();获取主线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

1、使用子线程执行异步操作
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
2、使用主线程执行异步操作
//dispatch_queue_t queue = dispatch_get_main_queue();
3、异步,主线程无法同步执行
dispatch_async(queue, ^{
NSLog(@"1");
4、需要注意的是这里:若是在子线程,runloop默认是没开启的,这样一来,performSelector的timer是无法执行的,所以只会打印1和2;如果是在主线程调用,runloop默认能开启就会f打印132
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}

-(void)test{
NSLog(@"2");
}

执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
01、通知Observers:进入Loop
02、通知Observers:即将处理Timers
03、通知Observers:即将处理Sources
04、处理Blocks
05、处理Source0(可能会再次处理Blocks)
06、如果存在Source1,就跳转到第8步
07、通知Observers:开始休眠(等待消息唤醒)
08、通知Observers:结束休眠(被某个消息唤醒)
01> 处理Timer
02> 处理GCD Async To Main Queue
03> 处理Source1
09、处理Blocks
10、根据前面的执行结果,决定如何操作
01> 回到第02步
02> 退出Loop
11、通知Observers:退出Loop

frame #0: 0x0000000102345a7c iOS源码分析项目`-[ViewController touchesBegan:withEvent:](self=0x0000000102902240, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00000002811f43c0) at ViewController.m:23:5
frame #1: 0x00000001b8256b64 UIKitCore`forwardTouchMethod + 328
frame #2: 0x00000001b8256a08 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 60
frame #3: 0x00000001b8264af0 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1692
frame #4: 0x00000001b82660a8 UIKitCore`-[UIWindow sendEvent:] + 3352
frame #5: 0x00000001b8242ae8 UIKitCore`-[UIApplication sendEvent:] + 336
frame #6: 0x00000001b82ba23c UIKitCore`__dispatchPreprocessedEventFromEventQueue + 5880
frame #7: 0x00000001b82bc798 UIKitCore`__handleEventQueueInternal + 4924
frame #8: 0x00000001b82b560c UIKitCore`__handleHIDEventFetcherDrain + 108
frame #9: 0x00000001b419a7e0 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #10: 0x00000001b419a738 CoreFoundation`__CFRunLoopDoSource0 + 80
frame #11: 0x00000001b4199ed0 CoreFoundation`__CFRunLoopDoSources0 + 180
frame #12: 0x00000001b419501c CoreFoundation`__CFRunLoopRun + 1080
frame #13: 0x00000001b41948bc CoreFoundation`CFRunLoopRunSpecific + 464
frame #14: 0x00000001be000328 GraphicsServices`GSEventRunModal + 104
frame #15: 0x00000001b822a6d4 UIKitCore`UIApplicationMain + 1936
frame #16: 0x00000001023459cc iOS源码分析项目`main(argc=1, argv=0x000000016dabf898) at main.m:18:12
frame #17: 0x00000001b401f460 libdyld.dylib`start + 4

  • 关于线程保活问题
  • 1.使用initwithtarget的循环引用
  • 2.生命周期是否跟随控制器

多线程

iOS中的多线程方案
技术方案 简介 语言 线程生命周期 使用频率
pthread 跨平台\可移植,通用的API C 程序猿管理 几乎不用
NSThread 使用更加面向对象,使用简单 OC 程序猿管理 偶尔使用
GCD 充分使用多核处理器,旨在替换NSThread C 自动管理 经常
NSOperation 基于GCD,相比GCD更加简单 OC 自动管理 经常
队列的执行效果
同步 \ 异步 并发队列 手动创建串行队列 主队列
同步(sync) 没有开启新线程、串行执行 没有开启新线程、串行执行 没有开启新线程、串行执行
异步(async) 开启新线程、并发执行 开启新线程、串行执行 没有开启新线程、串行执行
一般知识点

1.GCD源码:https://github.com/apple/swift-corelibs-libdispatch
2.注意死锁问题,造成死锁的关键在于:同步执行,并且在当前串行队列中添加任务就会造成死锁,即相互等待
3 .[self performSelector]的函数想要在子线程执行,需要启动子线程runloop,睡眠等该函数的执行,不然,在线程的任务一完成,子线程就会退出,这个时候执行performSelector的函数就会报错,提示该线程已经退出
4.多线程存在的安全隐患,多条线程同时操作块数据
5.cpu在执行多线程的时候就是使用时间片轮转调度算法
6.线程同步:不能让多条线程同时占用一份内存
7.GCD的常用函数:

1
2
3
4
//用同步的方式执行任务:
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block)、
//用异步的方式执行任务:
dispatch_async(dispatch_queue_t queue, dispatch_block_t block)
4个术语比较容易混淆:同步、异步、并发、串行

1.同步和异步主要影响:能不能开启新的线程
2.同步:在当前线程中执行任务,不具备开启新线程的能力
3.异步:在新的线程中执行任务,具备开启新线程的能力
4.并发和串行主要影响:任务的执行方式
5.并发:多个任务并发(同时)执行
6.串行:一个任务执行完毕后,再执行下一个任务

GNUstep

下载地址:http://gnustep.org/resources/downloads.php#core
用于参考Fundation框架

1
2
runmode的原理就是不断的调用

线程组 - 可以在开发过程中完成不同任务的执行,同时执行,顺序执行等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);

// 添加异步任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1-%@", [NSThread currentThread]);
}
});

dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2-%@", [NSThread currentThread]);
}
});

// 等前面的任务执行完毕后,会自动执行这个任务
// dispatch_group_notify(group, queue, ^{
// dispatch_async(dispatch_get_main_queue(), ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务3-%@", [NSThread currentThread]);
// }
// });
// });

// dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务3-%@", [NSThread currentThread]);
// }
// });

dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});

dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4-%@", [NSThread currentThread]);
}
});
OSSpinLock 自旋锁
  • #import <libkern/OSAtomic.h>
  • iOS10之后不推荐使用,OSSpinLockUnlock’ is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock_unlock() from <os/lock.h>
  • 等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
  • 目前已经不再安全,可能会出现优先级反转问题,就是存在优先级比较高的线程在盲等状态, 导致cpu不会给优先级比较低的线程分配时间,这个线程比较低的锁就会卡住,产生类似死锁的情况
  • 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13
@property (assign, nonatomic) OSSpinLock lock;
// 初始化
self.lock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&_lock);
// 解锁
OSSpinLockUnlock(&_lock);
//尝试加锁
if(OSSpinLockTry(&_lock)){

// 解锁
OSSpinLockUnlock(&_lock);
}
os_unfair_lock
  • 相比较OSSpinLock,不是盲等状态,而是让线程处于休眠状态,
1
2
3
4
5
6
//初始化
self.lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&_ticketLock);
//解锁
os_unfair_lock_unlock(&_ticketLock);
pthread_mutex - 互斥锁
  • 等待线程会处于睡眠状态
  • 初始化属性传空表示默认 pthread_mutex_init(mutex, NULL);
  • 关于属性为递归锁 PTHREAD_MUTEX_RECURSIVE:允许同一个线程对一把锁进行重复加锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define PTHREAD_MUTEX_NORMAL        0   //默认
#define PTHREAD_MUTEX_ERRORCHECK 1 //检查锁
#define PTHREAD_MUTEX_RECURSIVE 2 //递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL

@property (assign, nonatomic) pthread_mutex_t moneyMutex;

- (void)__initMutex:(pthread_mutex_t *)mutex
{
// 静态初始化
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// // 初始化属性
// pthread_mutexattr_t attr;
// pthread_mutexattr_init(&attr);
// pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// // 初始化锁
// pthread_mutex_init(mutex, &attr);
// // 销毁属性
// pthread_mutexattr_destroy(&attr);

// 初始化属性
// pthread_mutexattr_t attr;
// pthread_mutexattr_init(&attr);
// pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, NULL);
// 销毁属性
// pthread_mutexattr_destroy(&attr);
}


//加锁
pthread_mutex_lock(&_ticketMutex);
//解锁
pthread_mutex_unlock(&_ticketMutex);
//销毁
- (void)dealloc
{
pthread_mutex_destroy(&_mutex);
}

NSConditionLock - 条件锁
  • 相比较NSCondition条件更加丰富
  • [self.condition loclWhenCondition:1]; 当条件值为1的时候执行这个加锁,并且执行下去
  • [self.condition unLoclWhenCondition:2]; 解锁,同时给这个锁一个为2的条件值
Semaphore - 信号量
  • 控制并发访问的线程数量
    `
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    1.如果信号量的值大于0,进来减一
    2.如果信号量 = 0,就会休眠等待
    3.DISPATCH_TIME_FOREVER:一直等待,等待信号量大于0
    4.DISPATCH_TIME_NOW:不等
    self.mSemaphore = dispatch_semaphore_create(1);
    //信号量等待,并发的信号量数取决于mSemaphore设置的大小
    dispatch_semaphore_wait(self.mSemaphore, DISPATCH_TIME_FOREVER);
    //信号量增加
    dispatch_semaphore_signal(self.moneySemaphore);

对比几个锁的区别
  • 自旋锁:wile等待,汇编代码,LLDB调式情况下,会重复调用一段代码,即wile等待
  • pthread_mutex_lock:不使用的时候必须销毁,可以传入属性分别使用RECURSIVE递归锁或者默认锁
  • pthread_cond:使用wait等待以及signal信号实现锁的等待和重新开启
  • NSLock:pthread_mutex 的封装,OC形式的封装,
  • NSRecursiveLock:pthread_mutex的递归锁的封装,OC的形式使用递归锁
  • NSConditionLock:相当于包装了pthread_mutex和 pthread_cond 条件,遵守Locking协议,wait和signal配合使用

内存管理

atomic

  • 给属性的set和get方法增加t原子性操作,也就是线程同步,也就是加锁和解锁 slotlock 和unslotlock
  • 耗费性能
  • 无法在持续使用的时候保证线程安全

读写安全,IO操作安全

  • 读写锁 pthread_rwlock,自旋锁会等待
  • dispatch_barrier_async 异步栅栏调用,在使用异步栅栏的时候传入的必须是并发队列,如果传入的是全局的并发或者串行队列的话,效果类似dispatch_async,就不是异步栅栏调用了
  • 1.使用target的形式创建的定时器,可能会造成循环引用,使用弱指针无法解决这个定时器的循环引用的问题(不同于block),target还是以参数的形式传入
  • 2.使用block的形式创建的定时器,可以使用弱引用的方法来避免产生循环应用的问题
  • 3.增加中间代理形式也可以避免产生循环应用,其中NSProxy就是用来完成这样的操作,其中使用消息转发的机制可以将函数给被代理方,
  • 4.相比较NSObject做中间代理,完成消息转发的形式,NSProxy不需要到父类寻找方法,而是直接进入消息转发阶段,这样更加的高效

GCD定时器

  • NSTimer 依赖于Runloop,如果runloop的任务过重会导致nstimer不准时

内存分布

  • 从低到高、从上到下:1.保留,2.代码段(TEXT段),3.数据段(DATA)字符串常量,初始化和未初始化的全局变量和静态变量;4.堆区(heap);5.栈区(stack);6.内核区
  • 代码段:编译后的代码;
  • 数据段:1.字符串常量,2.已经初始化的全局变量和静态变量,3.未初始化的全局变量和静态变量;
  • 堆:通过alloc,malloc,calloc等动态分配的内存空间,地址从低到高;
  • 栈区:函数调用的开销(局部变量等),分配地址从高到低

Tagger Pointer

  • 对Tagger Pointer查看引用计数为-1
  • 可以存储NSDate、NSNumber、NSString等
  • OC对象最小使用16个字节,OC指针8个字节,也就是说在没有Tagger Pointer技术之前,要存储一个NSNumber的值需要使用24个字节,而 int只需要4个字节就行,(减少浪费)
  • OC对象存储:16个字节内存对齐,所以OC对象指针的最后一位存放0
  • Tagger Pointer技术将数据直接存放在指针里面
  • 编译器自行处理的
  • 当Tagger Pointer的8个字节存储不下了,就存放在堆区
  • 查找时候不需要像原来的调用,直接到指针内取值,节省了调用的开销
  • 如果指针 p & 1<<63 = 1<<63 的话,这个对象就是一个Tagger Pointer指针,详情查看apple源码的判断是否Tagger Pointer。(mac平台判断最低有效位,iOS判断最高有效位)
  • 总结使用了Tagger Pointer技术,存取都提高了效率

MRC

  • 引用计数:新创建的OC对象引用计数默认为1
  • Autorelease 自动释放,需要手动释放的mrc环境下,使用自动释放池,会在合适的时候将自动释放池内的内存进行释放

Copy、MutableCopy

  • 原则:拷贝出来的对象修改不影响原对象
  • 1.Copy拷贝出来的都是不可变,MutableCopy拷贝出来的都是可变
  • 2.MutableCopy(不管对可变不可变对象进行拷贝的时候)都是深拷贝,新开辟一块内存存储
  • 3.Copy对可变对象拷贝的时候,深拷贝,会生成一个不可变的对象,新的不可变内存
  • 4.特殊情况:Copy对不可变对象拷贝的时候:浅拷贝,指针拷贝,生成新指针指向同一块内存,且copy引用计数会加一;原因是对不可变对象Copy出来的还是不可变对象,既然是不可变对象不能修改,就不生成新的对象浪费内存
  • 5.属性中使用copy,表示就是将来新传进来的对象就是进行一次copy操作,所以属性只要用得copy关键字,就不要使用可变对象,后期使用可变的函数会报错,方法不存在
  • 6.实现copy协议可以使对象可以copy

引用计数

  • oc源码:NSObject.mm内 ratinCount函数,先判断isa是否64位新指针,再判断是否存在SideTqbles中的RefcountMap(哈希表)
  • 对象的引用计数存放在对象isa指针结构体得最后一个数(19位),如果19位不够存储,则将改引用计数存放在isa指针内的一个SideTqbles中(散列表结构)
  • retain加一,release减一
  • 对象引用计数减为0时候msgsend 发送dealloc消息销毁对象

weak指针

  • isa指针内的SideTable存在一个表,用于存放weak指针的weak_table_t(哈希表)中
  • 在对象销毁时会调用clearDeallocating函数找到存放弱指针的哈希表,清除弱引用

ARC

  • LLVM编译器会在合适得地方自动生成 release,autorelease代码
  • Runtime处理弱引用等操作
  • 编译器对局部对象在当前函数最后添加release,对象会在函数执行完毕时候就释放

AutoReleasePool自动释放池

  • 存在构造函数和析构函数,构造函数会调用push,析构函数会调用pop
  • 在释放池内得函数相当于夹在了释放池的push和pop两个函数之间
  • 释放池会在push()的时候入栈一个POOL_BOUNDAY(边界),在pop()结束的时候将这个边界的地址值传入,从最后开始Release,直到遇到这个边界表示释放完毕
  • 每个@AutoReleasePool回调一次push,每次push都会加一个边界
  • 查看自动释放池情况的函数(不开源的): 1.声明 extern void _objc_autoreleasePoolPrint(void); 2. 使用 _objc_autoreleasePoolPrint();
  • 释放时机:执行release是由runloop决定的,释放池会在runloop休眠时执行pop完成release操作

性能优化

卡顿
  • 将cpu和gpu处理的时间控制在16毫秒内,保证60fps的帧频
  • 尽量使用轻量级对象
  • 不要频繁使用View的相关属性,尽量避免不必要的修改
  • 提前计算好布局,在需要时一次性修改
  • Autolayout会比直接设置frame更消耗cpu资源
  • 控制线程并发数量
  • 图片得size最好跟UIImageView的大小保持一致
  • 耗时操作放入子线程(文本绘制,图片解码在子线程解码)
  • 减少视图层次
  • 减少透明的View
  • 避免离屏渲染(光栅化:layer.shouldRasterize = yes, 遮罩,圆角,阴影)
监控卡顿
  • 检测runloop 处理source的时间达到监控卡顿的效果
电量
  • 降低CPU、GPU的消耗
  • 尽量少用定时器
  • 优化文件操作
  • iOS提供了基于GCD得一部操作API,dispatch_io,优化了磁盘访问
  • 压缩网络数据
  • 尽量避免实时定位
  • 只需要一次获取位置可以使用requestLocation,该函数会调用一次定位获取用户位置信息,用完之后就对定位的硬件进行断电
  • 如果实时要求较高的,可以选择合适的定位精度,精度越高调用频率越高,耗电就越快

LLDB调试