iOS老司机两万字整理, 可能是最全的iOS实际业务场景相关Tips

1,243 阅读1小时+

一、iOS基础篇

RunLoop

image.png

RunLoop模式有哪些?

  • NSDefaultRunLoopMode, 默认的模式, 有事件响应的时候, 会阻塞旧事件
  • NSRunLoopCommonModes, 普通模式, 不会影响任何事件
  • UITrackingRunLoopMode, 只能是有事件的时候才会响应的模式
  • App刚启动的时候会执行一次的模式
  • 系统检测App各种事件的模式

RunLoop的基本执行原理

  • 原本系统就有一个RunLoop在检测App内的事件, 当输入源有执行操作的时候, 系统的RunLoop会监听输入源的状态, 进而在系统内部做一些对应的操作. 处理完事件后, 会自动回到睡眠状态, 等待下一次被唤醒.

RunLoop和线程的关系

  • 在默认情况下, 线程执行完之后就会退出, 就不能再继续任务了. 这时我们需要采用一种方式来让线程能够不断地处理任务, 并不退出. 所以, 我们就有了RunLoop.
  1. 一条线程对应一个RunLoop对象, 每条线程都有唯一一个与之对应的RunLoop对象.
  2. RunLoop并不保证线程安全. 我们只能在当前线程内部操作当前线程的RunLoop对象, 而不能在当前线程内部去操作其他线程的RunLoop对象方法.
  3. RunLoop对象在第一次获取RunLoop时创建, 销毁则是在线程结束的时候.
  4. 主线程的RunLoop对象系统自动帮助我们创建好了(UIApplicationMain函数), 子线程的RunLoop对象需要我们主动创建和维护.
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

CoreFoundation框架下关于RunLoop的5个类

  1. CFRunLoopRef: 代表RunLoop的对象
  2. CFRunLoopModeRef: 代表RunLoop的运行模式
  3. CFRunLoopSourceRef: 就是RunLoop模型图中提到的输入源(事件源)
  4. CFRunLoopTimerRef: 就是RunLoop模型图中提到的定时源
  5. CFRunLoopObserverRef: 观察者, 能够监听RunLoop的状态改变.
  • 一个RunLoop对象中包含若干个运行模式.每一个运行模式下又包含若干个输入源、定时源、观察者.
    • 每次RunLoop启动时, 只能指定其中一个运行模式, 这个运行模式被称作当前运行模式CurrentMode.
    • 如果需要切换运行模式, 只能退出当前Loop, 再重新指定一个运行模式进入.
    • 这样做主要是为了分隔开不同组的输入源、定时源、观察者, 让其互不影响. image.png

NSTimer中的scheduledTimerWithTimeInterval方法和RunLoop的关系.

NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
 
/**
 上面这句代码调用了scheduledTimer返回的定时器,
 NSTimer会自动加入到RunLoop的NSDefaultRunLoop模式下, 相当于下面两句代码.
*/
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; 
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

/**
 因为默认已经添加了NSDefaultRunLoopMode, 所以只给timer1添加了UITrackingRunLoopMode后,
 效果跟添加了NSRunLoopCommonModes一致, 拖动也不影响定时器
*/ 
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:UITrackingRunLoopMode];

// 开发中推荐使用
NSTimer *timer = [NSTimer timerWithTimeInterval:duration target:self selector:@selector(cs_toastTimerDidFinish:) userInfo:toast repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

为什么说NSTimer不准确

  • NSTimer的触发时间到的时候, runloop如果在阻塞状态, 触发时间就会推迟到下一个runloop周期
  • 可利用GCD优化
NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, 
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));

dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);

dispatch_source_ser_evernt_handler(_timer, ^{
    NSLog(@"GCD timer test");
});

dispatch_resume(_timer);

CFRunLoopSourceRef

  • CFRunLoopSourceRef是事件源, 有两种分类方法.
  1. 按照官方文档来分类
    • Port-Based Sources (基于端口)
    • Custom Input Sources (自定义)
    • Cocoa Perform Selector Sources
  1. 按照函数调用栈来分类
    • Source0: 非基于Port
    • Source1: 基于Port, 通过内核和其他线程通信, 接收、分发系统事件

RunLoop原理

image.png

  • 在每次运行开启RunLoop的时候, 所在线程的RunLoop会自动处理之前未处理的事件, 并且通知相关的观察者.
  1. 通知观察者RunLoop已经启动
  2. 通知观察者即将要开始定时器
  3. 通知观察者任何即将启动的非基于端口的源Source0
  4. 启动任何准备好的非基于端口的源Source0
  5. 如果基于端口的源Source1准备好并处于等待状态, 立即启动, 并进入步骤9
  6. 通知观察者线程进入休眠状态
  7. 将线程置于休眠直到下面任一种事件发生:
    • 某一事件到达基于端口的源Source1
    • 定时器启动
    • RunLoop设置的时间已经超时
    • RunLoop被显示唤醒
  1. 通知观察者线程将被唤醒
  2. 处理未处理的事件
    • 如果用户定义的定时器启动, 处理定时器事件并重启RunLoop, 进入步骤2
    • 如果输入源启动, 传递相应的消息
    • 如果RunLoop被显示唤醒而且时间还没超时, 重启RunLoop. 进入步骤2
  1. 通知观察者RunLoop结束.

RunLoop使用

  1. NSTimer不被手势操作影响
  2. 滑动tableview时cell中的ImageView推迟显示
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];

3. 后台常驻线程

- (void)viewDidLoad {
    // 创建线程,并调用run1方法执行任务 
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil]; 
    // 开启线程 
    [self.thread start];
}

- (void)run1 {
    // 这里写任务 
    NSLog(@"----run1-----");
    // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理 
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; 
    [[NSRunLoop currentRunLoop] run];
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。 
    NSLog(@"未开启RunLoop");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务 
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run2 {
     NSLog(@"----run2-----");
}


如何用RunLoop原理去监控卡顿

  • 戴銘RunLoop示意图 image.png
  • 卡顿跟FPS关系不大, 24帧的动画也是流畅的
  • 通过监控RunLoop的状态, 就能够发现调用方法是否执行时间过长, 从而判断出是否会出现卡顿.

image.png

  1. 要想监听 RunLoop,你就首先需要创建一个 CFRunLoopObserverContext 观察者,代码如下:
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);

2. 将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察。然后,创建一个持续的子线程专门用来监控主线程的 RunLoop 状态。 3. 一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。 4. 接下来,我们就可以通过三方库PLCrashReporter dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。

RunTime, objc_msgSend消息机制

  • 使用C、C++、汇编写成的, 提供运行时机制
  • 1234 123
  • 类的结构体
//Class也表示一个结构体指针的类型
typedef struct objc_class *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;

  • 分类结构体
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
  • sel-imp
    • objc_msgSend
    • CacheLoopup
      • 有缓存机制
      • CacheHib找到缓存
      • CheckMiss MethodTableLoopup
      • add
  • 对象 结构体 class(方法列表、属性列表、协议列表)
  • 快速查找、慢速查找
  • isa走位图 image.png
  • 对象 -> 类 -> 元类
  • 实例方法 存在类对象里面
  • 类方法 存在元类里面
  • 递归查找

消息机制

  • Objective-C采用消息发送策略,选择器向接收器发送消息,编译阶段无法知道对象是否有对应的方法,

  • 运行时根据isa指针,找到对象所属的类结构体,然后结合类中的缓存方法列表指针和虚函数指针找到选择器对应的SEL选择器类型变量,

  • 如果找到则SEL变量对应的IMP指针找到方法实现.

  • 如果找不到对应的方法,则会启动消息转发机制,如果仍然失败,抛出异常或崩溃.

  • objc_msgSend 方法执行的逻辑是:先获取对象对应类的信息,再获取方法的缓存,根据方法的 selector 查找函数指针,经过异常错误处理后,最后跳到对应函数的实现。

消息转发流程

image.png

#import "NSObject+JH.h"

@implementation NSObject (JH)

void jh_dynamicMethodIMP() {
    NSLog(@"%s", __func__);
}


+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 第一次机会
    NSLog(@"来了, 老弟");
    
    // 方法 -- 动态化
    // jh_run
    if (sel == @selector(run)) {
        class_addMethod([self class], sel, (IMP)jh_dynamicMethodIPM, @"v@:");
        return YES;
    }
    return NO;
}
  • 当前类中
// 第二次机会
- (id)forwardingTargetForSelector:(SEL)aSelector {
    //
    NSLog(@"%s", __func__);
    
    return [super forwardingTargetForSelector:aSelector];
}


// 第三次机会
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 方法签名
    if (aSelector == @selector(run)) {
        Method method = class_getInstanceMethod([self class], @selector(runMethod));
        const char *type = method_getTypeEncoding(method);
        return [NSMethodSignature signatureWithObjCTypes:type];
    }
    return [NSMethodSignature methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // pop alert
    NSLog(@"来了:%s-%@", __func__, NSStringFromSelector(anInvocation.selector));
    anInvocation.selector = @selector(runMethod);
    [anInvocation invoke];
}
- (void)runMethod {
    NSLog(@"%s", __func__);
}

类簇

1. 什么是类簇?

  • 类簇的英文名是class cluster, cluster本意是一群的意思, 简单的说类簇就是一组类, 下面是官方文档解释 A class cluster is an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. The grouping of classes in this way provides a simplified interface to the user, who sees only the publicly visible architecture.

类簇是一种体系结构,它将许多私有的、具体的子类组合在一个公共的、抽象的超类下。 以这种方式对类进行分组为用户提供了一个简化的界面,用户只能看到公开可见的架构。

  • 我们只需要调用基类提供的接口来实现相关功能, 而无需关心背后的具体实现细节.
  • 例如UIButton有一个工厂方法(UIButton *)buttonWithType:(UIButtonType)Type,
    • 调用这个方法可以返回具体的实例类, 此时UIButton就是一个抽象的基类, 返回的是他子类的实例,
    • 然后我们再调用基类提供的接口完成相关逻辑
// 返回子类的实例
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
// 我们只需要调用基类提供的方法,而不关心具体子类的实现
[button setImage:[UIImage imageNamed:@"1.png"] forState:UIControlStateNormal];


// 如果没有使用类簇模式的话, 上面的代码可能写成
RoundedRectButton *button =[RoundedRectButton alloc] init];
[button setImage:[UIImage imageNamed:@"1.png"] forState:UIControlStateNormal];
  • 如果子类很多, 我们需要针对每个子类都有一种写法 image.png

  • 测试NSArray

NSLog(@"test Class Cluster %@  %@  %@", [[NSArray arrayWithArray:@[@1, @2]] class], [[NSArray arrayWithObjects:@"1", @"2", nil] class], [[NSArray new] class]);

打印结果:
**test Class Cluster __NSArrayI  __NSArrayI  __NSArray0**

2. 使用类簇有什么好处?

  • 使用类簇对于开发者而言很方便维护和拓展, 具体的实现代码放在对应的子类里,
  • 加入我们想再拓展, 只需要继承子类, 重写对外的接口即可,
  • 对于调用者而言十分简单明了, 不用去关心子类是如何实现, 只需要调用基类提供的接口即可.

3. 需求实战

  • 用户点击下载按钮,如果下载成功显示成功的提醒,下载失败显示一个按钮允许用户点击后再次下载,那么我们可以按照下列实现
  • 思路:
    • 创建一个基类HintView,实现一个工厂方法,根据传入的不同值生成不同的的子类实例 ,并定义一个 showToView: 接口,由子类具体实现方法
    • 创建两个子类SuccessHintView,FailHintView,分别根据下载成功和失败的情况实现具体的showshowToView:方法
  1. 定义一个基类HintView.h
HintView.h:
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger,HintViewType){
    HintViewTypeSuccess,
    HintViewTypeFail
};

@interface HintView : NSObject

+ (HintView *)viewWithType:(HintViewType)type;
- (void)showToView:(UIView *)view;

@end


HintView.m:
@implementation HintView
+ (HintView *)viewWithType:(HintViewType)type{
    
    HintView *view = nil;
    switch (type) {
        case HintViewTypeSuccess:{
            view=  [[SuccessHintView alloc] init];
            break;
        }
        case HintViewTypeFail:{
            view=  [[FailHintView alloc] init];
            break;
        }
    }
    return view;
}

- (void)showToView:(UIView *)view {
    // subClass implement this
};

2. 然后分别在对应的子类实现方法

SuccessHintView.m:
#import "SuccessHintView.h"

@implementation SuccessHintView

- (void)showToView:(UIView *)view{
    CGPoint center = view.center;
    UILabel *lable = [[UILabel alloc] init];
    lable.frame = CGRectMake(0, 0, 200, 200);
    lable.center = center;
    lable.textAlignment = NSTextAlignmentCenter;
    lable.backgroundColor  = [UIColor redColor];
    [view addSubview:lable];
    lable.text = @"恭喜你下载成功";
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [lable removeFromSuperview];
    });
}


FailHintView.m:
#import "FailHintView.h"
@implementation FailHintView

- (void)showToView:(UIView *)view{
    CGPoint center = view.center;
    UIButton *button = [[UIButton alloc] init];
    button.frame = CGRectMake(0, 0, 200, 100);
    button.center = center;
    [view addSubview:button];
    [button setTitle:@"下载失败,点击我重试" forState:UIControlStateNormal];
    button.backgroundColor = [UIColor redColor];
    [button addTarget:self action:@selector(didCLickButton:) forControlEvents:UIControlEventTouchUpInside];

}

- (void)didCLickButton:(UIButton *)button{
    [button removeFromSuperview];
}

类簇总结

  • 类簇(class cluster)是一种设计模式,在Foundation Framework中被广泛使用.
  • 类簇的本质其实是抽象工程,类簇也可以有多个基类,如NSArrayNSMutableArray, 后者就是继承的前者。它对一些「大同小异」的问题,往往会有不错的效果。

1. 对象方法和类方法的区别?

  • 对象方法能访问成员变量; 类方法不能访问成员变量.
  • 类方法中不能直接调用对象方法, 想要调用对象方法, 必须创建或传入对象.
  • 类方法可以和对象方法重名.

1.1 如果在类方法中调用self会有什么问题?

  • 在实例方法中self不可以调用类方法, 此时的self不是Class.
  • 在类方法中的self可以调用其他类方法.
  • 在类方法中self不可以调用实例方法.
  • 类方法中的self是class; 实例方法中self是对象的首地址.

