阶段性知识点总结

目录
  1. 1. 引言
  2. 2. 设计模式
  • @property
  • 容器
    1. 1. 生命周期
  • KVC
  • KVO
  • Notification
  • 多线程
  • 性能优化
  • Runtime
  • Runloop
  • Crash日志收集与分析
  • 引言

    最近做了一款叫 "芝士超人" 的App,两个人做了一周提审上线,上线后不到一周,日活过百万,AppStore免费榜排行第8,还算比较成功。但是,这个项目做得很不开心,细节就不多说了,然后就有了这篇阶段性总结,写博客的同时自我调节吧。这个总结可能会持续的更新一两个月,我会尝试将我这几年研究的一些东西尽量全面的在这篇博客中写出来,后期也会拆分成不同的部分,方便大家的阅读。
    

    设计模式

    设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的事情

    1. MVC 和 MVVM 的区别

      1
      2
      3
      MVC:简单来说就是,逻辑、试图、数据进行分层,实现解耦。
      MVVM:是Model-View-ViewMode模式的简称。由视图(View)、视图模型(ViewModel)、模型(Model)三部分组成.比MVC更加释放控制器臃肿,将一部分逻辑(耗时,公共方法,网络请求等)和数据的处理等操作从控制器里面搬运到ViewModel中

      MVVM的特点:

      • 低耦合。View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
      • 可重用性。可以把一些视图的逻辑放在ViewModel里面,让很多View重用这段视图逻辑。
      • 独立开发。开发人员可以专注与业务逻辑和数据的开发(ViewModel)。设计人员可以专注于界面(View)的设计。
      • 可测试性。可以针对ViewModel来对界面(View)进行测试
    2. 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。

    3. 观察者模式:KVO是典型的观察者模式,观察某个属性的状态,状态发生变化时通知观察者。

    4. 委托模式:代理+协议的组合。实现1对1的反向传值操作。

    5. 工厂模式:通过一个类方法,批量的根据已有模板生产对象。

      实际上在iOS开发中还有许许多多的设计模式,比如UINavigation是中介者模式,比如我们经常会为了解耦,使用建造者模式(一个builder类来处理构建不同的业务),但是开发日常会使用的模式一般就是上面这一些,所以这里就不列举其他的设计模式了。

    @property

    @property = ivar + getter + setter;

    1. 属性关键字

      1. 原子性— nonatomic , atomic
      2. 读/写权限—readwrite(读写)、readonly (只读)
      3. 内存管理语义—assign、strong、 weak、unsafe_unretained、copy
      4. 方法名—getter= 、setter=
      5. 不常用的:nonnull,null_resettable,nullable
    2. 关键词的作用

      1). readwrite 是可读可写特性。需要生成getter方法和setter方法。

      2). readonly 是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。

      3). assign/weak 是赋值特性。setter方法将传入参数赋值给实例变量;仅设置变量时,assign用于基本数据类型。weak用于对象类型。

      4). strong表示持有特性。setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。

      5). copy 表示拷贝特性。setter方法将传入对象复制一份,需要完全一份新的变量时。

      6). nonatomic 非原子操作。决定编译器生成的setter和getter方法是否是原子操作,atomic表示多线程安全,一般使用nonatomic,效率高。

      7). nonnull,null_resettable,nullable

      ​ nonnull:字面意思就能知道:不能为空(用来修饰属性,或者方法的参数,方法的返回值)(不适用于assign属性,因为它是专门用来修饰指针的)

      ​ nullable:表示可以为空

      ​ null_resettable: get:不能返回空, set可以为空(注意:如果使用null_resettable,必须重写get方法或者set方法,处理传递的值为空的情况)

    3. weak 关键字相比 assign 有什么不同?

      assign 可以用非 OC 对象,而 weak 必须用于 OC 对象。

      weak 表明该属性定义了一种“非拥有关系”。在属性所指的对象销毁时,属性值会自动清空(nil)。

    4. 怎么用 copy 关键字?

      • NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

      • 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。

      • 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。

        使用copy的目的是,防止把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发送变化会无意间篡改不可变类型对象原来的值。

    5. 系统对象的 copy 与 mutableCopy 方法

      不管是集合类对象(NSArray、NSDictionary、NSSet ... 之类的对象),还是非集合类对象(NSString, NSNumber ... 之类的对象),接收到copy和mutableCopy消息时,都遵循以下准则:

      1. copy 返回的是不可变对象(immutableObject);如果用copy返回值调用mutable对象的方法就会crash。
      2. mutableCopy 返回的是可变对象(mutableObject)。
      3. 只有对不可变对象进行copy操作是指针复制(浅复制),其它情况(不可变对象的mutableCopy,可变对象的copy或者mutablCopy)都是内容复制(深复制)!
      4. 关于容器实现copy 或 metableCopy ,容器内元素默认都是 指针拷贝,不是内容复制。

      举个栗子:

      @property (nonatomic, copy) NSMutableArray *arr;(✘)这是错误的写法,请勿模仿!!!

      上面这个写法,当添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃。

      //如:-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460

      因为:copy后返回的是不可变对象(即 arr 是 NSArray 类型,NSArray 类型对象不能调用 NSMutableArray 类型对象的方法)

    容器

    1. NSSet:用于对象无序集合 (集合) 内存中存储不连续,在搜索某个元素时效率高,通过hash算法

    2. NSArray:用于对象有序集合(数组)内存中存储位置连续,通过索引寻找元素时效率更高,寻找特定元素需要使用遍历的方式。

    3. NSDictionary :用于键值映射(字典)内存中存储位置连续

      以上三种集合类是不可变的(一旦初始化后,就不能改变)

    4. NSMutableSet 可修改的集合。主要用于集合运算(并集,交集,差集)

    5. NSMutableArray 可对数组进行增删改

    6. NSMutableDictionary 允许用户添加和删除key和value

    注:这些集合类只能容纳cocoa对象(NSOjbect对象),如果想保存一些原始的C数据(例如,int, float, double, BOOL等),则需要将这些原始的C数据封装成**NSNumber**类型进行存储。NSNumber对象是cocoa对象,可以被保存在集合类中。

    1. 自定义对象数组去重

      下面列举一个对象数组去重的例子,本例中,进行去重的数组是Person对象的数组。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      -(NSMutableArray *)getOutRepeatObjectFrom:(NSArray *)arr{
      NSMutableArray *needArr = [[NSMutableArray alloc]init];
      for (Person *model in arr) {
      __block BOOL isExist = NO;
      [needArr enumerateObjectsUsingBlock:^(Person * _Nonnull obj, NSUInteger idx, BOOLBOOL * _Nonnull stop) {
      if ([obj isEqual:model]) {//数组中已经存在该对象
      *stop = YES;
      isExist = YES;
      }
      }];
      if (!isExist) {//如果不存在就添加进去
      [needArr addObject:model];
      }
      }
      return needArr;
      }

      下面是Person对象的实现

      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
      @interface Person : NSObject
      @property (nonatomic, copy) NSString *name;
      @property (nonatomic, strong) NSDate *birthday;
      @end
      @implementation Person
      - (BOOL)isEqual:(id)object {
      if (self == object) {
      return YES;
      }
      if (![object isKindOfClass:[Person class]]) {
      return NO;
      }
      return [self isEqualToPerson:(Person *)object];
      }
      - (BOOL)isEqualToPerson:(Person *)person {
      if (!person) {
      return NO;
      }
      BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
      BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
      return haveEqualNames && haveEqualBirthdays;
      }
      - (NSUInteger)hash {//这里用hash进行优化,当对象被加入NSSet中时,就会判断是否已经存在相同的hash,有相同的hash,就不会重复写入,完成了去重的功能实现。针对特定场景,只需要实现对应的hash方法,将对象插入NSSet中即可达到去重的目的。
      return [self.name hash] ^ [self.birthday hash];
      }
      @end

    生命周期

    1. ViewController生命周期

      按照执行顺序排列:

      1. initWithCoder:通过nib文件初始化时触发。
      2. awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
      3. loadView:开始加载视图控制器自带的view。
      4. viewDidLoad:视图控制器的view被加载完成。
      5. viewWillAppear:视图控制器的view将要显示在window上。
      6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
      7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
      8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
      9. viewDidAppear:视图控制器的view已经展示到window上。
      10. viewWillDisappear:视图控制器的view将要从window上消失。
      11. viewDidDisappear:视图控制器的view已经从window上消失。

    KVC

    1. KVC(Key-value coding)键值编码,是指在iOS的开发中,允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。

    2. 设值

      当调用setValue:属性值 forKey:<key>的代码时,底层的执行机制如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
      注:如果没有找到set<Key>:方法,KVC机制会检查+(BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:。
      2). 如果set方法不存在,且开发者未实现上个方法,并返回No,就会查找与<key>相同名称并且带下划线的成员变量_<key>,如果有,则直接给成员变量属性赋值。
      3). 如果没有找到_<key>,就会查找_is<Key>的成员变量。
      4). 和上面一样,如果还没找到,会继续搜索<key>和is<Key>的成员变量。再给它们赋值。
      5). 如果还没有找到,则调用setValue:forUndefinedKey:方法。
    3. 取值

      在KVC中取值和设值略有不同,当调用valueForKey:<key>的代码时,其搜索方式如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      1). 首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
      2). 如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
      3). 如果上面的方法没有找到,那么会同时查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>,enumeratorOf<Key>,memberOf<Key>组合的形式调用。
      后面就和取值调用方法类似了。
      4). 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
      5). 还没有找到的话,调用valueForUndefinedKey:
    4. KeyPath

      一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath

      例如:[people1 setValue:@"USA" forKeyPath:@"address.country"];

      如果你不小心错误的使用了key而非keyPath的话,比如上面的代码中KVC会直接查找 address.country 这个属性,很明显,这个属性并不存在,所以会再调用 undefinedKey 相关方法。而KVC对于keyPath的搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。

    5. KVC的使用

      有篇文章写的不错,这里指一下他的链接地址,大家有兴趣可以深入了解下:(https://www.jianshu.com/p/45cbd324ea65)

    KVO

    1. 介绍

      1
      2
      3
      4
      KVO 提供一种机制,指定一个被观察对象(例如 A 类),当对象某个属性(例如 A 中的字符串 name)发生更改时,对象会获得通知,并作出相应处理;【且不需要给被观察的对象添加任何额外代码,就能使用 KVO 机制】
      在 MVC 设计架构下的项目,KVO 机制很适合实现 mode 模型和 view 视图之间的通讯。
      例如:代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过 KVO 再在控制器使用回调方法处理实现视图 B 的更新;
    2. 原理

      当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

      1
      Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
      • NSKVONotifying_A* 类剖析:在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A 类,来实现当前类属性值改变的监听;

        所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。

      • isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

      • 子类setter方法剖析:KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用 2 个方法:

        被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

    3. 使用

      A.注册观察者:

      1
      2
      3
      4
      5
      6
      //第一个参数 observer:观察者 (这里观察self.myKVO对象的属性变化)
      //第二个参数 keyPath: 被观察的属性名称(这里观察 self.myKVO 中 num 属性值的改变)
      //第三个参数 options: 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
      //第四个参数 context: 上下文,可以为 KVO 的回调方法传值(例如设定为一个放置数据的字典)
      [self.myKVO addObserver:self forKeyPath:@"num" options:
      NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

      B. 属性(keyPath)的值发生变化时,收到通知,调用以下方法:

      1
      2
      3
      4
      5
      6
      7
      //keyPath:属性名称
      //object:被观察的对象
      //change:变化前后的值都存储在 change 字典中
      //context:注册观察者时,context 传过来的值
      -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
      {
      }

      C.释放观察者

      1
      2
      3
      - (void)dealloc{
      [self removeObserver:self forKeyPath:@"num" context:nil];
      }

    注意KVO的响应和KVO观察的值变化是在一个线程上的,所以,大多数时候,不要把KVO与多线程混合起来。除非能够保证所有的观察者都能线程安全的处理KVO。

    Notification

    1. 介绍:

      通知就是以Notification的形式从通知发送者发出,到通知中心,然后再分发给所有监听该通知的对象的,通知监听者们接收到通知之后,可以获取到传递过来的Notification对象,从而获取里面封装的一些信息,做相应的处理。

    2. NSNotificationCenter(通知中心)

      通知中心是整个通知机制的关键所在,它管理着监听者的注册注销,通知的发送接收。通知中心维护着一个通知的分发表,把所有发送者发送的通知,转发给对应的监听者们。每一个iOS程序都有一个唯一的通知中心,你不必自己去创建一个,它是一个单例,通过[NSNotificationCenter defaultCenter]方法获取。

      注册监听者方法:

      1
      2
      - (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
      - (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;

      第一个方法是大家常用的方法,不用多说,第二个方法带了一个block,这个block就是通知被触发时要执行的block,这个block带有一个notification参数;该方法还有一个queue参数,可以指定这个block在哪个队列上执行,如果传nil,这个block将会在发送通知的线程中同步执行。然后注意到,这个方法有一个id类型的返回值,这个返回值是一个不透明的中间值,用来充当监听者,使用时,我们需要将这个返回的监听者保存起来,在后面移除监听者的时候用到。

      移除监听者方法:

      1
      2
      - (void)removeObserver:(id)observer;
      - (void)removeObserver:(id)observer name:(nullable NSString *)aName object:(nullable id)anObject;

      在监听对象销毁前,记得把该对象监听的通知移除掉。

      发送通知方法:

      1
      2
      3
      - (void)postNotification:(NSNotification *)notification;
      - (void)postNotificationName:(NSString *)aName object:(nullable id)anObject;
      - (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

    注意:

    • 通知中心默认是以同步的方式发送通知的,也就是说,当一个对象发送了一个通知,只有当该通知的所有接受者都接受到了通知中心分发的通知消息并且处理完成后,发送通知的对象才能继续执行接下来的方法。异步发送通知的方法下面会说到。
    • 在一个多线程的程序中,发送方发送通知的线程通常就是监听者接受通知的线程,这可能和监听者注册时的线程不一样。
    1. NSNotificationQueue(通知队列)(通知合并,异步通知)

      • 通知合并

        你可能会这样做,设置一个flag来决定是否还需要发送通知,当第一个通知发出去时,把这个flag设置为不在需要发送通知,那么当相同的事件再发生时,就不会发送相同的通知了。

        问题在于:普通的通知发送方式默认是同步的,通知的发送者需要等到所有的监听者都接收并处理完消息才能接着处理接下来的业务逻辑,也就是说当第一个通知发出的时候,可能还没回来,第二个通知已经发出去了,在你改变flag的值的时候,可能已经发出去若干个通知了…

        这个时候,就需要用到通知队列的通知合并功能了。使用NSNotificationQueueenqueueNotification:postingStyle:coalesceMask:forModes:方法,设置第三个参数coalesceMask的值,来指定不同的合并规则,coalesceMask有3个给定的值:

        1
        2
        3
        4
        5
        typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
        NSNotificationNoCoalescing = 0,//不合并
        NSNotificationCoalescingOnName = 1,//按通知的名字合并
        NSNotificationCoalescingOnSender = 2//按通知的发送者合并
        };

        设置合并规则后再加入到通知队列中,通知队列会按照给定的合并规则,在之前入队的通知中查找,然后移除符合合并规则的通知,这样就达到了只发送一个通知的目的。

        另外,合并规则还可以用|符号连接,指定多个。

      • 异步通知

        使用通知队列的下面2个方法,将通知加到通知队列中,就可以将一个通知异步的发送到当前的线程,这些方法调用后会立即返回,不用再等待通知的所有监听者都接收并处理完。

        1
        2
        - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
        - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSString *> *)modes;

        注意

        ​ 1、如果通知入队的线程在该通知被通知队列发送到通知中心之前结束了,那么这个通知将不会被发送了。

        ​ 2、注意到上面第二个方法中,有一个modes参数,当指定了某种特定runloop mode后,该通知值有在当前runloop为指定mode的下,才会被发出。

        通知队列发送通知有3种类型,也就是上面方法中的postingStyle参数,它有3种取值:

        1
        2
        3
        4
        5
        typedef NS_ENUM(NSUInteger, NSPostingStyle) {
        NSPostWhenIdle = 1,//(空闲时发送)在运行循环处于等待状态时才被发出。
        NSPostASAP = 2,//(尽快发送 Posting As Soon As Possible)会在运行循环的当前迭代完成时被发送给通知中心,如果当前运行循环模式和请求的模式相匹配的话(如果请求的模式和当前模式不同,则通知在进入请求的模式时被发出)。
        NSPostNow = 3//(立即发送)开发者可以在不需要异步调用行为的时候 使用NSPostNow风格(或者通过NSNotificationCenter的postNotification:方法来发送)。在很多编程环境下,我们不仅允许同步的行为,而且希望使用这种行为:即开发者希望通知中心在通知派发之后返回,以便确定观察者对象收到通知并进行了处理。当然,当开发者希望通过合并移除队列中类似的通知时,应该用enqueueNotification...方法,且使用NSPostNow风格,而不是使用postNotification:方法。
        };

    多线程

    性能优化

    1. UITableView 的优化

      1). 正确的复用cell。

      2). 设计统一规格的Cell

      3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;

      4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;

      4). 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!

      5). 减少子视图的层级关系

      6). 尽量使所有的视图不透明化以及做切圆操作。

      7). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示。

      8). 使用调试工具分析问题。

    Runtime

    1. runtime 如何实现 weak 属性

      要实现weak属性,首先要搞清楚weak属性的特点:

      weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

      那么runtime如何实现weak变量的自动置nil?

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

    2. objc中向一个nil对象发送消息将会发生什么?

      在Objective-C中向nil发送消息是完全有效的 ---- 只是在运行时不会有任何作用:

      如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。

      1)如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者long long的整型标量,发送给nil的消息将返回0。

      2)如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0。

      3)如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。

      objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      struct objc_class {
      Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
      if !OBJC2
      Class super_class OBJC2_UNAVAILABLE; // 父类
      const char *name OBJC2_UNAVAILABLE; // 类名
      long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
      long info OBJC2_UNAVAILABLE;// 类信息,供运行期使用的一些位标识
      long instance_size OBJC2_UNAVAILABLE;// 该类的实例变量大小
      struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
      struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
      struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
      struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
      #endif
      } OBJC2_UNAVAILABLE;

      objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。

    3. 什么时候会报unrecognized selector的异常(消息转发)?

      简单来说:当该对象上某个方法,而该对象上没有实现这个方法的时候,会触发异常可以通过 消息转发 进行解决。

      objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。

      objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:

      • Method resolution

        objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程,如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发(Message Forwarding)。

      • Fast forwarding

        如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。

      • Normal forwarding

        这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

    4. 对象如何找到对应的方法去调用

      //方法保存到什么地方?

      对象方法保存到类中,类方法保存到元类(meta class),每一个类都有方法列表methodList

      //明确去哪个类中调用,

      1
      2
      3
      4
      通过isa指针
      * 1.根据对象的isa去对应的类查找方法,isa:判断去哪个类查找对应的方法 指向方法调用的类
      * 2.根据传入的方法编号SEL,里面有个哈希列表,在列表中找到对应方法Method(方法名)
      * 3.根据方法名(函数入口)找到函数实现,函数实现在方法区
    5. 方法交换

      方法交换其实我之前的博客有详细讲过(http://sarieltang.github.io/2016/10/25/总结分析/2016-10-25/index/),下面讲一个比较简单的例子:

      需求:比如我有个项目,已经开发2年,之前都是使用UIImage去加载图片,组长想要在调用imageNamed,就给我提示,是否加载成功,

      1
      2
      3
      4
      5
      1、解决方式 自定义UIImage类,缺点:每次用要导入自己的类
      2、解决方法:UIImage分类扩充一个这样方法,缺点:需要导入,无法写super和self,会干掉系统方法,解决:给系统方法加个前缀,与系统方法区分,如:xmg_imageNamed:
      3、交换方法实现,步骤: 1.提供分类 2.写一个有这样功能方法 3.用系统方法与这个功能方法交换实现,在+load方法中实现
      注意:在分类一定不要重写系统方法,就直接把系统方法干掉,如果真的想重写,在系统方法前面加前缀,方法里面去调用系统方法

      下面是代码:

      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
      #import "UIImage+Image.h"
      #import <objc/message.h>
      @implementation UIImage (Image)
      // 加载类的时候调用,肯定只会调用一次
      +(void)load
      {
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      // 交互方法实现xmg_imageNamed,imageNamed
      Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
      Method xmg_imageNameMethod = class_getClassMethod(self, @selector(xmg_imageNamed:));
      //用runtime对imageNameMethod和xmg_imageNameMethod方法进行交换
      method_exchangeImplementations(imageNameMethod, xmg_imageNameMethod);
      });
      }
      //外界调用imageNamed:方法,其实是调用下面方法,调用xmg_imageNamed就是调用imageNamed:
      + (UIImage *)xmg_imageNamed:(NSString *)name
      {
      //已经把xmg_imageNamed换成imageNamed,所以下面其实是调用的imageNamed:
      UIImage *image = [UIImage xmg_imageNamed:name];
      if (image == nil) {
      NSLog(@"加载失败");
      }
      return image;
      }
      @end

    Runloop

    1. 关于Runloop,AutoreleasePool的简单理解

      • NSRunloop

        通常我们在iOS开发中提到的runloop是指NSRunloop这个由苹果提供的对象,NSRunloop是一个消息循环,它会侦测输入源(input source)和定时源(timer source),然后做回调处理。

        下面是一段苹果官方文档(多线程编程指南)的描述:

        "run loop 是用来在线程上管理事件异步到达的基础设施......run loop在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗CPU周期轮询,并防止处理器本身进入休眠状态并节省电源。"

        所以,NSRunloop的两大作用分别就是:一、消除CPU空转带来的消耗。二、监听异步到达的事件。

        1
        2
        3
        4
        每个线程都有一个默认的NSRunloop。主线程的NSRunloop默认是运行的。非主线程的NSRunloop默认是没有运行的,需要为NSRunloop添加一个事件,然后去run,一般情况下没有必要启用线程的runloop,除非需要长久地监测某个异步事件。
        举个栗子:
        NSURLConnection网络数据请求,默认是异步的方式,其实现原理就是创建之后将其作为事件源加入到当前的 RunLoop,而等待网络响应以及网络数据接受的过程则在一个新创建的独立的线程中完成,当这个线程处理到某个阶段的时候比如得到对方的响应或者接受完了网络数据之后便通知之前的线程去执行其相关的delegate方法。
      • AutoreleasePool

        App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

        • 第一个 Observer 监视的事件是:

          Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

        • 第二个 Observer 监视了两个事件:

          1. BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()* 释放旧的池并创建新池;
          2. Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

    注:在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

    RunLoop 回调函数整理版本:(当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数)

    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
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
    /// 2. 通知 Observers: 即将触发 Timer 回调。
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
    /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
    /// 4. 触发 Source0 (非基于port的) 回调。
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
    /// 6. 通知Observers,即将进入休眠
    /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
    /// 7. sleep to wait msg.
    mach_msg() -> mach_msg_trap();
    /// 8. 通知Observers,线程被唤醒
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
    /// 9. 如果是被Timer唤醒的,回调Timer
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
    /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
    /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
    } while (...);
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);

    Crash日志收集与分析

    1. Crash日志的收集和分析

      通常我们现在的Crash都是通过集成了三方的bugly或者fabric之类的工具来进行收集的,不过这里面还有一些细节需要说明一下。

      通常crash日志显示的时候都是一些堆栈信息,bugly之类的工具虽然能够提供部分代码的大概执行范围,但是通常都不够全面。

      这个时候就需要一个非常重要的东西来做补充了,那就是 dYSM文件 也就是我们常说的符号表

      如果用的是第三方的包括bugly或者fabric之类的SDK,也可以直接通过脚本的方式将符号表文件在打包阶段上传到bugly或者fabric上面。

      那么说了那么久的符号表,符号表究竟有什么作用呢?

      简单来说,符号表记录了你的方法的详细位置,我们可以通过符号表非常方便的找到崩溃信息。

      比如这么一段crash日志:

      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
      NSConcreteMutableAttributedString addAttribute:value:range:: nil value
      (null)
      ((
      CoreFoundation 0x0000000185c642f4 + 160
      libobjc.A.dylib 0x00000001974880e4 objc_exception_throw + 60
      CoreFoundation 0x0000000185c64218 + 0
      Foundation 0x0000000186a9dfb4 + 152
      Xmen 0x10073fb30 Xmen + 7600944
      Xmen 0x1006bbbf4 Xmen + 7060468
      UIKit 0x000000018a9a47fc + 60
      UIKit 0x000000018a9a512c + 104
      UIKit 0x000000018a6b2b6c + 88
      UIKit 0x000000018a9a4fd4 + 444
      UIKit 0x000000018a78e274 + 1012
      UIKit 0x000000018a999aac + 2904
      UIKit 0x000000018a785268 + 172
      UIKit 0x000000018a6a1760 + 580
      QuartzCore 0x0000000189fe9e1c + 152
      QuartzCore 0x0000000189fe4884 + 320
      QuartzCore 0x0000000189fe4728 + 32
      QuartzCore 0x0000000189fe3ebc + 276
      QuartzCore 0x0000000189fe3c3c + 528
      QuartzCore 0x0000000189fdd364 + 80
      CoreFoundation 0x0000000185c1c2a4 + 32
      CoreFoundation 0x0000000185c19230 + 360
      CoreFoundation 0x0000000185c19610 + 836
      CoreFoundation 0x0000000185b452d4 CFRunLoopRunSpecific + 396
      GraphicsServices 0x000000018f35b6fc GSEventRunModal + 168
      UIKit 0x000000018a70afac UIApplicationMain + 1488
      Xmen 0x1008cf9c0 Xmen + 9238976
      libdyld.dylib 0x0000000197b06a08 + 4
      )
      dSYM UUID: 30833A40-0F40-3980-B76B-D6E86E4DBA85
      CPU Type: arm64
      Slide Address: 0x0000000100000000
      Binary Image: Xmen
      Base Address: 0x000000010007c000

      直观来看是无法直观的了解到crash的原因的。

      那么,符号表文件又是怎么来定位方法的实现,以便告诉我们那个位置crash的呢?下面我简单做个人工的分析

      首先,我们提取出几个关键信息:

      • NSConcreteMutableAttributedString addAttribute:value:range:: nil value 崩溃的原因是value为nil
      • “4 Xmen 0x10073fb30 Xmen + 7600944” 它指出了应用名称,崩溃时的调用方法的地址,文件的地址以及方法所在的行的位置,我们需要的是这一个:”0x10073fb30”。
      • “dSYM UUID: 30833A40-0F40-3980-B76B-D6E86E4DBA85”。
      • “CPU Type: arm64”。

      然后验证一下崩溃日志里的uuid和本地的dYSM文件是否匹配:

      dwarfdump --uuid Xmen.app.dSYM

      结果:

      UUID: BFF6AE00-8B5F-39BD-AFD0-27707C489B25 (armv7) Xmen.app.dSYM/Contents/Resources/DWARF/Xmen

      UUID: 30833A40-0F40-3980-B76B-D6E86E4DBA85 (arm64) Xmen.app.dSYM/Contents/Resources/DWARF/Xmen

      发现与我们日志中的:UUID和CPU Type是相匹配的

      然后,我们通过上面拿到的这些关键信息来获取具体的错误信息

      dwarfdump --arch=arm64 --lookup 0x10073fb30 /Dandy/XMEN/上线版本/2.0.17_105/aaaa.xcarchive/dSYMs/Xmen.app.dSYM/Contents/Resources/DWARF/Xmen

      然后我们就会拿到这么一段结果

      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
      File: /Dandy/XMEN/上线版本/2.0.17_105/aaaa.xcarchive/dSYMs/Xmen.app.dSYM/Contents/Resources/DWARF/Xmen (arm64)
      Looking up address: 0x000000010073fb30 in .debug_info... found!
      0x00219b05: Compile Unit: length = 0x00003dd0 version = 0x0002 abbr_offset = 0x00000000 addr_size = 0x08 (next CU at 0x0021d8d9)
      0x00219b10: TAG_compile_unit [107] *
      AT_producer( "Apple LLVM version 8.0.0 (clang-800.0.42.1)" )
      AT_language( DW_LANG_ObjC )
      AT_name( "/Dandy/checkSvn/IOS/xmen/Xmen/Modules/StoreManage/SellerOrder/View/DSSellerOrderSectionHeaderView.m" )
      AT_stmt_list( 0x001272a9 )
      AT_comp_dir( "/Dandy/checkSvn/IOS/xmen" )
      AT_APPLE_major_runtime_vers( 0x02 )
      AT_low_pc( 0x000000010072b8ac )
      AT_high_pc( 0x000000010074e350 )
      0x0021aec5: TAG_subprogram [119] *
      AT_low_pc( 0x0000000100739810 )
      AT_high_pc( 0x000000010074006c )
      AT_frame_base( reg29 )
      AT_object_pointer( {0x0021aee3} )
      AT_name( "-[DSSellerOrderSectionHeaderView updateContentWithOrderData:isEdit:]" )
      AT_decl_file( "/Dandy/checkSvn/IOS/xmen/Xmen/Modules/StoreManage/SellerOrder/View/DSSellerOrderSectionHeaderView.m" )
      AT_decl_line( 248 )
      AT_prototyped( 0x01 )
      0x0021af36: TAG_lexical_block [138] *
      AT_ranges( 0x00008640
      [0x000000010073cf90 - 0x000000010073fb88)
      [0x000000010073fbc0 - 0x000000010073fbc4)
      End )
      Line table dir : '/Dandy/checkSvn/IOS/xmen/Xmen/Modules/StoreManage/SellerOrder/View'
      Line table file: 'DSSellerOrderSectionHeaderView.m' line 680, column 9 with start address 0x000000010073faf8
      Looking up address: 0x000000010073fb30 in .debug_frame... not found.

      Line table dir : '/Dandy/checkSvn/IOS/xmen/Xmen/Modules/StoreManage/SellerOrder/View'

      上面这一行告诉我们崩溃的代码所在的文件的目录

      AT_decl_file( "/Dandy/checkSvn/IOS/xmen/Xmen/Modules/StoreManage/SellerOrder/View/DSSellerOrderSectionHeaderView.m")

      上面这一行告诉我们崩溃代码所在的具体文件

      AT_name( "-[DSSellerOrderSectionHeaderView updateContentWithOrderData:isEdit:]" )

      上面这一行告诉我们崩溃代码是在哪一个方法里面

      Line table file: 'DSSellerOrderSectionHeaderView.m'line 680, column 9 with start address 0x000000010073faf8

      最后这一行告诉我们崩溃代码具体在哪一行了。

    当我们把dSYMs文件上报给第三方后,第三方就会有类似的处理,帮助将一些crash日志给我们定位到具体的方法。当然,如果遇到一些情况无法显示具体crash位置时,我们还是可以通过手动分析dSYMs文件的方式去查找崩溃的位置和原因。

    1. 手机上的crash查看

      下面再介绍一个平时在使用release版本的应用时,在自己手机上出现的crash,怎么直接去分析的方法。

      当我们手机上自己的应用突然出现crash的时候,可以通过将iPhone连接到mac上,然后通过xcode中Window菜单的Devices来找到自己的手机,选中我们的app后,通过点击ViewDeviceLogs来看到你自己设备中的所有崩溃信息,可以根据时间进行排序。

    crash_header

    ​ 崩溃信息大致大致可以分为以上几个部分,下面来详细介绍:

    第一部分是闪退进程的相关信息:

    ​ Incident Identifier : 是崩溃报告的唯一标识符。

    ​ CrashReporter Key: 是与设备标识相对应的唯一键值。虽然它不是真正的设 备标识符,但也是一个非常有用的情报:如果你看到100个崩溃日志的CrashReporter Key值都是相同的,或者只有少数几个不同的CrashReport值,说明这不是一个普遍的问题,只发生在一个或少数几个设备上。

    ​ Hardware Model :标识设备类型。 如果很多崩溃日志都是来自相同的设备类型,说明应用只在某特定类型的设备上有问题。上面的日志里,崩溃日志产生的设备是iPhone 4s。

    ​ Process:对项目的操作权限,上面的是可读可写

    ​ Path:崩溃文件的路径

    ​ Identifier:项目标识符,就是Bundle Id

    ​ Version:版本号

    ​ …..等等…….

    第二部分

    ​ 给出了一些基本信息,包括闪退发生的日期Date/Time和时间Launch Time, 设备的iOS版本OS Version等。

    第三部分
    ​ Exception Type:异常的类型。
    ​ Exception Codes :异常错误码
    ​ Termination Reason:闪退的原因,比如常见的数组越界啊,什么的。
    ​ Triggered by Thread:出现问题在哪个线程,这个比较重要,首先确定在哪个线程中出了问题,然后再去定位。

    第四部分
    ​ 这部分提供应用中所有线程的回溯日志。 线程调用的一些,堆栈信息。

    1. 常见的异常代码(Exception Codes)

      异常代码可能包含异常类型(Exception Type)异常子类型(Exception Subtype)处理器的详细异常代码(processor-specific Exception Codes)和其它能提供更多Crash信息的字段,最后一个字段列出了触发Crash的线程索引。下面是异常代码的示例。

      常见的异常类型有以下几种。

      a. Bad Memory Access [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]

      此类型的Excpetion是最常见的Crash,通常由访问了无效的内存导致。

      SIGSEGV:访问了无效地址,没有物理内存对应该地址,通常由于重复释放对象导致。

      SIGBUS:总线错误,与 SIGSEGV 不同的是,SIGBUS 访问的是有效地址,但总线访问异常,通常是访问了未对齐的数据。

      SEGV:代表无效内存地址,比如空指针、未初始化指针、栈溢出等。

      b. Abnormal Exit [EXC_CRASH // SIGABRT]

      进程异常退出,造成Crash通常是因为未捕获到Objective-C/C++的异常。

      SIGABRT:收到Abort信号退出,通常Foundation库中的容器为了保护状态正常会做一些检测,例如插入nil到数组中等会遇到此类错误。

      c. 其它异常类型

      有些异常类型没有被命名,以16进制数字表示。

      0xbaaaaaad:意味着该Crash log并非一个真正的Crash,它仅仅只是包含了整个系统某一时刻的运行状态,由用户同时按Home键和音量键触发。

      0xbad22222:当VoIP程序在后台太过频繁的激活时,系统可能会终止此类程序。

      0x8badf00d:程序启动或者恢复时间过长被watch dog终止。

      0xc00010ff:程序执行大量耗费CPU和GPU的运算,导致设备过热,触发系统过热保护被系统终止。

      0xdead10cc:程序退到后台时还占用系统资源(如通讯录)被系统终止。

      0xdeadfa11:程序无响应用户强制退出。当用户长按电源键,直到屏幕出现关机确认画面后再长按Home键,将强制退出应用。我们可以合理认为用户这么做的原因是应用程序没有响应。

    2. 下面我简单对于上面几个异常做一些更加细致的描述

      • SIGSEGV

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        对于不正确的内存处理,如当程序企图访问 CPU 无法定址的内存区块时,计算机程序可能抛出 SIGSEGV。操作系统可能使用信号栈向一个处于自然状态的应用程序通告错误,由此,开发者可以使用它来调试程序或处理错误。
        在一个程序接收到 SIGSEGV 时的默认动作是异常终止。这个动作也许会结束进程,但是可能生成一个核心文件以帮助调试,或者执行一些其他特定于某些平台的动作。
        SIGSEGV可以被捕获。也就是说,应用程序可以请求它们想要的动作,以替代默认发生的动作。这样的动作可以是忽略它、调用一个函数,或恢复默认的动作。在一些情形下,忽略 SIGSEGV 导致未定义行为。
        一个应用程序可能处理SIGSEGV的例子是调试器,它可能检查信号栈并通知开发者目前所发生的,以及程序终止的位置。
        SIGSEGV通常由操作系统生成,但是有适当权限的用户可以在需要时使用kill系统调用或kill命令(一个用户级程序,或者一个shell内建命令)来向一个进程发送信号。
        闪退场景一:recorder deleteRecording 之前 先判断文件是否存在,否则会造成过度释放,解决方法:
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.recorder.url.path]) {
        if (![self.recorder deleteRecording])
        NSLog(@"Failed to delete %@", self.recorder.url);
        }
        闪退场景二: delegate = nil 。
        将XXViewContrller设置为delegate时,当页面发生跳转时,XXViewController的对象会被释放,这是代码走到[_delegate callbackMethod],便出现crash。解决方法有二:1.将@property (nonatomic ,assign) id <BLELibDelegate>delegate; 中 assign关键字改为weak。 2.在XXViewController的delloc方法中添加:xxx.delegate = nil;
      • SIGBUS

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        在POSIX兼容的平台上, bus error (总线错误) 是系统检测到硬件问题后发送给进程的信号,但是通常该信号的产生不是因为硬件有物理上的损坏,更多的还是程序 bug 导致的。
        总线错误几乎总是由于对未对齐的读或写引起的。它之所以称为总线错误是因为对未对齐的内存访问时,被阻塞的组件就是地址总线。
        对齐(alignment)数据项只能存储在地址是数据项大小的整数倍的内存位置上,这样可以加速内存访问。如:访问一个8字节的double的数据时,地址只能是8的整数倍,所以存储一个double的地址只能是24,8008,但不能存储于地址1006因为它不能被8整除,只要保证这个原则,就可以保证一个原子项数据不会跨页或cache块的边界。
        引起总线错误的示例:
        union
        {
        char a[10];
        int i;
        }u;
        int *p =(int*)&(u.a[1]);
        *p =17;/*p中未对齐的地址将会引起总线错误*/
        因为数组和int的联合确保了a是按照int的4字节来对齐的,所以“a+1”肯定不是int来对齐的。
      • SIGABRT

        1
        2
        3
        4
        5
        6
        7
        8
        SIG是信号名的通用前缀。ABRT是abort program的缩写。
        当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制,必要的话,它能要求进程进行清理工作。在调试造成此信号的底层错误时,并没有什么妙招。 如 cocos2d 或 UIKit 等框架通常会在特定的前提条件没有满足或一些糟糕的情况出现时调用 C 函数 abort (由它来发送此信号)。
        如果是iOS系统:
        发生在UIApplication WillTerminate 时,是主动退出应用时发生的,所以对用户没什么实际影响。
        iOS10访问相册时发生,目前只发生在iOS10+系统,需要修改工程plist文件,加入访问权限提示信息。
        补充:iOS 10 has updated privacy policy and implemented new privacy rules. You have to update your Info.plist app with this following fields by authorisation asked.

    本作品采用 署名-非商业性使用-相同方式共享 2.5 中国大陆 (CC BY-NC-SA 2.5)协议 进行许可,欢迎转载,但转载请注明来自SarielTang,并保持转载后文章内容的完整。本人保留所有版权相关权利。

    本文永久链接:http://sarieltang.github.io/2018/01/23/总结分析/2018-01-23/index/