1.2 讲一下对象、类对象、元类、跟元类结构体组成以及他们是如何相关联的?

  • 对象的结构体当中存方法isa指针和成员变量, isa指针指向类对象.
  • 类对象的isa指针指向元类, 元类的isa指针指向NSObject的元类.
  • 类对象和元类的结构体有isa、superClass、cache等等.

1.3 为什么对象方法中没有保存对象结构体里面, 而是保存在类对象的结构体里面?

  • 方法是每个对象相互可以共用的, 如果每个对象都存储一份方法列表太浪费内存, 由于对象的isa是指向类对象的, 当调用的时候, 直接去类对象中查找就可以了, 节约了内存空间.

1.4 类方法存在哪里? 为什么要有元类的存在?

  • 所有的类自身也是一个对象, 我们可以向这个对象发送消息(调用类方法).
  • 为了应采用类方法, 这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体. 这就引出了meta-class的概念, 元类中保存了创建类对象以及类方法所需的所有信息.

1.5 什么是野指针? 如何检测野指针

  • 野指针就是指向一个被释放或者被回收的对象, 但是对指向该对象的指针没有做任何修改, 以至于该指针让指向已经回收后的内存地址.
  • 访问野指针是没有问题的, 使用野指针的时候会出现Crash, 可用__unsafe_unretained模拟.
  • XCode Edit Scheme -> Diagnostics -> Memory Management -> Zombie Objects
1.5.1 __unsafe_unretained__weak的区别
  • __unsafe_unretained: 不会对对象进行retain, 当对象销毁时, 会依然指向之前的内存空间(野指针).
  • __weak: 不会对对象进行retain, 当对象销毁时, 会自动指向nil.

1.6 导致闪退的原因有哪些?

  • 找不到方法的实现unrecognized selector sent to instance
  • KVC、KVO造成的闪退
  • 集合类数组越界造成闪退
  • 坏内存访问EXC_BAD_ACCESS
  • 多线程造成闪退
  • Socket长连接, 进入后台没有关闭造成闪退
  • WatchDog超时造成闪退
  • 后台返回NSNull造成闪退

1.7 不使用第三方, 如何知道已经上线的App崩溃问题, 具体到哪一个类的哪一个方法?

  • 使用NSSetUncaughtExceptionHandler可以统计闪退的信息.
  • 将统计到的信息以data的形式发送给后台服务器
  • 在后台手机信息, 进行排查

2. iOS中内省的几个方法?

  • isMemberOfClass, 对象是否是某个类型的对象.
  • isKindOfClass, 对象是否是某个类型或某个类型子类的对象.
  • isSubclassOfClass, 某个类对象是否是另一个类型的子类.
  • isAncestorOfObject, 某个类对象是否是另一个类型的父类.
  • respondsToSelector, 是否能响应某个方法.
  • conformsToProtocol, 是否遵循某个协议.

2.1 ==isEqualToStringisEqual区别?

  • ==, 比较的是两个指针的值(内存地址是否相同).
  • isEqualToString, 比较的是两个字符串是否相等.
  • isEqual, 判断两个对象在类型和值上是否都一样.

2.2 class方法和object_getClass方法有什么区别?

  • 实例class方法直接返回object_getClass(self).
  • 类class直接返回self.
  • object_getClass(类对象), 则返回的是元类.

3. 深拷贝和浅拷贝

  • 所谓深浅指的是, 是否创建了一个新的对象(开辟了新的内存地址), 还是仅仅做了指针的复制.
  • copymutableCopy针对的是可变和不可变, copy结果均为不可变,mutableCopy结果均为可变.
  • mutableCopy均是深复制.
  • copy操作不可变的是浅复制, 操作可变的是深复制.

4. NSString类型为什么要用copy修饰?

  • 主要是防止NSString被修改, 如果没有修改的说法, 用strong也行.
  • 当NSString的赋值来源是NSString时, strongcopy作用相同.
  • 当NSString的赋值来源是NSMutableString, copy会做深拷贝, 重新生成一个新的对象, 修改赋值来源不会影响NSString的值.

5. iOS中的block捕获外部局部变量实际上发生了什么? __block中又做了什么?

  • block捕获的是当前的block内部执行的外部局部变量的瞬时值, 为什么说是瞬时值呢? 看一下C++源码中得知, 其内部代码在捕获的同时....
  • block底层生成了一个和外部变量相同名称的属性值, 如果内部修改值, 其实修改的是捕获之前的值, 其捕获的内部的值因代码只做了一次捕获, 并没有做再一次的捕获, 所以block里面不可以修改值.
  • 如果当前捕获的为对象类型, 其block内部可以认为重新创建了一个指向当前对象内存地址的指针(堆), 操控内部操作的东西均为同一块内存地址, 所以可以修改当前内部的对象里面的属性, 但是不能直接修改当前的指针(无法直接修改栈中的内容, 即重新生成一个新的内存地址). 其原理和捕获基本数据类型一致.
  • 如果加上__block在运行时创建了一个外部变量的"副本"属性, 把栈中的内存地址放到了堆中进而在block内部也能修改外部变量的值.

6. iOS Block为什么用copy修饰?

  • block是一个对象.
  • MRC的时候block在创建的时候, 他的内存比较奇葩, 非得分配到栈上, 而不是在传统的堆上, 它本身的作用域就属于见光死, 一旦在创建时候的作用域外面调用它, 会导致崩溃.
  • 所以利用copy把原本在栈上的复制到堆里面, 就保住了它.
  • ARC的时候, 由于ARC中已经看不到栈中的block了, 用strong和copy一样随意, 用copy是遵循其传统.

7. 为什么分类中不能创建属性Property(runtime除外)?

  • 分类的实现原理是将category中的方法、属性、协议数据放在category_t结构体中, 然后将结构体内的方法列表拷贝到类对象的方法列表中. Category可以添加属性, 但是并不会自动生成成员变量及set/get方法. 因为category_t结构体中并不存在成员变量.
  • 通过之前对对象的分析, 我们知道成员变量是存放在实例对象中的, 并且编译的那一刻就以经决定好了. 而分类是在运行时才去加载的. 那么我们就无法在程序运行时将分类的成员变量添加到实例对象的结构体中. 因此分类中不可以添加成员变量.
  • 在往深一点的回答就是类在内存中的位置是编译时期决定的, 之后再修改代码也不会改变内存中的位置, class_ro_t的属性在运行期间就不能再改变了, 再添加方法是会修改class_rw_t的methods而不是class_ro_t中的baseMethods.

7.1 用关联对象给分类添加属性, 关联对象的原理?

.h文件中:
#import <Foundation/Foundation.h>

@interface NSObject (Person)

@property (nonatomic, copy) NSString *name;

@end


.m文件中:
#import "NSObject+Person.h"
#import <objc/runtime.h> /*或者 #import <objc/message.h>*/
static NSString *nameKey = @"nameKey"; //那么的key

@interface NSObject ()

@end

@implementation NSObject (Person)

/**
 setter方法
 */
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY);
}

/**
 getter方法
 */
- (NSString *)name {
    return objc_getAssociatedObject(self, &nameKey);
}
@end


// 其他类中使用:
- (void)viewDidLoad {
    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"almost";
    NSLog(@"%@", objc.name);
}
  • 关联对象并不是存储在关联对象本身内存中, 而是存储在全局统一的一个容器中.
  • AssociationsManager管理并在它维护的一个单例Hash表AssociationsHashMap中存储.
  • 使用AssociationsManagerLock自旋锁保证了线程安全.

7.2 分类可以添加哪些内容?

  • 实例方法
  • 类方法
  • 协议
  • 属性

7.3 Category的实现原理?

  • Category在刚刚编译完成的时候, 和原来的类是分开的, 只有在程序运行起来的时候, 通过runtime合并在一起.

7.4 使用runtime Associate方法关联的对象, 需要在主对象dealloc的时候释放吗?

  • 不需要, 被关联的对象的生命周期内要比对象本身释放晚很多, 它们会在被NSObject-dealloc调用的object_dispose()方法中释放.

7.5 能否向编译后得到的类中增加实例变量? 能否向运行时创建的类中添加实例变量?

  • 不能再编译后得到的类中增加实例变量. 因为编译后的类已经注册在runtime中, 类结构体中objc_ivar_list实例变量的链表和objc_ivar_list实例变量的内存大小已经确定, 所以不能向存在的类中添加实例变量.
  • 能在运行时创建的类中添加实例变量. 调用class_addIvar函数.

7.6 主类执行了foo方法, 分类也执行了foo方法, 在执行的地方执行了foo方法, 主类的foo会被覆盖吗? 如果想只执行主类的foo方法, 如何做?

  • 主类的方法被分类的foo覆盖了, 其实分类并没有覆盖主类的foo方法, 只是分类的方法排在方法列表前面, 主类的方法列表被挤到了后面, 调用的时候会首先找到第一次出现的方法.
  • 如果想要只是执行主类的方法, 可逆序遍历方法列表, 第一次遍历到的foo方法就是主类的方法.
- (void)foo{ 
    [类 invokeOriginalMethod:self selector:_cmd]; 
} 
+ (void)invokeOriginalMethod:(id)target selector:(SEL)selector { 
    uint count; 
    Method *list = class_copyMethodList([target class], &count); 
    for ( int i = count - 1 ; i >= 0; i--) { 
        Method method = list[i];
        SEL name = method_getName(method); 
        IMP imp = method_getImplementation(method); 
        if (name == selector) { ((void (*)(id, SEL))imp)(target, name); 
        break;
        } 
   }
   free(list);
}

8. loadinitialize的调用情况, 以及子类的调用顺序问题.

  • 调用时刻:
    • +load方法会在Runtime加载类、分类时调用(不管有没有用到这些类, 在程序运行起来的时候都会加载进内存, 并调用+load方法); 每个类、分类的+load, 在程序运行过程中只调用一次(除非开发者手动调用).
    • +initialize方法会在类第一次接收到消息时调用. 如果子类没有实现+initialize方法, 会调用父类的+initialize, 所以父类的+initialize方法可能会被调用多次, 但不代表父类初始化多次, 每个类只会初始化一次.
  • 调用方式:
    • 系统自动调用+load方式为直接通过函数地址调用, 开发者手动调用+load方式为消息机制objc_msgSend函数调用.
    • 消息机制objc_msgSend函数调用.
  • 调用顺序
    • 先调用类的+load, 按照编译先后顺序调用, 先编译先调用, 调用子类的+load之前会先调用父类的+load; 再调用分类的+load, 按照编译先后顺序, 先编译先调用.(注意: 分类的其他方法是: 后编译, 优先调用)
    • 先调用父类的+initialize, 在调用子类的+initialize(先初始化父类, 再初始化子类).
  • +initialize方法的调用方式为消息机制, 而非像+load那样直接通过函数地址调用.

9. 什么是线程安全?锁

  • 多线程同时访问一段代码, 不会造成数据混乱的情况.

9.1 你接触到的项目, 哪些场景运用到了线程安全?

  • 举例12306同一列火车的车票, 同一时间段多人抢票, 如何解决互斥锁使用格式?
synchronized(锁对象) {
    // 需要锁定的代码
    // 锁定一份代码只用一把锁, 用多把锁是无效的
}
  • 互斥锁的优缺点
    • 优点: 能有效防止因多线程抢夺资源造成的数据安全问题
    • 缺点: 需要消耗大量的CPU资源
  • 互斥锁的使用前提: 多条线程抢夺同一块资源
  • 线程同步是指: 多条线程按顺序地执行任务
  • 互斥锁就是使用了线程同步技术

9.2 OC中的原子和非原子属性

  • OC在定义属性时有nonatomic和atomic两种选择
  • atomic(默认): 用于保证属性setter、getter的原子性操作, 相当于在getter和setter内部加了线程同步的锁
    • 线程安全, 需要消耗大量的资源
    • 不一定能保证使用属性的过程是线程安全的
      • 一个线程在连续多次读取某条属性值的时候, 同时别的线程在改值, 这样还是会读取到不同的属性值.
      • 一个线程在获取当前属性的值, 另一个线程把这个属性释放了, 可能会造成崩溃
  • nonatomic: 非原子属性, 不加锁
    • 非线程安全, 适合内存小的移动设备
// atomic加锁原理:
@property (atomic, assign) int age;
- (void)setAge:(int)age {
    @synchronized(self) {
        _age = age;
    }
}
  • 开发中建议所有属性都声明为nonatomic, 尽量避免多线程抢夺同一块资源,
  • 将加锁、资源抢夺的业务逻辑交给服务端处理

9.3 @synchronized

  • @synchronized是对mutex递归锁的封装
  • 源码查看: objc4中的objc-sync.mm文件
  • @synchronized(obj)内部会生成obj对应的递归锁, 然后进行加锁、解锁操作
@synchronized(obj) {
    // 任务
}

9.4 各种锁

OSSpinLok
  • 操作系统多线程轮转算法 image.png
os_unfair_lock

image.png

pthread_mutex

image.png

pthread_mutex - 递归锁

image.png

pthread_mutex - 条件

image.png

NSLock、NSRecursiveLock、NSCondition
  • NSLock是对mutex普通锁的封装

image.png

  • NSRecursiveLock也是对mutex递归锁的封装, API跟NSLock基本一致

  • NSCondition是对mutexcond的封装

    • 对锁和条件的封装
    • 把C语言的一些数据结构屏蔽了 image.png
  • NSConditionLock是对NSCondition的进一步封装, 可以设置具体的条件值

image.png

9.5 自旋锁、互斥锁比较

  • 什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用, 但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器
  • 什么情况使用互斥锁比较好?

    • 预计线程等待的时间较长
    • 单核处理器
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈

9.6 iOS线程同步方案加锁性能比较

  • 性能从高到低排序

image.png

9.7 多线程的坑

  • 写 UIKit、AFNetworking、FMDB 这些库的“大神”们,并不是解决不了多线程技术可能会带来的问题,
  • 而相反正是因为他们非常清楚这些可能存在的问题,所以为避免使用者滥用多线程,亦或是出于性能考虑,
  • 而选择了使用单一线程来保证这些基础库的稳定可用。
9.7.1 常驻线程
  • 常驻线程
    • 指的就是那些不会停止,一直存在于内存中的线程。
    • AFNetworking 2.0 专门创建了一个线程来接收 NSOperationQueue 的回调,这个线程其实就是一个常驻线程。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        // 先用 NSThread 创建了一个线程
        [[NSThread currentThread] setName:@"AFNetworking"];
        // 使用 run 方法添加 runloop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

image.png

  • 虽然说,在一个 App 里网络请求这个动作的占比很高,但也有很多不需要网络的场景,所以线程一直常驻在内存中,也是不合理的。
  • AFNetworking 在 3.0 版本时,使用苹果公司新推出的 NSURLSession 替换了 NSURLConnection,从而避免了常驻线程这个坑。
    • NSURLSession 可以指定回调 NSOperationQueue,这样请求就不需要让线程一直常驻在内存里去等待回调了。
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
    • NSURLSession 发起的请求,可以指定回调的 delegateQueue,不再需要在当前线程进行代理方法的回调。所以说,NSURLSession 解决了 NSURLConnection 的线程回调问题。
  • AFNetworking 2.0 使用常驻线程也是无奈之举,一旦有方案能够替代常驻线程,它就会毫不犹豫地废弃常驻线程。
9.7.2 并发
  • 总结来讲,类似数据库这种需要频繁读写磁盘操作的任务,尽量使用串行队列来管理,避免因为多线程并发而出现内存问题。
  • 创建线程的过程,需要用到物理内存,CPU 也会消耗时间。
    • 而且,新建一个线程,系统还需要为这个进程空间分配一定的内存作为线程堆栈。
    • 堆栈大小是 4KB 的倍数。在 iOS 开发中,主线程堆栈大小是 1MB,新创建的子线程堆栈大小是 512KB。
    • 除了内存开销外,线程创建得多了,CPU 在切换线程上下文时,还会更新寄存器,更新寄存器的时候需要寻址,而寻址的过程还会有较大的 CPU 消耗。
  • 线程过多时内存和 CPU 都会有大量的消耗,从而导致 App 整体性能降低,使得用户体验变成差。
9.7.3 多线程避坑
  • 一提到多线程技术,我们往往都会联想到死锁等锁的问题,但其实锁的问题是最容易查出来的,
    • 反而是那些藏在背后,会慢慢吃尽你系统资源的问题,才是你在使用多线程技术时需要时刻注意的。
  1. 常驻线程一定不要滥用,最好不用。
  2. 除非是并发数量少且可控,或者必须要在短时间内快速处理数据的情况,否则我们在一般情况下为避免数量不可控的并发处理,都需要把并发队列改成串行队列来处理。
9.7.4 线程的优先级反转
  1. 什么是优先级反转?请解释其概念并举例说明。

优先级反转是指一个低优先级的任务持有了一个共享资源的锁,而高优先级的任务等待该资源释放,此时中优先级的任务运行并抢占了低优先级的CPU先执行了,会导致高优先级的任务被“反转”到中优先级任务之后运行。

举例:

假设有三个任务A、B、C,它们的优先级分别为高、中、低。任务C持有一个锁并正在运行,任务A需要该锁才能运行,但任务B抢占了CPU时间。这会导致,原本任务A优先级更高,应该在任务B前执行,却导致任务A必须等待任务B完成后才能运行。

  1. 在iOS开发中,如何避免或解决优先级反转问题?

优先级继承 : 确保持有锁的低优先级任务提升到高优先级任务的级别,直到释放锁,再恢复成原来的优先级。(记录了锁持有者的api都可以自动避免优先级反转,系统会通过提高相关线程的优先级来解决优先级反转的问题,如 dispatch_sync)

避免使用dispatch_semphore(信号量)做线程同步:dispatch_semaphore 容易造成优先级反转,因为api没有记录是哪个线程持有了信号量,所以有高优先级的线程在等待锁的时候,内核无法知道该提高那个线程的优先级(QoS);

使用合适的锁机制:选择合适的锁机制(如NSLock、NSRecursiveLock等)和避免长时间持有锁。

避免锁竞争:减少共享资源的使用和锁的粒度,避免长时间锁竞争。

10. 你实现过单例模式吗?

  • dispatch_once这个GCD中的函数, 可以保证挣个应用生命周期中某段代码只被执行一次.
// GCD实现单例
+ (instancetype)sharedUser {
    static PKUserHelper * _user = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _user = [[PKUserHelper alloc] init];
    });
    return _user;
}

10.1 单例是怎么销毁的?

  • dispatch_once_t的工作原理是, static修饰会默认将其初始化为0
    • 当且仅当其为0的时候, dispatch_once(&onceToken, ^{})这个函数才能被调用
    • 如果执行了这个函数, 这个dispatch_once_t静态变成了 -1, 就永不会被调用
// 必须把static dispatch_once_t onceToken; 这个拿到函数体外, 成为全局的
+ (void)attempDealloc {
    // 只有置成0, GCD才会认为他从未执行过, 
    // 它默认为0, 这样才能保证下次再次调用shareInstance的时候,再次创建对象
    onceToken = 0;
    _shareInstance = nil;
}

10.2 如何不使用dispatch_once实现单例

// 1. 重写allocWithZone实现单例
+ (instancetyp)allocWithZone:(struce _NSZone)zone {
    static id instance = nil;
    @synchronized (self) {// 互斥锁
        if (instance == nil) {
            instance [super allocWithZone:zone];
        }
    }
    return instance;
}

// 2. 直接用@synchronized来保证线程安全
+ (instancetyp)sharedSingletion {
    static id instance = nil;
    @synchronized (self) {// 互斥锁
        if (instance == nil) {
            instance [super allocWithZone:zone];
        }
    }
    return instance;
}

项目中, 你都用单例做了什么?

  • 当整个程序公用一份资源的时候可以用单例
    • 访问应用的配置信息
    • 用户的个人信息用的NSUserDefault存储, 对登录类进一步采用单例模式封装方便全局访问

11. 关键字static、const、extern、define

11.1 static

  • static关键字声明局部变量
    • 改变变量的生命周期, 使变量成为静态的局部变量
    • 编译时就把变量分配在静态区, 程序退出才释放内存
    • 使得该局部变量可以记录上次的数据
    • 作用域不变, 由于仍是局部变量, 只能在代码块内部使用
  • static声明全局变量
    • 全局变量就被定义成为一个静态全局变量(全局变量和静态全局变量的生命周期是一样的, 都是在堆中的静态区)
    • 静态全局变量限制了其作用域, 使其只能在本文件内部有效, 避免其他文件重定义导致命名冲突.
static NSTimeInterval kAnimationDuration = 0.3; 
@implementation PKAnimationVC

  • 通过static修饰的函数或变量, 在该文件中, 所有位于这条语句之后的方法和函数都可以访问, 其他文件中的方法和函数则不能访问.
  • 类方法不可以访问实例变量(函数), 通过static修饰的实例变量(函数), 可以被类方法访问.
  • static修饰的变量, 能且只能被初始化一次.
  • static修饰的变量, 默认初始化为0;

11.2 const

  • const表示常量,被修饰之后的数据类型, 由变量转为常量, 不可以被修改, 在编译阶段会执行类型检查, 存储区域位于常量区, 常配合staticextern使用.
  • 修饰全局静态变量
// 全局静态变量被修饰后变成全局静态常量, 其内存区域由静态区移动到常量区
static const NSTimeInterval kAnimationDuration = 0.3; 
@implementation JSDAnimationVC
  • 修饰全局变量
  • 外部文件访问时需要使用关键字extern并且指明常量声明的类型来使用, 否则编译器默认以int类型来处理.
extern NSString* const JSDLoginManagerDidLoginNotification;
@interface JSDLoginManagerVC : ViewController
@end
NSString * const JSDLoginManagerDidLoginNotification = @"JSDLoginManagerDidLoginNotification";
@implementation JSDCrashVC
  • 修饰局部变量, const修饰其后面内容
// 下面两种写法都使*name指针地址不可变, 实际指向内容不受影响, 修改指针地址编译器会报错
const NSString *name = @"Jack";
NSString const *name = @"Jack";

// 使*name指针指向的内容不可变, 指针地址不受影响, 修改内容则编译器报错
NSString * const name = @"Jack";

11.3 extern

  • 作用是声明外部全局变量. 注意extern只能声明, 不能用于实现.
  • 当使用extern来声明变量时, 其会现在编译单元内部进行查找, 如果没有则继续道外部查找, 如果缺少实现并使用到了此数据, 会导致编译不通过.

11.4 define

  • 其实就是字符替换, 可用于修饰数据、函数、结构体、方法等, 系统不会对其做类型检查.
  1. 编译检查: 宏是预编译不做检查, const是编译阶段会有类型检查有错误会提示
  2. 宏的优缺点: 方便灵活, 可以用替换各种函数、方法、结构体、数据等; 预编译期间完成, 增大编译时间
  3. const的优缺点: 编译器通常不为普通const常量分配存储空间, 而是将它们保存在符号表中, 成为了一个编译期间的常量, 没有了存储于读取内存的操作, 更高效; 只能用于定义常量
  4. 宏定义的所定义的生命周期与所在的载体的生命周期有关
  5. const修饰具有就近性、只读性
  6. 苹果官方推荐能使用常量定义完成的, 尽量使用const常量,少用宏.

12. APNS推送的基本原理

  • 基本
  1. 应用程序的服务端把要发送的信息、目标iPhone的标识打包, 发送给苹果APNS服务器
  2. APNS在自身已注册Push服务的iPhone列表中, 查找到有相应标识的iPhone并推送消息
  3. iPhone把收到的推送消息传递给对应的App, 并按照设定弹出Push通知.
  • 注册流程
  1. Device连接APNS服务器并携带UUID
  2. 连接成功, APNS经过打包和处理产生deviceToken并返回给注册的Device
  3. Device携带获取的deviceToken发送到我们自己的服务器
  4. 完成需要被推送的Device在APNS服务器和我们自己服务器的注册
  • 推送过程
  1. 首先手机装有当前的App, 开启权限, 保持有网络的情况下, APNS服务器会验证deviceToken, 验证成功后会处于一个长连接状态.
  2. 当我们推送消息的时候, 我们的服务器会按照指定格式进行打包, 结合deviceToken一起发送给APNS服务器.
  3. APNS服务器将新消息推送到iOS设备上
  4. iOS设备收到推送消息后, 会通知我们的App, 并给与推送提示.

13. weak属性

  • 说说你理解的weak属性? 实现weak后, 为什么对象释放后会为nil?
  1. weak关键字的作用是弱引用, 所引用对象的计数器不会加1, 并在引用对象被释放的时候自动被设置为nil.
  2. weak的原理在于底层维护了一张weak_table_t结构的哈希表, key是所指对象的地址, value是weak指针的地址数组.
  3. 对象释放时, 调用clearDeallocation函数根据对象地址key获取所有weak指针地址的数组
    • 然后遍历这个数组, 把其中的数据设为nil, 最后把这个entry从weak表中删除, 最后清理对象的记录.
// 只是一个深层次函数调用的入口, 在该方法内部调用了`storeWeak`方法
id objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

image.png

  • weak散列表
    • 自旋锁
    • 引用计数
    • 弱引用表
      • 哈希表
      • 哈希函数
      • 下标
  • weak_register_no_lock
  • weak_entry_t weak_table&referent
  • 存储我们的弱引用对象 entry->inline_referrers[i]= new_referrer;
  • weak_referrer_t *new_referrers entry->referrers
  • 散列表 弱引用表 entry实体 数组 弱引用对象
  • Person
    • name
    • subject
  • dealloc weak 弱引用对象 => nil

13.1 OC中向一个nil对象发送消息将会发生什么?

  • 在寻找对象化的isa指针时就是0地址返回了, 所以不会有任何错误

13.2 OC在向一个对象发送消息时, 发生了什么?

  • 首先是通过obj的isa指针找到对应的Class
  • 先去操作对象中的缓存方法列表中的objc_cache中去寻找当前方法, 如果找到就直接实现对应IMP
  • 如果在缓存中找不到, 则在Class中找到对应的Method list中对应foo
  • 如果在Class中没有找到对应的foo, 就会实现foo对应的IMP

14. UIView和CALayer是什么关系?

  • UIView可以接受并处理事件, 而Layer不可以
  • 每个UIView内部都有一个CALayer提供内容的绘制和显示.
  • 两者都有树桩层级结构, subLayers、subViews, 但是Layer比UIView多了个AnchorPoint
  • 在显示的时候, UIView作为Layer的CALayerDelegate, UIView的显示内容由CALayer的display
  • CALayer默认修改属性支持隐式动画, 在给UIView的layer做动画的时候, UIView作为layer的代理,
  • layer通过actionForLayer:forKey:向UIView请求相应的action动画行为
  • layer内部维护着三份layer tree
    • 动画树presentLayer Tree
    • 模型树modeLayer Tree
    • 渲染树Render Tree
  • 在做iOS动画的时候, 我们修改动画的属性, 在动画的其实是layer的presentLayer的属性值,
  • 而最终展示在界面上的其实是提供View的modelLayer

14.1 说说响应者链和事件传递

  • 响应者链: 是由链接在一起的响应者(UIResponder子类)组成的,
    • 一般由第一响应者到application对象以及中间所有响应者一起组成.
  • 事件传递机制: iOS的事件传递系统将触摸和其他事件(如动作, 手势)发送到视图层级结构中的适当对象. 在事件传递过程中, 系统通常从根视图开始查找, 并递归向下查找以找到最合适处理该事件的视图.

寻找第一响应者的过程:

  1. 当App发生触摸事件后, 系统会将事件添加到UIApplication管理的一个队列中
  2. UIApplication将处于任务队列最前端的事件向下分发到UIWindow
  3. UIWindow将事件向下分发到UIView
  4. UIView调用hitTest先看自己能否处理这个事件,
    • 判断触摸点是否在自己身上, 自己的透明度是否大于0.01 && userINteractionEnabled = YES && hidden == NO
    • 如果这些都满足, 那么继续寻找其子视图
  1. 遍历子控件, 重复上一步骤
    • 如果没有找到, 那么自己就是该事件的处理者
  1. 如果如果自己不能处理, 那么久不作任何处理, 即视为没有合适的View能处理当前事件, 该事件被抛弃

14.2 怎么寻找当前触摸的View?

  • 事件传递给控件之后, 就会调用hitTest: withEvent方法去寻找更合适的View
    • 如果当前View存在子控件, 则在子控件继续调用hitTest: withEvent方法判断是否是合适的View
    • 判断触摸点是否在视图内pointInside: withEvent
// 因为所有的视图类都是继承BaseView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   // 1.判断当前控件能否接收事件
   if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
   // 2. 判断点在不在当前控件
   if ([self pointInside:point withEvent:event] == NO) return nil;
   // 3.从后往前遍历自己的子控件
   NSInteger count = self.subviews.count;
   for (NSInteger i = count - 1; i >= 0; i--) {
      UIView *childView = self.subviews[I];
       // 把当前控件上的坐标系转换成子控件上的坐标系
	  CGPoint childP = [self convertPoint:point toView:childView];
      UIView *fitView = [childView hitTest:childP withEvent:event];
       if (fitView) { // 寻找到最合适的view
           return fitView;
       }
   }
   // 循环结束,表示没有比自己更合适的view
   return self;
}

14.3 给tableView添加一个tap手势, 点击当前的cell, 那个事件被响应?

  • tap事件被响应, 因为tap事件添加之后, 默认是取消当前tap以外的所有事件
  • 也就是说tap事件处于当前响应者链的最顶端, 解决的办法是执行tap的delegate, 实现下面代码
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  {  
    if([touch.view isKindOfClass:[XXXXcell class]]) {  
        return NO;  
    }  
    return YES;  
}

14.4. 点击子控件,让父控件响应事件

  • 在子控件中重写hitTest:withEvent方法,返回父控件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    return [self superview]; // return nil; 
    // 此处返回nil也可以。返回nil就相当于当前的view不是最合适的view
}
  • 让谁响应,就直接重写谁的touchesBegan:withEvent方法

14.5. 点击子控件,父控件和子控件都响应事件

touches方法默认不处理事件,而是把事件顺着响应者链条传递给上一个响应者,在子控件中重写touches方法,在touches方法中调用父控件的touches方法

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"-- touchGreen");
[super touchesBegan:touches withEvent:event];
}

14.6. 扩大按钮的响应范围

  • 重写一个Button类,这个button类继承与UIButton,
  • 重写- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event, 作用:判断下传入过来的点在不在方法调用者的坐标系上
#import "JHCustomButton.h"

@implementation JHCustomButton

// 作用:判断下传入过来的点在不在方法调用者的坐标系上
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    
    CGRect bounds =CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
    
    //宽高希望扩展的范围
    CGFloat widthDelta = 20;
    CGFloat heightDelta = 20;
    
    //相当于bounds 上下左右都增加了10的额外
    bounds = CGRectInset(bounds, -0.5*widthDelta, -0.5* heightDelta);//注意这里是负数,扩大了之前的bounds的范围
    
    //点击的点是否在这个范围
    return CGRectContainsPoint(bounds, point);

}
@end

14.7 做动画时frame改变了吗?

14.8 layoutIfNeedssetNeedsLayout的区别?

  • setNeedsLayout:当事件正在处理时,视图更新的操作不会立即执行,等待下一个更新周期更新视图(即异步执行)。需要注意:当我们修改frame或者约束时,系统会自动标记布局需要重新计算。它会触发layoutsubview方法。
  • layoutIfNeeded:强制视图立即更新其布局,即同步执行。当使用Auto Layout时,布局引擎根据约束的变化更新视图的位置。该方法的接收者将作为根视图,布局时也将从视图树的根视图开始。如果没有待处理的布局更新,则此方法将直接退出,而不会修改布局,或调用任何与布局有关的方法。

15. @synthesize@dynamic分别有什么作用

  • @property默认是@synthesize
  • @synthesize的语义是编译器会自动为属性添加一个实例变量名, 同时会为该属性生成setter/getter方法.
  • @dynamic告诉编译器: 属性的setter与getter方法由用户自己实现, 不自动生成

16. UIViewController的生命周期

  • 按照执行顺序排列
  1. loadView: 开始加载视图控制器自带的view
  2. viewDidLoad: 视图控制器的view被加载完成
  3. viewWillAppear: 视图控制器的view将要显示在window上
  4. updateViewConstraints: 视图控制器的view开始更新AutoLayout约束
  5. viewWillLayoutSubViews: 视图控制器的view将要更新内容视图的位置
  6. viewDidLayoutSubview: 视图控制器的view已经更新视图的位置
  7. viewDidAppear: 视图控制器的view已经显示在window上
  8. viewWillDisappear: 视图控制器的view将要从window上消失
  9. viewDidDisappear: 视图控制器的view已经从window上消失

17. 有没有使用过performSelector?

  • 动态添加方法
  • 使用+ (BOOL)resolveInstanceMethod:(SEL)sel来动态添加方法
  • [p performSelector:@selector(run) wihObject:nil afterDelay:3.0]
  • 上面这段代码放在子线程中是怎么执行?
    • 内部创建了一个NSTimer定时器, 然后这个定时器会添加在当前的RunLoop中
    • 所以上面代码放到了子线程中不会有任何定时器相关方法被执行,
    • 如果想要执行需要[[NSRunLoop currentRunLoop] run]
    • NSTimer直接在子线程是不会被调用的, 想要执行需要开启当前的RunLoop

18. iOS静态库、动态库、Framework

  • 存放关系 image.png

  • 库(Library)说白了就是一段编译好的二进制代码, 加上头文件就可以供别人使用.

  • 某些代码需要给别人使用, 但是不希望被别人看到源码时, 就需要以库的形式进行封装, 只暴露出头文件.

  • 另外一种情况是, 对于某些不会进行大的改动的代码, 我们只想减少编译时间,

    • 就可以把代码打包成库, 因为库是已经编译好的二进制了,
    • 编译的时候只需要Link链接一下, 不会浪费编译时间.

18.1 静态库与动态库的区别

静态库
  • 静态库即静态链接库.a. 之所以叫做静态, 是因为静态库在编译的时候会被直接考备一份,
  • 复制到目标程序里, 这段代码在目标程序里就不会再改变了.
  • 静态库的优点是, 编译完成之后, 库文件实际上就没有作用了. 目标程序没有外部依赖, 直接就可以运行.
  • 静态库的缺点是, 会使目标程序的体积增大, 模块更新困难.
动态库

image.png

  • 动态库即动态链接库.dylib/.tbd. 与静态库相反, 动态库在编译时不会被拷贝到目标程序中,
  • 目标程序中只会存储指向动态库的引用. 等到程序运行时, 动态库才会被真正加载起来.
  • 动态库的优点是, 不需要拷贝到目标程序中, 不会影响目标程序的体积, 而且同一份库可以被多个程序使用(又叫共享库).
    • 同时, 运行时才载入的特性, 也可以让我们随时对库进行替换, 而不需要重新编译代码.
  • 动态库的缺点是, 动态载入会带来一部分性能损失, 使用动态库也会使得程序依赖于外部环境.
    • 如果环境缺少动态库或库的版本不正确, 就会导致程序无法运行(lib not found错误).
什么是tbd格式?
  • 全称是text-based stub libraries, 本质上就是一个YAML描述的文本文件.
  • 作用是用于记录动态库的一些信息, 包括导出的符号、动态库的框架信息、动态库的依赖信息.
  • 用于避免在真机开发过程中直接使用传统的dylib.
  • 对于真机来说, 由于动态库都是在设备上, 在XCode上使用基于tbd格式的伪framework可以大大减少XCode的大小.
Framework

image.png

  • Framework实际上是一种打包方式, 将库的二进制文件、header.h头文件、有关的资源文件打包到一起, 方便管理和分发.
  • iOS 8之前, iOS不支持使用动态Framework, 开发者可以使用的Framework只有苹果自家的UIKit.frameworkFoundation.framework等.
    • 这种限制可能是处于安全的考虑.
    • 换一个角度讲, 因为iOS应用都是运行在沙盒当中, 不同的程序之间不能共享代码,
    • 同时动态下载代码又是被苹果禁止的, 没法发挥出动态库的优势, 实际上动态库也就没有存在的必要了.
    • 开发者想要在iOS平台共享代码, 唯一的选择就是打包成静态库.a文件, 同时附上头文件.
    • 但是这样的打包方式不够方便也比较麻烦, 大家还是希望共享代码都能像Framework一样, 直接扔到工程里就可以用.
    • 于是人们想出了各种黑魔法让XCode Build出iOS可以用的动态Framework.
  • iOS 8(XCode 6)推出之后, iOS平台添加了动态库的支持, 同时XCode6也原生自带了Framework支持(动态和静态都可以),
    • 上面提到的黑魔法就没有必要了. 添加动态库支持的理由大概就是Extension的出现.
    • Extension和App是两个分开的可执行文件, 同时需要共享代码, 这种情况下动态库的支持是必不可少的了.
    • 但是这种动态Framework和系统的UIKit.framework还是有很大区别. 系统的Framework不需要拷贝到目标程序中,
    • 我们自己做出来的Framework哪怕是动态的, 最后也还是要拷贝到App中(App和Extension的Bundle是共享的),
    • 因此苹果又把这种Framework称为**Embedded Framework.**
XCFramework
  • 是苹果官方推荐的、支持的, 可以更方便的表示一个多个平台和架构的分发二进制库的格式.
  • 需要XCode11以上支持.
  • 和传统的framework相比:
  1. 可以用单个.xcframework文件提供多个平台的分发二进制文件
  2. 与Fat Header相比, 可以按照平台划分, 可以包含相同框架的不同平台的文件
  3. 在使用时, 不需要再通过脚本去剥离不需要的架构体系.
Swift支持
  • 跟着iOS 8同时发布的还有Swift. 如果要在项目中使用外部的代码, 可选的方式只有两种,
    • 一种是把代码拷贝到工程中, 另一种是用动态Framework.
    • 使用静态库是不支持的.
  • 造成这个问题的原因主要是Swift的运行库没有被包含在iOS系统中, 而是会打包进App中(这也是造成Swift App体积大的原因),
    • 静态库会导致最终的目标程序中包含重复的运行库.
    • 同事拷贝Runtime这种做法也会导致在纯OC的项目中使用Swift库出现问题.
    • 苹果生成等到Swift的Runtime稳定之后就会被加入到系统当中, 这时候这个限制就会被去除了.
CocoaPods的做法
  • 在纯OC的项目中, CocoaPods使用编译静态库.a方法将代码集成到项目中.
    • 在Pods项目中的每个target都对应这一个Pod的静态库.
    • 不过在编译过程中并不会真的产出.a文件.
    • 或者使用CocoaPods-Packager这个插件.
  • 当不想发布代码的时候, 也可以使用Framewok发布Pod, CocoaPods提供了vendored_framework选项来使用第三方Framework.
  • 对于Swift项目, CocoaPods提供了动态Framework的支持. 通过use_frameworks!选项控制.
    • 对于Swift写的库来说, 想通过CocoaPods引入工程, 必须加入user_frameworkd!选项.

19. 为什么刷新UI要在主线程操作?

  • UIKit不是线程安全的,这意味着如果在非主线程上进行UI更新操作,可能会导致UI不稳定、出现异常情况,甚至引发崩溃。因此,为了保证应用程序的稳定性和流畅性,所有与UI相关的操作都必须放在主线程上执行。

19.1 为什么不把UIKit设计为线程安全的呢?

  • 因为线程安全需要加锁, 加锁会消耗性能, 影响处理和渲染速度, 我们在写@property时都会写nonatomic来提高性能.
  • 假设能够异步设置View的属性, 那我们究竟是希望这些改动能够同时生效, 还是按照各自RunLoop的进度去改变这个View的属性呢?
  • 假设UITableView在其他线程去移除了一个cell, 而在另一个线程却对这个cell所在的index进行一些操作, 这时就可能会引发闪退.
  • 如果在后台线程移除了一个View, 这个时候RunLoop周期还没完结, 用户在主线程点击了这个"将要消失的View", 那么究竟该不该响应事件? 在哪条线程进行响应?
  • 在CocoaTouch框架中, UIApplication初始化工作是在主线程进行的.
    • 而界面上所有的视图都是在UIApplication实例的子结点,
    • 所以所有的手势交互操作都是在主线程上才能响应.

19.2 NSNotification刷新UI

  • NSNotificationCenter在子线程中发出通知, 也要在主线程中刷新UI
   dispatch_async(dispatch_get_main_queue(), ^{
       // 刷新UI
   });

19.2.1 NSNotificationCenter用完之后不移除, 会闪退吗?
  • 有时候会导致闪退, 如在通知事件中处理数据或者UI事件,
  • 通知的闪退相对难检测.

20. 说说内存管理

  • alloc copy new生成的对象里面, 在底层源码也同时和当前对象相关联的散列表(SideTable),
  • 其中内部有三个属性, 一个是自旋锁, 一个是引用计数器, 一个是维护weak的表,
  • 其中retain、release对利用键值对, 通过位移对当前对象的引用计数器进行加减操作,
  • 如果当前引用计数器为0的时候, 其dealloc内部会删除当前的引用计数器, 并释放当前对象.

20.1

21. KVO&KVC

21.0 KVO的实现原理

  • 通过runtime派生子类的方式复写相关需要KVO监听的属性, 在该属性setter之前和之后调用NSObject的监听方法, 这样KVO就实现了属性变换前后的回调.
  • KVO派生的子类具体格式应该是:NSKVONotifying_+类名的类, 如NSKVONotifying_Person
  • 未使用KVO监听的对象的isa指针结构图

image.png

  • 使用了KVO监听的对象的isa指针结构图

image.png

image.png

  • _NSSetValueAndNotify的内部实现 image.png

image.png

  • 子类的内部方法
  • class方法屏蔽了内部实现, 隐藏了NSKVONotifying_MJPerson类的存在, runtime可以拿到真实的类型 image.png

image.png

21.1 如何手动触发一个KVO

  • 键值观察通知依赖于NSObject的两个方法
    • willChangeValueForKey:
    • didChangeValueForKey:
  • 在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用, 这就会记录旧的值.
  • 当改变发生后, didChangeValueForKey:会被调用, 继而observeValueForKey:ofObject:change:context也会被调用.
  • 如果可以手动实现这些调用, 就可以实现"手动触发KVO"了.

21.2 如何给系统KVO设置筛选条件?

  • 如:取消Personage属性的默认KVO, 设置age大于18时, 手动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setAge:(NSInteger)age {
    if (age >= 18) {
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    } else {
        _age = age;
    }
}

21.3 通过KVC修改属性会触发KVO吗?

  • 会触发KVO, 即使没有声明属性, 只有成员变量, 只要accessInstanceVariablesDirectly返回的是YES, 允许访问其成员变量,
  • 那么不管有没有调用setter方法, 通过KVC修改成员变量的值, 都会触发KVO.
  • 这也说明通过KVC内部实现了willChangeValueForKey:方法和didChangeValueForKey:方法.

21.4 直接修改成员变量会触发KVO吗?

  • 不会触发KVO, 直接修改成员变量内部并没有做处理只是单纯的赋值, 所以不会触发KVO.

21.5 KVC的底层实现是什么?

  • 赋值方法setValue:forKey:的原理
  1. 首先会按照顺序一次查找setKey:方法和_setKey:方法, 只要找到这两个方法当中的任何一个就直接传递参数, 调用方法;
  2. 如果没有找到setKey:_setKey:方法, 那么这个时候会查看accessInstanceVariablesDirectly方法的返回值, 如果返回NO(也就是不允许直接访问成员变量), 那么会调用setValue:forUndefineKey:方法, 并抛出异常NSUnknownKeyException;
  3. 如果accessInstanceVariablesDirectly方法返回的是YES, 也就是说可以访问其成员变量, 那么就会按照顺序一次查找_key、_isKey、key、isKey这四个成员变量, 如果查找到了, 就直接赋值; 如果依然没有查到, 那么会调用setValue:forUndefineKey:方法, 并抛出异常NSUnknownKeyException.

image.png

  • 取值方法valueForKey:的原理
  1. 首先会按照顺序一次查找getKey、key、isKey、_key:这四个方法, 只要找到这四个方法当中的任何一个就直接调用该方法;
  2. 如果没有找到, 那么这个时候会查看accessInstanceVariablesDirectly方法的返回值, 如果返回的是NO(不允许直接访问成员变量), 那么会调用valueforUndefineKey:方法, 并抛出异常NSUnknownKeyException;
  3. 如果accessInstanceVariablesDirectly方法返回的是YES, 也就是说可以访问其成员变量, 那么就会按照顺序一次查找_key、_isKey、key、isKey这四个成员变量, 如果找到了, 就直接取值; 如果依然没有找到成员变量, 那么会调用valueforUndefineKey方法, 并抛出异常NSUnknownKeyException.

image.png

21.6 KVC的使用

  • 动态地取值和设置值
  • 访问和修改私有变量
    • 修改一些控件的内部属性
    • 如之前的UITextFieldplaceHolderText.
  • 模型和字典的转换

22. 多线程

22.1 iOS的多线程方案有哪几种?

image.png

  • GCD
  • 只要是同步或主队列, 都是串行执行

image.png

  • 死锁的产生, 出现了相互等待产生死锁
  • 使用sync函数往当前串行队列中添加任务, 会卡主当前的串行队列产生死锁
  • sync执行完旧任务才能执行新任务
- (void)testDeadLock {
    NSLog(@"执行任务1");
    dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{// block0
        NSLog(@"执行任务2");
        
        // sync + queue, 同步串行 同一个队列, 死锁
        // sync + queu2, 同步并发 不同的队列, 不死锁
        // sync + queue3, 同步串行 不同的队列, 不死锁
        // async + queue, 异步串行, 不死锁
        dispatch_sync(queue3, ^{// block1
            NSLog(@"执行任务3 - %@", [NSThread currentThread]);
        });
        NSLog(@"执行任务4");
    });
    NSLog(@"执行任务5");
}

image.png

  • 全局并发队列只有一份, 打印出来的内存地址一样

22.2 队列组的使用

image.png

23. NSNotification的多线程

  • 通知中心采用单例的模式,整个系统只有一个通知中心。可以通过[NSNotificationCenter defaultCenter]来获取对象。
  • 在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。
    • Notification的发送与接收处理都是在同一个线程中。
    • 虽然我们在主线程中注册了通知的观察者,但在全局队列中post的Notification,并不是在主线程处理的。
    • 如果我们想在回调中处理与UI相关的操作,需要确保是在主线程中执行回调。
  • 那么怎么才能做到一个Notification的post线程与转发线程不是同一个线程呢?
    • 设置[NSOperationQueuemainQueue],就可以实现在主线程中刷新UI的操作。
[[NSNotificationCenter defaultCenter] addObserverForName:@"Test_Notification" object:nil queue [NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"接收和处理通知的线程%@", [NSThread currentThread]);
    }];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"Test_Notification" object:nil userInfo:nil];
    NSLog(@"发送通知的线程为%@", [NSThread currentThread]);
});

23. [UIImage imageNamed:@""][UIImage imageWithContentWithFile:@""][UIImage initWithContentsFile:]这三个方法的区别

23.1 imageNamed:方法

  • imageNamed方法, 获取到的对象是autorelease的. 它在生成image对象的同时, 会把图像数据根据它的名字缓存在系统内存中, 提高了imageNamed方法获得相同图片的image对象的性能.
  • 即使生成的对象被autoreleasePool释放了, 这份缓存也不释放. 这对App中有大量相同图片的情况非常有用, 可以提高性能和内存利用率.
  • 当图片文件较小、使用比较频繁的时候, 使用imageNamed:方法比较好. 只会把图片对象的指针指向同一块内存. 可以直接从缓存中取得数据, 而不用遍历整个工程文件. 同一个图片对象系统只会把它缓存到内存一次, 方便重复利用.

23.2 imageWithContentsOfFile:方法

  • 得到的对象是autorelease的, 当autoReleasePool释放时才释放, 不做系统缓存.
  • 图像会被系统以数据的形式加载到内存. 当你不需要重用该图像, 或者需要将图像以数据的方式存储到数据库时使用.

23.3 initWithContentsFile:方法

  • 获取到的对象没用后, 要手动release掉. 系统不做缓存. release后立即释放, 一般用在封面等图片比较大的情况.

二、网络篇

OSI网络七层协议: 应, 表, 会, 传, 网, 数, 物

image.png

1. TCP的三次握手和四次挥手?

1.1 三次握手

  1. 客户端向服务端发起请求链接, 首先发送SYN报文, SYN=1, seq=x, 并且客户端进入SYN_SENT状态
  2. 服务端收到请求链接, 向客户端进行回复, 并发送响应报文, SYN=1, seq=y, ACK=1, ack=x+1, 并且服务端进入到SYN_RCVD状态
  3. 开始会话客户端收到响应报文后, 向服务端发送确认报文, ACK=1, ack=y+1, 此时客户端进入到ESTABLISHED, 服务端收到用户端发送过来的确认报文后, 也进入到established(已确立的), 此时连接建立成功.

1.2 为什么需要三次握手?

  • 为了防止已失效的链接请求报文段突然又传送到了服务端, 这样会产生错误.
  • 假设这是一个早已失效的报文段, 但服务端收到此失效的连接请求报文段后, 就误认为是客户端再次发出的一个新的连接请求. 于是就向客户端发出确认报文段, 同意建立连接.
  • 假设不采用"三次握手", 那么只要服务端发出确认, 新的连接就建立了, 由于现在客户端并没有发出建立连接的请求, 因此不会理睬服务端的确认, 也不会向服务端发送数据. 但服务端却以为新的连接已经建立, 并一直等待客户端发来数据. 这样一来服务端的很多资源就浪费了.

1.3 四次挥手

  1. 客户端服务端发起关闭链接, 并停止发送数据.嗨, 不发了
  2. 服务端收到关闭链接的请求时, 向客户端发送回应, 我知道了, 然后停止接收数据.知道了, 不收了
  3. 服务端发送数据结束之后, 向客户端发起关闭链接, 并停止发送数据.嗨, 我也不发了
  4. 客户端收到关闭链接的请求时, 向服务端发送回应, 我知道了, 然后停止接收数据.知道了, 我也不收了

1.4 为什么需要四次挥手?

  • 因为TCP是全双工通信(通信的双方可以同时发送和接收信息的信息交互方式)的, 在接收到客户端的关闭请求时, 还可能在向客户端发送着数据, 因此不能在回应关闭链接的请求的同时发送关闭链接的请求.

2. HTTP和HTTPS有什么区别?

  • HTTP协议是一种使用明文数据传输的网络协议.
  • HTTPS协议可以理解为HTTP协议的升级, 在HTTP的基础上增加了数据加密. 在数据进行传输之前, 对数据进行加密, 然后再发送到服务器. 这样, 就算数据被截获, 也很难看到明文数据.

2.1 HTTPS的加密方式

  • HTTPS采用对称加密和非对称加密结合的方式来进行通信.
  • HTTPS不是应用层的新协议, 而是HTTP通信接口用SSL(Secure Socket Layer安全套接层)和TLS(Transport Layer Security 安全传输层协议,SSL的新版本3.1)来加强加密和认证机制.
    • 对称加密: 加密和解密都是同一个秘钥.
    • 非对称加密: 分为公钥和私钥, 公钥双方保留

2.2 HTTP和HTTPS的建立连接过程

  • HTTP
  1. 建立连接完毕以后客户端会发送请求给服务端.
  2. 服务端接收请求并且做出响应发送给客户端.
  3. 客户端收到响应并且解析响应给用户.
  • HTTPS
  1. 私钥加密的密文只有公钥才能解开;公钥加密的密文只有私钥才能解开。
  2. 在使用HTTPS时需要保证服务端配置了正确的安全证书
  3. 客户端发送请求到服务端
  4. 服务端返回公钥和证书到客户端
  5. 客户端接收后, 会验证证书的安全性, 如果通过则会生成一个随机数, 用公钥对其加密(只有服务端的私钥才能解开), 发送到服务端
  6. 服务端接收到这个加密后的随机数后, 会用私钥对其解密, 得到真正的随机数, 然后调用这个随机数当做秘钥对需要发送的数据进行对称加密.
  7. 客户端接收到加密后的数据, 使用之前生成的随机数秘钥对数据进行解密, 并且解析数据呈现给用户.

2.3 HTTP协议中GET和POST的区别

  • GET在特定的浏览器和服务器对URL的长度是有限制的.
  • POST不是通过URL进行传值, 理论上不受限制.
  • GET会把请求参数拼接到URL后面, 不安全.
  • POST把参数放到请求体里面, 会比GET相对安全些.
  • GET比POST的请求速度快.
    • POST请求的过程, 会先将请求头发送给服务器确认, 然后才真正的发送数据
    • GET请求过程会在连接建立后就将请求头和数据一起发送给服务器.
  • POST的请求过程
    • 三次握手之后, 第三次会把POST请求头发送到服务端
    • 服务器返回100 continue响应
    • 客户端开始发送数据
    • 服务器返回200 OK响应

3. AFNetworking

3.1 AFN2.0 和3.0的主要区别

  • AFN3.0去除了所有NSURLConnection请求的API
  • AFN3.0使用NSURLSession代替AFN2.0的常驻线程

3.2 AFN2.X常驻线分析

  • 常驻线程
    • 指的就是那些不会停止,一直存在于内存中的线程。
    • AFNetworking 2.0 专门创建了一个线程来接收 NSOperationQueue 的回调,这个线程其实就是一个常驻线程。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        // 先用 NSThread 创建了一个线程
        [[NSThread currentThread] setName:@"AFNetworking"];
        // 使用 run 方法添加 runloop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

image.png

  • 虽然说,在一个 App 里网络请求这个动作的占比很高,但也有很多不需要网络的场景,所以线程一直常驻在内存中,也是不合理的。
  1. 在请求完成后我们需要对数据进行一些处理, 如果我们在主线程中处理就会导致UI卡顿
  2. 这时我们就需要一个子线程来处理事件和网络请求的回调. 但是子线程在处理完事件后就会自动结束生命周期,
    • 这时后面的一些网络请求的回调我们就无法接收了,
    • 所以我们就需要开启子线程的RunLoop使线程常驻来保活线程.

3.3 AFN3.X不在常驻线程的分析

  • AFNetworking 在 3.0 版本时,使用苹果公司新推出的 NSURLSession 替换了 NSURLConnection,从而避免了常驻线程这个坑。
    • NSURLSession 可以指定回调 NSOperationQueue,这样请求就不需要让线程一直常驻在内存里去等待回调了。
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
    • NSURLSession 发起的请求,可以指定回调的 delegateQueue,不再需要在当前线程进行代理方法的回调。所以说,NSURLSession 解决了 NSURLConnection 的线程回调问题。
  • AFNetworking 2.0 使用常驻线程也是无奈之举,一旦有方案能够替代常驻线程,它就会毫不犹豫地废弃常驻线程。
  1. 在AFN3.X中使用的是NSURLSession进行封装,
    • 对比NSURLConnection, NSURLSession不需要再当前的线程等待网络回调,
    • 而是可以让开发者自己设定需要回调的队列.
  1. 在AFN3.X中使用了NSOperationQueue管理网络,
    • 并设置self.operationQueue.maxConcurrentOperationCount = 1;,保证了最大的并发数为1,
    • 也就是说让网络请求串行执行. 避免了多线程环境下的资源抢夺问题.

3.4 AFNetworking建立HTTPS连接流程图

image.png

4. WebSocket

  • Socket是套接字, 描述ip地址和端口, 它本身并不是协议, 而是一个调用接口, 为了大家使用更底层的协议(TCP/UDP), 是对TCP/IP或UDP/IP的封装. 是应用层与传输层间的一个抽象层.
  • WebSocket是一个协议, 是基于http协议的, 建立在TCP连接之上, 是一个应用层协议, 和socket不是一个概念.
  • WebSocket的特点
    • WebSocket可以传输文本和二进制.
    • WebSocket的协议头是ws开头, 不是http.

4.1 WebSocket和HTTP协议

    • WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通信的协议.
    • HTTP协议是一种无状态、无连接的、单向的应用层协议.才用了请求/响应模型.
      • 通信请求只能由用户端发起, 服务端对请求做出应答. 这种通信模型有一个弊端:
      • HTTP协议无法实现服务器端动向用户端发起消息.
      • 这种单项请求的特点, 注定了假如服务端有连续的状态变化, 用户端要获知就非常麻烦.
  • 大多数Web程序通过频繁的异步JS和XML请求实现长轮询. 轮询的效率非常低, 浪费资源.
  • WebSocket连接允许用户端和服务端之间进行全双工通信, 以便任一方都可以通过建立的连接将数据发送到另一方.
    • WebSocket只要建立一次连接, 即可以一直保持连接状态. 这相比于轮询的方式更高效.

4.2 WebSocket与Socket的关系

  • Socket其实并不是一个协议, 而是为了方便用TCP/UDP二笼统出来的一层, 是位于应用层和传输层之间的一组接口.
    • Socket其实就是一个门面模式, 它把复杂的TCP/IP协议族隐藏在Socket接口后面,
    • 对用户来说, 一组简单的接口就是一律让Socket去组织数据, 以符合指定的协议.
    • 当两台主机通信时, 必须通过Socket连接, Socket则利用TCP/IP协议建立TCP连接.
  • TCP连接则更依靠于底层的IP协议, IP协议的连接则依赖于数据链路层等更低层次.
  • WebSocket则是一个典型的应用层协议. 区别是Socket应用层与传输层间的一个抽象层, WebSocket是应用层协议.

4.3SocketRocket

  • SocketRocket是一个WebSocket客户端, 采用OC编写.
    • 支持TLS
    • 使用NNStrea/CFNetworking
    • 使用ARC
    • 采用并行架构. 大部分的工作由后端的工作队列完成.
    • 基于委托代理编程
  • 可通过CocoaPods集成SocketRocket

5. WebRTC

5.1 什么是WebRTC?

  • WebRTC发布于2011年, 是HTML5规范, 可用于直接在浏览器和设备之间添加实时媒体通信.
  • 简单地说, WebRTC可以通过网页实现语音和视频通信. 而且你可做到这一点, 而无需再浏览器中安装任何插件.
  • 2016年,已经有安装20亿个能够与WebRTC一起使用的浏览器。
  • WebRTC也是完全免费的。它是已嵌入到浏览器中的开源项目,但是你可以根据自己的需要采用它。
  • WebRTC技术已经较为成熟,其集成了最佳的音/视频引擎,十分先进的codec,但是Google对于这些技术不收取任何费用。

5.2 WebRTC如何工作?

  • 如果几年前你想构建允许语音或视频通话的任何东西,那么你很可能会使用C / C ++。这意味着较长的开发周期和较高的开发成本。
  • WebRTC用Javascript API代替C / C ++。
  • WebRTC在顶部带有一个Javascript API层,你可以在浏览器中使用它。
  • 今天的WebRTC在所有现代浏览器中都可用。 Google Chrome,Mozilla Firefox,Apple Safari和Microsoft Edge支持WebRTC。
  • 你也可以“使用” WebRTC,并将其集成到应用程序或嵌入式设备中,而根本不需要浏览器。

三、应用篇

1. SDWebImage怎么做缓存的?

  • 采用了二级缓存策略, 图片缓存的时候, 在内存中有缓存, 在磁盘中也有缓存
  • 内存缓存使用NSCache做的.

1.1 缓存步骤

  1. 下载图片, 将图片缓存在内存中
  2. 判断图片的格式为png或jpeg, 将图片转成NSData数据
  3. 获取图片的存储路径, 图片的文件名是通过传入key经过MD5加密后获得的
  4. 将图片存进磁盘中

1.2 如何获取图片

  1. 在内存缓存中找
  2. 如果内存中找不到, 去默认磁盘目录中找, 找不到在去自定义磁盘目录中找
  3. 如果磁盘中也找不到就会下载图片
  4. 获取图片数据之后, 根据图片类型将图片数据从NSData转成UIImage
  5. 默认对图片进行解压缩, 生成位图图片
  6. 将位图图片返回

1.3 图片是如何被解压缩的?

  1. 判断图片是否是动图, 如果是就不能解压缩
  2. 判断图片是否透明, 如果是, 不能解压缩
  3. 判断图片的颜色控件是不是RGB, 如果不是, 就不能解压缩
  4. 根据图片的大小创建一个上下文
  5. 将图片绘制在上下文中
  6. 从上下文中读取一个不透明的位图图像, 该图像就是解压缩后的图像
  7. 将位图图像返回

1.4 NSCache

  • NSCache说白了就是做缓存专用的一个系统类
  • 类似可变字典一样, 但是NSCache是线程安全的, 系统类自动做好了加锁和释放锁等一系列的操作,
    • 还有一个重要的是如果内存不足的时候, NSCache会自动释放掉存储的对象, 不需要开发者手动干预.

1.5 SDWebImage是如何解决tableView复用时出现图片错乱问题的?

  • 错乱是在UIImageView+WebCache文件中这个方法每次都会调用[self sd_cancelCurrentImageLoad].

SDWebImage大致流程

image.png

2. 页面卡顿

2.1 iPhone成像和卡顿的产生

  • 在屏幕成像的过程中, CPU和GPU起着至关重要的作用
    • CPU: 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
    • GPU: 纹理的渲染
  • 在iOS中是双缓冲机制, 有前帧缓存、后帧缓存

image.png

  • 屏幕成像原理

image.png

  • 卡顿产生的原因

  • 垂直同步信号VSyn image.png

  • CPU的卡顿优化

image.png

  • GPU的卡顿优化

image.png

  • 卡顿检测

image.png

2. 离屏渲染

image.png

  • 常见触发离屏渲染的几种情况:
  1. 使用了mask的layer(layer.mask)
  2. 需要进行裁剪的layer(layer.masksToBounds/ view.clipsToBounds)
  3. 设置了组透明度为YES, 并且透明度不为1的layer(layer.allowsGroupOpacity/ layer.opacity)
  4. 添加了投影的layer(layer.shadow)
  5. 采用了光栅化的layer(layer.shouldRasterize)
  6. 绘制了文字的layer(UILabel、CATextLayer、Core Text等)

image.png

3. 动画效果

3.1 Lottie

  • 手动编写动画的代码非常复杂,不容易维护,很多动画细节的调整还需要和动画设计师不断沟通打磨。
  • Lottie是Airbnb开源的动画框架, 很好地解决了动画制作与开发隔离及多平台统一的问题.
  • 动画设计师做好动画以后, 可是使用After Effects将动画导出成JSON文件,
  • 然后由Lottie加载和渲染这个JSON文件.
  • 这些动画,由动画设计师使用After Effects创作, 然后使用Bodymovin(After Effects的插件)进行导出的,
  • 开发者完全不用做什么额外的代码工作, 就能够使用原生方式将其渲染出来.
3.1.1 Bodymovin
  • 你需要先到Adobe 官网下载 Bodymovin 插件,并在 After Effects 中安装。
  • 使用 After Effects 制作完动画后,选择 Windows 菜单,找到 Extensions 的 Bodymovin 项,在菜单中选择 Render 按钮就可以输出 JSON 文件了。
  • LottieFiles 网站还是一个动画设计师分享作品的平台,每个动画效果的 JSON 文件都可下载使用。
  • 所以,如果你现在没有动画设计师配合的话,可以到这个网站去查找并下载一个 Bodymovin 生成的 JSON 文件,然后运用到工程中去试试效果。
3.1.2 iOS中使用Lottie
  1. 在 iOS 开发中使用 Lottie 也很简单,只要集成 Lottie 框架,然后在程序中通过 Lottie 的接口控制 After Effects 生成的动画 JSON 就行了。
LOTAnimationView *animation = [LOTAnimationView animationNamed:@"Lottie"];
[self.view addSubview:animation];
[animation playWithCompletion:^(BOOL animationFinished) {
  // 动画完成后需要处理的事情
}];
3.1.3 Lottie实现原理
  • Lottie在iOS内做的事情就是将After Effects编辑的动画内容, 通过JSON文件这个中间媒介,
  • 一一映射到iOS的LayerModel、Kerframe、ShapeItem、DashElement、Marker、Mask、Transform这些类的属性中并保存了下来,
  • 接下来再通过CoreAnimation进行渲染。这就和你手写动画代码的实现是一样的,只不过这个过程的精准描述, 全部由动画设计师通过JSON文件输入进来了。
3.1.4 使用Lottie的好处
  1. 这样的工作流程是未来的趋势,将描述业务的代码转换成JSON这样的中间代码, 不同平台再对相同的中间代码进行解析处理,以执行中间代码描述的业务逻辑。
  2. 这样做不仅可以减轻App包的大小,实现多端逻辑的统一处理,还可以让团队分工更加明确,一部分人专门开发业务代码,另一部分人负责端内稳定性、质量把控、性能提升工作的建设。

4. iOS系统更新主要差别

  1. iOS9, 从HTTP升级到HTTPS, App瘦身, 新增UIStackView
  2. iOS10, 新增通知推送相关的操作, 自定义通知弹窗, 自定义通知类型
  3. iOS11, 无线调试, 齐刘海, 导航条, 安全距离
  4. iOS12, 启动速度优化, 启动速度提升40%, 键盘响应速度提升50%, 相机响应速度提升70%
  5. iOS13, 暗黑模式
  6. iOS14, 画中画, 小组件, 隐私政策
  7. iOS15, 加强隐私政策, 导航栏背景颜色失效

5. 悬浮提词器技术实现

分析

  1. iOS决定了开发者没法为用户提供一个系统级别UIView
    • 不能实现所谓的悬浮窗等功能, 所以只能通过AVPictureInPictureController来实现类似的效果.
  1. AVPictureInPictureController一般用于实现视频画中画播放功能.
    • 悬浮提词器效果这个问题就变成了制作一个播放词语的视频, 然后进行播放.
    • 如何保持视频时间和系统事件的同步, 让用户的快进暂停等操作不影响到时间变化.
    • 比如用户暂停了视频播放, 时间就不动了(难点)

实现思路

  1. 制作一段视频, 视频内容就是题词, 速率和真实时间一致. 在用户操作后进行播放.
    • 事实上这个方案行不通, 因为用户很容易通过暂停快进等操作导致时间不同步.
  1. 如果只是在App内部显示当前题词, 我们只需要通过UILabelNSTimer就很容易实现.
    • 如果能在AVPictureInPictureController控制的图层上添加视图, 也就能很方便的达到需求,
    • 虽然可以通过比较硬核的方式在画中画上添加UIView, 但会有很多问题, 如用户缩放手势, UIView不会随之缩放
  1. 最终才用了视频实时渲染的方案,
    • 当用户开始使用画中画, 我们实时渲染当前的题词, 我们在视频停止播放的时候, 也要刷新屏幕时间.
    • 这样就能呈现给用户一个随着系统事件实时刷新题词的提词器了 -- iOS14才开始支持画中画功能, iPhone模拟器目前不支持画中画功能, iPad模拟器支持

代码实现流程

  1. 使用一个tmp.mov视频文件作为视频进入画中画模式的呈现, 这个视频非常短, 本身也没有任何内容.
  2. 等到视频加载完成, 调用setupComposition方法进行视频合成的配置.
    • 使用自己创建的HintVideoComposition作为视频的合成器,
    • 然后通过HintVideoCompositionInstruction进行视频合成.
  1. 核心部分就是在HintVideoCompositionInstruction这个类中实现.
    • 当视频需要我们提供界面的时候, 我们在getPixelBuffer提供对应的界面. 工程中使用了CoreText绘制了当前题词
    • 这时, 基本流程已经走通了. 用户使用画中画功能, 视频调用合成器进行显示内容的处理, 我们提供需要显示的画面.
  1. 在视频加载完成以后, 除了配置合成器, 我们还开启了一个定时器.
    • 定时器开启后, 我们每次都需要给AVPlayerItem进行赋值, 以此触发视频的刷新,
    • 这样一来不论用户是否播放暂停视频, 我们都能在界面上显示最新的内容.

6. 启动时间优化

image.png

image.png

  • 启动时间是用户点击App图标, 到第一个界面展示的时间.
  • 以main函数作为分水岭, 启动时间其实包含了两部分,
    • main函数之前, 分析并加载动态库, 注册需要的类, Category中的方法也会注册到对应的类中, 执行必要的初始化+load方法
    • main函数到第一个界面的viewDidAppear.
  • 所以, 优化也是从两个方面进行的, 建议先优化第二部分, 因为绝大多数App的瓶颈在自己的代码里.

6.1 mian函数之前的启动优化

image.png

  • 减少动态库的数量
  • 合并动态库, 比如自己写的UI控件合并成自己的UIKit
  • 确认动态库是optional还是required.
    • 如果该Framework在当前App支持的所有iOS系统版本都存在,
    • 那么就设为required, 否则就设为optional, 因为option会有些额外的检查.
  • 合并Category, 如UIView+FrameUIView+AutoLayout合并成一个.

image.png

  • 将不必须在+load方法中做的事, 放到+initialize中去做.

6.2 main函数之后的启动优化

  • main函数开始执行到显示出第一个页面, 这段时间做了哪些事?
  1. 执行didFinishLaunchingWithOptions方法
  2. 初始化Window, 初始化基础ViewController
  3. 获取数据
  4. 展示给用户
  • 减少创建线程, 线程不仅有创建时的时间开销, 还会消耗内存, 每个线程大约消耗1kb的内存空间.
    • 线程创建的耗时, 区间范围在4-5毫秒, 创建线程后启动线程的耗时区间为5-100毫秒, 平均在29毫秒.
    • 这是很大的时间开销, 若在应用启动时开启多个线程, 则尤为明显. 多次的上下文切换会带来开销.
    • 在开发中避免滥用多线程.
  • 合并或者删减不必要的类/分类/函数, 类越多, 函数越多, 启动越慢.
  • 在设计师可接受的范围内, 尽量使用小的图片.

6.3 AppDelegate中

  • 从AppDelegate先入手优化
  • didFinishLaunchingWithOptionsapplicationDidBecomeActive
  • 优化的核心思想就是, 能延时的延时, 不能延时的尽量放到后台去优化.
    • 日志、统计等必须在App已启动就最先配置的事件, 仍留在didFinishLaunchingWithOptions里启动.
    • 项目配置、环境配置、用户信息的初始化、推送、IM等事件, 这些功能在用户进入App首屏之前是必须要加载完的, 放到开屏广告页面的viewDidAppear里.
    • 其他SDK和配置事件, 由于启动时间不是必须的, 可以放在首屏的viewDidAppear方法里, 在这里不会影响启动时间.
    • 每次用NSLog方式打印会隐式的创建一个Calendar, 因此需要删减启动时各业务的log, 或者仅针对内测版输出log.
    • 尽量不要在didFinishLaunchingWithOptions里面创建和开启多线程.

image.png image.png

7. App电量消耗

  • 耗电的主要来源
  • 定位、网络请求、CPU处理、GPU处理、蓝牙等都会增大电量消耗

image.png

7.1 耗电优化

  • 尽可能降低CPU、GPU功耗
  • 少用定时器
  • 优化I/O操作
    • 尽量不要频繁写入小数据, 最好批量一次性写入
    • 读写大量重要数据时, 考虑用dispatch_io, 其提供了基于GCD的异步操作文件I/O的API. 用dispatch_io系统会优化磁盘访问
    • 数据量比较大的, 建议使用数据库
  • 网络优化
    • 减少、压缩网络数据
    • 如果多次请求的结果是相同的, 尽量使用缓存
    • 使用断点续传, 否则网络不稳定时可能多次传输相同的内容
    • 网络不可用时, 不要尝试执行网络请求
    • 让用户可以取消长时间运行或速度很慢的网络操作不要蒙版盖住, 设置合适的超时时间
    • 批量传输, 下载视频流时, 不要传输很小的数据包, 直接下载整个文件或者一大块一大块的下载.
    • 如果下载广告, 一次性多下载一些, 然后再慢慢展示.
  • 定位优化

image.png

8. App瘦身

  • 删除陈旧代码、陈旧xib/SB、无用的图片资源(检测未使用图片的工具LSUnuserdReaources)
  • 无损压缩图片, 本地音视频压缩.
  • 使用webP格式的图片
  • 减少使用静态库
  • 一些主题类的东西提供下载功能, 不直接打包在应用包里面, 按需加载资源
  • iOS 9 之后的新特性App Slicing、Bitcode、On Demand Resources

image.png

9. 原生与前端交互

JavaScriptCore

  • JavaScriptCore 为原生编程语言 Objective-C、Swift 提供调用 JavaScript 程序的动态能力,还能为 JavaScript 提供原生能力来弥补前端所缺能力。
  • 桥梁作用
  • JavaScriptCore,原本是 WebKit 中用来解释执行 JavaScript 代码的核心引擎
    • 解释执行 JavaScript 代码的引擎自 JavaScript 诞生起就有,不断演进,一直发展到现在
    • 如今苹果公司有 JavaScriptCore 引擎、谷歌有 V8 引擎
    • iOS7 之前,苹果公司没有开放 JavaScriptCore 引擎。
    • 从 iOS7 开始,苹果公司开始将 JavaScriptCore 框架引入 iOS 系统,并将其作为系统级的框架提供给开发者使用
  • JavaScriptCore 框架的框架名是 JavaScriptCore.framework。
  • JavaScriptCore 框架主要由 JSVirtualMachine 、JSContext、JSValue 类组成。
  • JSVirturalMachine 的作用,是为 JavaScript 代码的运行提供一个虚拟机环境。
    • JSContext 是 JavaScript 运行环境的上下文,负责原生和 JavaScript 的数据传递。
      • JSValue 是 JavaScript 的值对象,用来记录 JavaScript 的原始值,并提供进行原生值对象转换的接口方法。

image.png

  • JavaScript 代码的 JavaScriptCore 和原生应用是怎么交互的? image.png

  • JavaScriptCore 和原生应用要想交互,首先要有 JSContext。

  • 除了 Block 外,我们还可以通过 JSExport 协议来实现在 JavaScript 中调用原生代码,

    • 也就是原生代码中让遵循 JSExport 协议的类,能够供 JavaScript 使用。
- (void)testJavaScriptCore {
    // 创建JSVirtualMachine对象
    JSVirtualMachine *jsvm = [JSVirtualMachine new];
    // 使用 jsvm的JSContext对象
    JSContext *ct = [[JSContext alloc] initWithVirtualMachine:jsvm];

    //通过JavaScriptCore在原生代码中调用JavaScript变量
    // 解析执行JavaScript脚本
    [ct evaluateScript:@"var i = 1 + 2"];

    // 转换"i"变量为原生对象
    NSNumber *iNumber = [ct[@"i"] toNumber];
    NSLog(@"var i is %@, iNumber is %@", ct[@"i"], iNumber);

    // 解析执行 JavaScript脚本
    [ct evaluateScript:@"function subtraction(x, y) { return x - y }"];

    // 获得 subtraction 函数
    JSValue *subtraction = ct[@"subtraction"];

    // 传入参数执行subtraction函数
    JSValue *resultValue = [subtraction callWithArguments:@[@(3), @(1)]];

    // 将 subtraction 函数执行的结果转成原生NSNumber来使用
    NSLog(@"function is %@, reusltValue is %@", subtraction, [resultValue toNumber]);
}

总结

  • 总结来说,JavaScriptCore 提供了前端与原生相互调用的接口,接口层上主要用的是 JSContext 和 JSValue 这两个类,
  • 通过 JSValue 的 evaluateScript 方法、Block 赋值 context、JSExport 协议导出来达到互通的效果。

10. CocoaPods

  • CocoaPods是iOS、macOS开发中的包依赖管理工具, 源码使用runy写的, GitHub源码.
  • 如果不使用包依赖管理工具, 我们需要手动管理第三方包, 包括但不限于
    • 将这些第三方库的源码拷贝到项目中.
    • 第三方库代码有可能依赖一些系统framework, 我们需要把第三方库依赖的framework导入到项目中.
    • 当第三方库有更新时, 需要将更新过的代码拷贝到项目中.
  • CocoaPods可以将我们从这些繁琐的工作中解放出来.

10.1 CocoaPods的工作原理

  • CocoaPods是将所有以来的第三方库都放到了Pods项目中.
  • 所有的源码管理工作从主项目转移到了Pods项目.
  • Pods项目最终会编译成一个libPods-项目名.a的静态库, 主项目只需要依赖这个.a静态库即可.
  • 对于libPods-项目名.a这个文件, 可以将其理解为各个第三方库的.a文件的集合.

10.2 开源项目支持CocoaPods

  1. 注册CocoaPods
pod trunk register 邮箱 '昵称' --description='描述'

执行完毕后, CocoaPods会给对应的邮箱发送一封确认邮件, 点击邮件中的确认链接即可.

  1. GitHub上新建仓库
    • 首先需要在GitHub上新建仓库, 新建仓库时选择开源协议,通常选MIT, 项目设置成public.
  1. 将GitHub上的仓库克隆到本地
    • 克隆完之后, 在本地仓库上新建项目, 并完成对应的功能. 推送到远程仓库.
  1. 新建podspec文件
    • 支持CocoaPods的开源库, 都需要具备podspec文件, podspec文件可以理解成是对该开源库的描述, 包括作者信息, 项目主页等. pod spec create 项目名
  1. 编辑podspec文件
    • podspec文件新建之后, 里面会有一些信息, 可以看做是一个模板, 只需要稍对podspec的文件做改动即可
s.name         = "项目名"
s.version      = "1.0.0"
s.summary      = "This is a moreResponseArea Button"
s.homepage     = "https://github.com/acBool/ACMoreResponseButton"
s.license      = "MIT"
s.author       = { "wmn" => "acbool@163.com" }
s.platform     = :ios, "9.0"
s.source       = { :git => "https://github.com/acBool/ACMoreResponseButton.git", :tag => "1.1.0" }
s.source_files  = "MoreResponseButtonExample/MoreResponseButtonExample/ACMoreResponseButton/*"
s.exclude_files = "UIKit"
s.requires_arc = true

5. 分支新建tag, 并推送到远程仓库 6. 验证podspec文件pod spec lint 项目名.podspec

    • 如果验证不通过, 会提示警告、error, 都需要解决
  1. 提交至CocoaPods
  • 只有podspec文件验证通过后, 才能将开源库提交至CocoaPods pod trunk push 项目名.podspec

10.3 私有库支持CocoaPods

  • 公司项目中, 有时一些通用的功能会封装成框架, 这些框架也是可以支持CocoaPods的. 我们希望这些框架只为公司内部使用, 不开源, 可称之为私有库.
  • 私有库支持CocoaPods的步骤和公有库基本一致, 区别就是不需要提交至CocoaPods, 验证podspec文件通过后就可以了.
  • 使用私有库时, Podfile文件的写法也有细微区别.
// 注意替换私有git域名
pod 'ProjectName',git=>"https://XXX.git"

11. CTMediator

  • 架构是需要演进的, 如果项目规模大了还不演进, 必然就会拖累业务的发展速度.
  • 当业务需求量和团队规模达到一定程度后, 任何一款App都需要考虑架构设计的合理性.
  • 快速迭代的需求开发和漫长重构之间的矛盾, 就像在飞行的飞机上换引擎.
  • 将业务完全解耦, 通用功能下沉
    • 每个业务都是一个独立的Git仓库
    • 每个业务都能生成一个Pod库, 最后再集成到一起
  • MVC是很好的面向对象编程范式, 非常适合个人开发或者小团队开发.
    • 项目大了, 人员多了以后, 这种架构就扛不住了. 因为这时候功能的量级不一样了.
    • 一个大功能, 会由多个功能合并而成, 每个功能都成了一个独立的业务, 团队成员也会按照业务分成不同的团队.
    • 此时, 简单的逻辑、视图、数据划分再也无法满足App大规模工程化的需求.

1. 模块粒度如何划分? SOLID原则

  • iOS组件, 应该是包含UI控件、相关多个小功能的集合, 是一种粒度适中的模块.
  • S(SRP)单一功能原则: 对象功能要单一, 不要在一个对象里添加很多功能.
  • O(OCP)开闭原则: 扩展是开放的, 修改是封闭的.
  • L(LSP/LOD)里氏替换原则:子类对象是可以替换基类对象的; 迪米特法则:只与你的直接朋友交谈, 不跟"陌生人"说话.
  • I(ISP)接口隔离原则: 接口的用途要单一, 不要在一个接口上根据不同入参实现多个功能.
  • D(DIP)依赖反转原则: 方法应该依赖接口, 不要依赖实例. iOS开发就是高层业务方法依赖于协议.

image.png

2. 如何分层?

  • 层级最多不要超过三个.
  1. 底层可以使与业务无关的基础组件, 比如网络和存储等.
  2. 中间层一般是通用的业务组件, 比如账号、埋点、支付、购物车等.
  3. 最上层是迭代业务组件, 更新频率最高.

image.png

3. 多团队如何协作?

  • 合理的团队结构应该是这样的:
    • 首先, 需要一个专门的基建团队, 负责业务无关的基础功能组件和业务相关通用业务组件的开发.
    • 然后, 每个业务都由一个专门的团队来负责开发. 业务可以按照功能耦合度来划分, 耦合度高的业务可以划分成单独的业务团队.
    • 基建团队人员应该是流动的, 从业务团队里来, 再回到业务团队中去. 避免资源浪费, 解决重复建设的问题.
  • 好的架构, 需要在业务开发过程中及早发现开发的痛点, 进行有针对性的改良, 不然就会和实际开发越走越远.
  • 好的架构一定是不简单、不通用的, 但是一定是接地气的, 这样才能更适合自己的团队, 才能够用得上能落地.

中间者架构

  • 采用中间者统一管理的方式, 来控制App的整个生命周期中组件间的调用关系.
  • 好的架构一定是健壮的、灵活的. 中间者架构的易管控使架构更稳固, 易扩展带来了灵活性. image.png
  • CTMediator使用的是运行时解耦.
// CTMediator调用弹窗显示
[self performTarget:kCTMediatorTargetA
                 action:kCTMediatorActionShowAlert
                 params:paramsToSend
      shouldCacheTarget:NO];
  • Casa的CTMediator实现原理
  • Casa的CTMediator实践
  • CTMediator 本质就是一个方法,用来接收 target、action、params。由于 target、action 都是字符串,params 是字典,对于调用者来说十分不友好,因为调用者要写字符串,而且调用的时候若是不看文档,他也不知道这个字典里该塞什么东西。
  • 所以实际情况中,调用者是不会直接调用 CTMediator 的方法的。那调用者怎么发起调用呢?通过响应者给 CTMediator 做的 category 或者 extension 发起调用。
  • category 或 extension 以函数声明的方式,解决了参数的问题。调用者看这个函数长什么样子,就知道给哪些参数。在 category 或 extension 的方法实现中,把参数字典化,顺便把 target、action 这俩字符串写死在调用里。
  • 于是,对于调用者来说,他就不必查文档去看参数怎么给,也不必担心 target、action 字符串是什么了。这个 category 是一个独立的 Pod,由响应者业务的开发给到。
  • 所以,当一个工程师开发一个业务的时候,他会开发两个 Pod,一个是 category Pod,一个是自己本身的业务 Pod。这样就完美解决了 CTMediator 它自身的缺点。
  • 对于调用者来说,他不会直接依赖 CTMediator 去发起调用,而是直接依赖 category Pod 去发起调用的。这么一来,CTMediator 方案就完美了。
  • 然后还有一点可能需要强调:基于 CTMediator 方案的工程,每一个组件无所谓是 OC 还是 Swift,Pod 也无所谓是 category 还是 extension。也就是说,假设一个工程由 100 个组件组成,那可以是 50 个 OC、50 个 Swift。因为 CTMediator 抹去了不同语言的组件之间的隔阂,所以大家老的 OC 工程可以先应用 CTMediator,把组件拆出来。然后新的业务来了,用 Swift 写,等有空的时候再把老的 OC 改成 Swift,或者不改,都是没问题的。
  • 解耦的精髓在于业务逻辑能够独立出来, 并不是形式上的解除编译上的耦合.
    • 在考虑架构设计时, 我们更多的还是需要在功能逻辑和组件划分上做到同层级解耦, 上下层依赖清晰,
    • 这样的结构才能够使得上层组件易插拔, 下层组件更稳固.

11.1 MVP架构

  • Model View Presenter

  • 有新的View搞一个新的Presenter来做管理

  • ViewController中设置Presenter, Presenter相当于取代了控制器的角色, 对控制器进行一个瘦身

image.png

  • Presenter还能这样优化

image.png image.png

image.png

  • View层这样用

image.png

11.2 MVVM架构

  • 属性监听, 赋值, KVO做监听有点麻烦, Facebook有个FBKVOController, RAC框架有点重
  • 控制器做了瘦身, 里的代码很少

image.png

  • V

image.png

image.png

  • VM

image.png

image.png

image.png

12. LLVM编译器、链接器

  • LLVM编译器, 相比XCode5之前使用的GCC, 编译速度提高了3倍.
  • LLVM是编译器工具链技术的一个集合. 而其中的lld项目, 就是内置链接器.
    • 链接器会对每个文件进行编译, 生成Mach-O可执行文件,
    • 链接器会将项目中的多个Mach-O文件合并成一个.
  • 链接器最主要的作用, 就是将符号绑定到地址上.
  • iOS编写的代码是先用编译器把代码编译成机器码, 然后直接在CPU上执行机器码.
  • 编译器和解释器的区别
    • 采用编译器生成机器码执行的好处是效率高, 缺点是调试周期长
    • 解释器执行的好处是便携调试方便, 缺点是执行效率低
  • 编译器和解释器的对比 image.png

12.1 编译的几个主要过程

  1. 你写好代码后, LLVM会预处理你的代码, 比如把宏嵌入到对应的位置.
  2. 预处理完后, LLVM会对代码进行词法分析和语法分析, 生成AST(抽象语法树).
    • AST结构上比代码更精简, 遍历起来更快, 所以使用AST能够更快速地进行静态检查,
    • 同时还能更快地生成中间表示(IR).
  1. 最后AST会生成IR(一种更接近机器码的语言, 区别在于和平台无关),
    • 通过IR可以生成多份适合不同平台的机器码.
    • 对于iOS系统, IR生成的可执行文件就是Mach-O. image.png

12.2 编译时链接器做了什么?

  • 编译阶段由于有了链接器, 你的代码可以写在不同的文件里, 每个文件都能够独立编成Mach-O文件进行标记.
    • 编译器可以根据你修改的文件范围来减少编译,
    • 通过这种方式提高每次编译的速度.
  • Mach-O文件里面的内容, 主要就是代码和数据
    • 代码是函数的定义; 数据是全局变量的定义, 包括全局变量的初始值.
    • 不管是代码还是数据, 它们的实例都需要有符号将其关联起来.
  • 因为Mach-O文件里的那些代码, 如if、for生成的机器指令序列,
    • 要操作的数据会存储在某个地方, 变量符号就需要绑定到数据的存储地址.
  • 你写的代码还会引用其他的代码, 引用的函数符号也需要绑定到该函数的地址上.
  • **链接器的作用就是完成变量、函数符号和其地址绑定这样的任务.**这里我们所说的符号, 就可以理解为变量名和函数名.

12.3 链接器对代码主要做了哪几件事?

  1. 去项目文件里查找目标代码文件里没有定义的变量.
  2. 扫描项目中的不同文件, 将所有符号定义和引用地址收集起来, 并放到全局符号表中.
  3. 计算合并后长度及位置, 生成同类型的段进行合并, 建立绑定.
  4. 对项目中不同文件里的变量进行地址重定位.

image.png

12.4 注入动态库的方式实现极速编译调试

12.4.1 Flutter Hot Reload
  • Flutter是Google开发的一个跨平台开发框架, 调试也是快速实时的.
  • 在Flutter编辑器中修改文字代码后, 点击reload, App不用重启, 模拟器的内容就会立刻改变.
  • Flutter实现实时编译的原理
    • Flutter会在点击reload时取查看上次编译以后改动过的代码, 重新编译涉及到的代码库.
    • 重新编译过的库会转换成内核文件发到Dart VM里, DartVM会重新加载新的内核文件,
    • 加载后会让Flutter framework触发所有的Widgets和Render Objects进行重建、重布局、重绘.
  • Flutter为了能够支持跨平台开发, 使用了自研的Dart语言配合在App内集成Dart VM的方式运行Flutter程序.
Injection for XCodeGitHub地址
  • 使用步骤
  1. Mac的App Store上下载安装InjectionIII.

image.png 2. 打开InjectionIII, Open Project, 选择你的项目目录. image.png

  1. 选择的项目会在Open Recent中出现, 保持File Watcher的选项勾选.
  2. 在AppDelegate的DidFinishLaunchingWithOptions配置InjectionIII的路径
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
#ifdef DEBUG

    //InjectionIII 注入
    [[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];

#else

#endif
    return YES;
}

5. 在需要动态调试的页面控制器中写上injected方法, 把需要操作的UI方法添加到injected中执行, 如果想让全部的控制器都能使用, 直接添加到BaseViewController.

- (void)injected {
    #ifdef DEBUG
        NSLog(@"I've been injected: %@", self);
        [self viewDidLoad];
    #endif    
}

6. 重新编译项目, 控制台可以看到

**💉 InjectionIII connected /Users/***/Desktop/***/**/***.xcworkspace**
**💉 Watching files under /Users/***/Desktop/****
// 下面的只是警告, 作者在Issue中已经解释, 不耽误正常使用.
**💉 💉 ⚠️ Your project file seems to be in the Desktop or Documents folder and may prevent InjectionIII working as it has special permissions.**

image.png 7. 修改完UI, 直接cmd + S就能看到效果, 部分页面可能耗时比较久或无法使用, 正常页面均能使用. enjoy :)

  • Injection工具可以动态地将iOS代码在已运行的程序中执行, 不用重启.
    • Injection会监听源代码文件的变化, 如果文件被改动了,
    • Injection Server就会执行rebuildClass重新进行编译、打包成动态库.dylib文件,
    • 编译、打包成动态库后, 使用writeString方法通过Socket通知运行的App.
- (BOOL)writeString:(NSString *)string {
    const char *utf8 = string.UTF8String;
    uint32_t length = (uint32_t)strlen(utf8);
    if (write(clientSocket, &length, sizeof length) != sizeof length ||
        write(clientSocket, utf8, length) != length)
        return FALSE;
    return TRUE;
}
    • Server会在后台发送和监听Socket消息, Client也会开启一个后台去发送和监听Socket消息.
    • Client接收到消息后会调用inject(tmpfile: String)方法, 运行时进行类的动态替换(新类动态替换旧类).
    • dlopen会把tmpfile动态库文件载入运行的App里, 返回指针dl.
    • 接下来, dlsym会得到tmpfile动态库的符号地址, 然后就可以处理类的替换工作了.
    • 当类的方法都被替换后, 我们就可以开始重新绘制界面了.
    • 使用动态库方式极速调试, 整个过程无需重新编译和重启App.

image.png

13. WKWebView加载优化

  • 打开一个h5页面通常会有较长时间出现白屏, 几秒后出现内容, 在这几秒钟webview做了什么?
    • 初始化webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求js/css资源
    • DOM渲染 -> 解析JS执行 -> JS请求数据 -> 解析渲染 -> 下载渲染图片

image.png

  • 总的来说, webview渲染需要下面6个步骤
  1. 解析HTML文件
  2. 加载JavaScript和CSS文件
  3. 解析并执行JavaScript
  4. 构建DOM结构
  5. 加载图片等资源
  6. 页面加载完毕
  • 一般页面是在DOM渲染后才能展示, H5首屏渲染白屏问题的原因关键在于, 如何去优化请求下载 -> 渲染之间的耗时成了重点.

优化方案

  1. 降低请求量: 合并资源, 减少HTTP请求书, minify/gzip压缩, webP, lazyload
  2. 加快请求速度: 预解析DNS, 减少域名数, 并行加载, CDN分发
  3. 缓存: HTTP协议缓存请求, 离线缓存manifest, 离线数据缓存 localStorage
  4. 渲染: JS/CSS优化, 加载顺序, 服务端渲染模板直出.

14. 性能检测工具Instruments

15. 如何降低App的崩溃率

  • 无痕植入的思路: AOP(面向切面编程)的思想. 基于OC的runtime运行时特性, 打点, 自动在App运行时实时捕获导致App崩溃的因子, 然后通过针对性的的方法去应对因子, 做防崩处理.

15.1 常见的7大崩溃产生原因

  1. unrecognized selector crash造成的崩溃: 没有找到对应的方法选择器.
  2. KVO 造成的崩溃: KVO的被观察者在dealloc时仍然注册着KVO导致的崩溃, 重复添加观察者或重复移除观察者.
  3. NSNotification 造成的崩溃: 当一个对象添加了Notification之后, 在dealloc的时候, 仍然持有Notification.
  4. NSTimer 造成的崩溃: 需要在合适的时机invalidate定时器, 否则就会犹豫定时器的timer强引用target导致target不被释放, 造成内存泄漏.
  5. 容器类型越界造成的崩溃: Array越界、Dictionary插入nil
  6. 非主线程刷新UI造成的崩溃: 在子线程刷新UI会导致App崩溃.
  7. 野指针造成的崩溃: 访问了野指针, 对象已经被释放.

15.2 常见的7大崩溃解决思路

15.2.1 unrecognized selector crash造成的崩溃处理
  • 采用拦截调用的方式, 在找不到调用的方法之后, App崩溃之前, 我们有机会通过重写NSObject的四个消息转发方法来做防崩溃处理.
+ (BOOL)resolveClassMethod:(SEL)sel; // 动态在方法决议机制, 决议类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel; // 动态的对象方法决议, 决议对象方法

// 后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector; // 转发给其它的一个对象去处理
- (void)forwardInvocation:(NSInvocation *)anInvocation; // 灵活地将目标函数以其他形式执行
  • 拦截调用的整个流程即OC的消息转发机制. runtime提供了3种方式去补救:
  1. 调用resolveInstanceMethod给个机会让类添加这个函数实现.
    • 需要在类的本身动态地添加它不存在的方法, 这些方法对于该类是冗余的.
  1. 调用forwardingTargetForSelector让别的对象去执行这个函数.
    • 可以通过NSInvocation的形式将消息转发给多个对象, 但是开销比较大,
    • 需要创建新的NSInvocation对象, 并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制, 不适合多次重写.
  1. 调用forwardingInvocation(函数执行器)灵活地将目标函数以其他形式执行.
    • 可以将消息转发给一个同一对象, 开销较小, 并且被重写的概率较低, 推荐在这重写.
  • 如果都不行, 系统才会调用doesNotRecognizeSelector抛出异常.

  • 重写NSObjectforwardingTargetForSelector具体步骤:

  1. 为类动态地重建一个桩类.
  2. 动态为桩类添加对应的Selector, 用一个通用的返回0的函数来实现该SELIMP.
  3. 将消息直接转发到这个桩类对象上.
- (id)jh_forwardingTargetForSelector:(SEL)aSelector {
    if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
        IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
        IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
        if (imp != impOfNSObject) {
            NSLog(@"class has implemented invocation");
            return nil;
        }
    }
    
    JHUnrecognizedSelectorSolveObject *solveObject = [JHUnrecoginzedSelectorSolveObject new];
    solveObject.objc = self;
    return solveObject;
}
  • ps: 如果对象的类本身重写了forwardInvocation方法的话, 就不应该对forwardingTargetForSelector进行重写了, 否则会影响到该类型的对象原本的消息转发流程.
15.2.2 KVO 造成的崩溃处理
  • 产生原因主要有2种
  1. KVO的被观察者dealloc时仍然注册着KVO导致的崩溃.
  2. 添加KVO重复添加观察者或重复移除观察者导致的崩溃.

image.png

  • 如上图所示: 一个被观察的对象有多个观察者, 每个观察者又有多个keyPath,

  • 如果观察者和keyPath的数量一多, 很容易不清楚被观察的对象整个KVO关系,

  • 导致被观察者在dealloc的时候, 仍然残存着一些关系没有被注销,

  • 同时还会导致KVO注册者和移除观察者不匹配的情况发生,

  • 尤其是多线程环境下, 导致KVO重复添加观察者或者重复移除观察者的情况, 这种类似的情况比较男排查.

  • 可以这样管理混乱的KVO关系:

  • 让观察者对象持有一个KVO的delegate, 所有和KVO相关的操作均通过delegate来进行管理,

  • delegate通过建立一张Map表来维护KVO的整个关系, 如下图:

image.png

  • 这样做的好处如下:
  1. 如果出现KVO重复添加或移除观察者(KVO注册者不匹配)的情况, delegate可以直接阻止这些异常操作.
  2. 被观察对象dealloc之前, 可以通过delegate自动将与自己有关的KVO关系都注销掉, 避免了KVO的被观察者dealloc时仍然注册着KVO导致的崩溃.
15.2.3 NSNotification造成的崩溃处理
  • iOS9之前, 当一个对象添加了Notification之后, 如果dealloc的时候, 仍然持有Notification, 就会出现NSNotification类型的崩溃.
  • iOS9之后苹果专门针对这种情况做了处理, 所以在iOS9之后, 即使开发者没有移除Observer, Notification崩溃也不会再产生了.
  • 针对iOS9之前的用户, 防止NSNotification崩溃的思路是:
  • 利用method swizzling hook NSObjectdealloc方法,
  • 在对象真正dealloc之前先调用一下[[NSNotificationCenter defaultCenter] removeObserve:self].
15.2.4 NSTimer内存泄漏造成的崩溃处理
  • 产生原因: Runloop -> NSTimer --> <- - 对象 <-VC
  • 这就导致了内存泄漏
  • 处理方法如下:
  • NSTimer和对象间添加一个中间对象, NSTimer强引用中间对象, 中间对象弱引用NSTimer、对象 image.png
15.2.5 容器类型越界造成的崩溃处理
  • 针对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSCache的一些常用的, 可能会导致崩溃的API进行基于runtime的method swizzling, 然后在swizzle的新方法中针对Debug环境和Release加入一些判空处理操作, 从而让这些API变得更难崩溃.
15.2.6 子线程刷新UI造成的崩溃处理
  • 采用基于runtime的swizzleUIView类的刷新UI方法
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
  • 在自定义的交换方法里, 调用上面几个方法时, 判断一下当前的线程, 如果不是主线程, 直接调用dispatch_async(dispatch_get_main_queue(),^{// 原代码});, 来将对应的刷新UI操作转移到主线程来做, 也可统计错误信息Debug模式下给到提示.
15.2.7 野指针造成的崩溃处理
  • 当Bugly统计到Exception Type:SIGSEGV, Exception Codes:SEGV_ACCERR时, 就代表发生了野指针访问.
  • 然而解决野指针造成的崩溃是一件比较棘手的事, 主要是因为崩溃信息很难提供精准的定位, 这就导致野指针崩溃的场景不一定好复现.
  • XCode为了开发阶段调试时就发现野指针问题, 提供了Zombie机制, 能够在发生野指针时提示出现野指针的类, 从而解决了开发阶段出现野指针的问题.
  • 但是线上环境产生的野指针问题, 依旧很难定位到具体的发生野指针的代码. 所以专门针对野指针做一层防崩措施, 在生产环境中就显得很有必要. 常见的一个思路:
  • 在类init初始化的时候做一个标记, 在该类dealloc时再做一个标记. 通过2次的标记来判断是否存在野指针. 但是对于UIVIew、UIImageView这些常用的类来说, 多次分配释放内存的CPU开销还是很大的, 这只是一个思路.
  • 更推荐腾讯的MLeaksFinder.
  • MLeaksFinder的思路:
MLeaksFinder一开始从UIViewController入手,
当一个UIViewController被pop或dismiss后, 该UIViewController包括他的view及subviews将很亏被释放. 
于是, 我们只需要在一个UIViewController被pop或dismiss一小段时间后, 
看看这个UIViewController及它的view、subviews等是否还存在.
MLeaksFinder具体的方法是为积累NSObject添加一个方法 -(void)willDealloc, 该方法的作用是:
先用一个弱指针指向self, 并在一小段时间后, 
通过这个弱指针调用 -(void)assertNotDealloc, 而 assertNotDealloc主要作用是直接调用中断言.
若果它没被释放(即发生了内存泄漏), assertNotDealloc就会被调用中断言.
这样一来, 当一个UIViewController被pop或dismiss时, 我们遍历该UIViewController上所有的view, 依次调用 willDealloc, 若一小段时间(如2s)之后还没释放, 那么指向它的weak指针还是存在的,
所以可以调用其tuntime绑定的方法 willDealloc 来提示野指针内存泄漏.
发文不易, 喜欢点赞的人更有好运气👍 :), 定期更新+关注不迷路~

ps:欢迎加入笔者18年建立的研究iOS审核及前沿技术的三千人扣群:662339934,坑位有限,备注“掘金网友”可被群管通过~