开发小知识(二)

开发小知识(一)

开发小知识(二)

目录

五十一、关联对象

关联对象的 key

实际开发中一般使用属性名作为key。

objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");

另外一种方式是使用get方法的@selecor作为key。这里要知道 _cmd实际上等价于 @selector(getter),两者都是 SEL类型。

objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// 隐式参数 _cmd == @selector(getter)
objc_getAssociatedObject(obj, _cmd)
objc_getAssociatedObject(obj, @selector(getter))
关联对象的懒加载
- (UIView *) testView{
    UIView * testView = objc_getAssociatedObject(self, _cmd);
    if (! testView) {
        testView = [[UIView alloc]init];
        objc_setAssociatedObject(self, _cmd, testView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return testView;
}

五十二、TCP 面向连接的本质是什么?TCP 和 UDP 的区别?

一般面试的时候问UDP和TCP这两个协议的区别,大部分人会回答,TCP 是面向连接的,UDP 是面向无连接的。什么叫面向连接,什么叫无连接呢?在互通之前,面向连接的协议会先建立连接。例如,TCP 会三次握手,而 UDP 不会。为什么要建立连接呢?所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。

为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化状态如下。最初,客户端和服务端都处于 CLOSED 状态。首先,服务端处于 LISTEN 状态,主要为了主动监听某个端口。客户端主动发起连接 SYN,变为 SYN-SENT 状态。然后,服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于ESTABLISHED 状态,因为一发一收成功了。服务端收到 ACK 的 ACK 之后,也同样变为 ESTABLISHED 状态。


另外,TCP 是可以拥塞控制的。它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。UDP 就不会,应用让发就发,从不考虑网络状况。

五十三、高效安全读写方案

读写操作中为了保证线程安全可以为读和写操作都添加锁。但是此种情况似乎有些浪费,往往都是因为写操作会引发线程安全问题,而读操作一般不会引发线程安全问题。为了优化读写效率,一般是允许同一时间有多个读操作,但同一时间不能有多个写操作,且同一时间不能既有读操作又有写操作,即所谓的多读单写。针对该种情况,一般有两种处理方法:读写锁和异步栅栏函数。

读写锁方案pthread_rwlock_t
@property (assign, nonatomic) pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);// 初始化锁

- (void)read {
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}
- (void)write{
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}
- (void)dealloc{
    pthread_rwlock_destroy(&_lock);
}
异步栅栏函数方案

每次必须等前面所有读操作执行完之后,才能执行写操作。数据的正确性主要取决于写入操作,只要保证写入时,线程便是安全的,即便读取操作是并发的,也可以保证数据的正确性。dispatch_barrier_async 使得操作在并发队列里“有序进行”,保证了写入操作的任务是在串行队列里,即必须等所有读操作执行完毕后再执行写操作。注意这里的 队列必须是dispatch_queue_create创建的同一个队列 ,如果dispatch_barrier_async中传入的是全局并发队列,该函数就等同于dispatch_async效果。

@property (strong, nonatomic) dispatch_queue_t queue;
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }


如上图,随着时间的推移有一堆读和写操作,当执行到 dispatch_barrier_async 时会在写的两边加上屏障(栅栏),使其和读操作隔离。

另外提示,如果仅仅只是对写操作加锁,读操作不做任何处理,并不能保证线程安全,仅对写操作加锁仅仅只能保证不会同时出现两个或多个写操作,并不能避免同一时刻既有写操作又有读操作。实现正在进行读操作,此时来了第一个写操作,但是相关锁并没有加锁,所以读写操作可同时进行。

补充:对于线程安全方案,除了加锁之外,还可以借助串行队列确保代码执行的顺序,保证线程安全。

dispatch_queue_t writerRecordsQueue = dispatch_queue_create([@"serialQueue.yawei" UTF8String], DISPATCH_QUEUE_SERIAL);
- (void)clearRecords
{
    dispatch_async(writerRecordsQueue, ^{
        [self.records removeAllObjects];
    });
}

- (void)writeData:(id)data
{
    if (!data) {
        return;
    }
    dispatch_async(writerRecordsQueue, ^{
        [self.records addObject:data];
    });
}

五十四、死锁

所谓死锁,通常指有两个线程 T1 和 T2 都卡住了,并等待对方完成某些操作。T1 不能完成是因为它在等待 T2 完成。T2 也不能完成,因为在等待 T1 完成。于是大家都完不成,就导致了死锁(DeadLock),就类似一条比较窄的马路有两辆车相向而行,互相等着对方过了之后再过。

重要:sync 函数当前串行队列 中添加任务会卡住当前线程,产生死锁。这里要额外注意 当前串行队列

案列一:

- (void)ViewDidLoad{
   NSLog(@"1");// 任务1
  dispatch_sync(dispatch_get_main_queue(),^{
      NSLog(@"2");// 任务2
  });
  NSLog(@"3");// 任务3
}
结合上述重要提示分析:

上述情况会产生死锁。 ViewDidLoad 存在于主队列,向主队列 (当前串行队列) 中添加了 NSLog(@"2"); 任务,主而队列又是串行队列,所以导致死锁。

结合线程和队列实际情况分析:

首先要知道线程和队列的关系。执行代码逻辑时,线程从队列中取出任务并放入线程中执行,当线程中对应模块执行完毕时,队列同时移除对应模块。


述方法展开后,主线程存在任务 1 、sync 、任务3,主队列中原本仅存在viewDidLoad ,当主线程从任务 1 依次执行到 sync 时,此时会往主队列中追加任务 2 。dispatch_sync 有个特点,要求立马在当前线程执行任务。

  • 从主队列方面看,按照队列先进先进先出的原则,主队列中的viewDidLoad 没执行完,任务 2 只能等待 viewDidLoad 执行完毕再去执行;
  • 从主线程方面来看,因为dispatch_sync要求立马在当前线程执行任务,任务 3 只能等待。

如此一来造成一个循环,任务3 要等同步线程中热任务2执行完才能执行,而任务 2 排在任务 3 后面需要等待任务 3 执行完 ,最终谁也无法执行完形成死锁。可另外参照下图加深理解。

案列二:

- (void)ViewDidLoad{
   NSLog(@"1");// 任务1
  dispatch_ync(dispatch_get_main_queue(),^{
      NSLog(@"2");// 任务2
  });
  NSLog(@"3");// 任务3
}

dispatch_sync 改为 dispatch_asyncdispatch_async 不会要求立马在当前线程执行任务,所以同案列一相比,不存在任务 3 等待 dispatch_async 的情况,所以这里不会产生死锁。代码执行顺序为任务1、任务3、任务2。

案列三:

- (void)ViewDidLoad{
  dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_async(globalQueue, ^{ // 0
        [self test];
    });
 }
- (void)test{
  NSLog(@"1");// 任务1
  dispatch_sync(dispatch_get_main_queue(),^{
      NSLog(@"2");// 任务2
  });
  NSLog(@"3");// 任务3
}

同案列一相比,上述代码放到子线程线程中执行,而不是放在 viewDidLoad(主线程) 中执行。任务1、dispatch_sync、任务1当前所处队列为 全局并发队列,而 dispatch_sync 是向 主队列添加任务,所以不会产生死锁。

案列四:

- (void)test{
    //手动创建的串行队列
  dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ 
        NSLog(@"执行任务1");
        //往当前串行队列添加任务
        dispatch_sync(queue, ^{ 
            NSLog(@"执行任务2");
        });
        NSLog(@"执行任务3");
    });
}

无论 test 方法在任何地方调用都会产生死锁,因为上述方法已经满足dispatch_sync当前串行队列添加任务的条件,这里的当前串行队列指手动创建的串行队列 queue,同案列一相比,这里只是将主队列替换为手动创建的串行队列。

案列五:

- (void)test{
    //手动创建的串行队列1
  dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
//手动创建的串行队列2
dispatch_queue_t queue2 = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ 
        NSLog(@"执行任务1");
        //往当前串行队列添加任务
        dispatch_sync(queue2, ^{ 
            NSLog(@"执行任务2");
        });
        NSLog(@"执行任务3");
    });
}

此种情况不会产生死锁,因为这里有两个串行队列,任务1、dispatch_sync、任务1当前所处队列为 queue1 串行队列,而 dispatch_sync 是向 queue2 串行队列添加任务。此种情况同案列三类似。

五十五、如何理解代理和协议?

实际面试过程中有问到应试者对协议和代理的理解,个别应试者只知道代理和协议的用法,连协议和代理的意义都说不清楚。举个简单的例子:一位导演很忙,因为他要把主要精力放到电影创作上。因此需要找代理人把重要的琐事分担出去,或者说把重要的琐事让”代理人”去做。其中的代理人就是代码中代理,协议主要是规定了代理人要做的事。 协议的用处还有很多,可看看此篇文章

五十六、MVP && MMVM

MVP

MVP 同 MVC 相比,本质上是将 Controller 的职责给分离出去,按照功能和业务逻辑划分为若干个 Presenter。Controller 中引入 Presenter ,Presenter 中同样也引入 Controller,Presenter 中处理各种业务逻辑,必要的时候再通过代理或 block 等形式回传到 Controller 中。要注意,为了避免循环引用 Presenter 要弱引用 Controller。

@interface ViewController ()
@property (strong, nonatomic) Presenter *presenter;
@end
@interface Presenter()
@property (weak, nonatomic) UIViewController *controller;
@end
MVVM

MVVM 总的来说和 MVP 非常类似,唯一不同点在于 View 和 ViewModel 双向绑定。实际开发通常是 Controller 中引入 ViewModel, ViewModel 中引入 Model,ViewModel 中会进行网络请求并进行数据处理逻辑。View 中会引入 ViewModel 给 View 设置内容,并且 View 还会监听 ViewModel 的变化,当 ViewModel 数据变化时,通过监听更新 View 上对应内容,实现双向绑定。因为 UI 的操作事件中可以动态改变模型,但是模型的改变不是很直接的体现到界面上,所以通常需要在 View 中监听 ViewModel 的变化。这种监听也可以通过监听实现,可以通过 RAC 实现,但是 RAC 过重,有一定的学习和维护成本。建议使用 KVOController 实现这种监听,如下一段代码是 View 中引入 ViewModel ,重写 ViewModel 的 set 方法,并监听 ViewModel 的变化刷新 UI 。笔者认为没有绝对好的架构模式,适合特定业务场景的架构模式才是好的架构。MVVM 特别适合那种模型和视图双向反馈较多的场景,比如列表页面的选中和非选中状态,通过改变 ViewModel 很轻松就能实现数据和界面的统一。 但是对于一般的业务场景而言(双向反馈较少的场景),MVVM 同 MVC 相比处理能拆分 Controller 的业务逻辑之外,貌似也没太多的优点,反而会增加调试的难度。假设出现一些 bug ,该 bug 可能源于视图也可能源于 ViewModel,会增加 bug 定位的难度。

- (void)setViewModel:(ViewModel *)viewModel{
    _viewModel = viewModel;
    __weak typeof(self) waekSelf = self;
    [self.KVOController observe:viewModel keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        waekSelf.nameLabel.text = change[NSKeyValueChangeNewKey];
    }];
}

五十七、简单工厂和工厂模式

简单工厂和工厂模式都属于类创建型模式。

简单工厂模式

简单工厂主要有三个部分组成:

  • 抽象产品:抽象产品是工厂所创建的所有产品对象的父类,负责声明所有产品实例所共有的公共接口。
  • 具体产品:具体产品是工厂所创建的所有产品对象类,它以自己的方式来实现其共同父类声明的接口。
  • 工厂类:实现创建所有产品实例的逻辑。
//抽象产品
//Operate.h文件
@interface Operate : NSObject
@property(nonatomic,assign)CGFloat numOne;
@property(nonatomic,assign)CGFloat numTwo;
- (CGFloat)getResult;
@end
//Operate.m文件
@implementation Operate
- (CGFloat)getResult{
    return 0.0;
}
@end
//具体产品1
//OperateAdd.m文件
@implementation OperateAdd
- (CGFloat)getResult{
    return self.numOne + self.numTwo;
}
@end
//具体产品2
//OperateSub.m文件
@implementation OperateSub
- (CGFloat)getResult{
    return self.numOne - self.numTwo;
}
@end

//工厂类
//OperateFactory.h文件
@class Operate;
@interface OperateFactory : NSObject
+ (Operate *)createOperateWithStr:(NSString *)str;
@end

//OperateFactory.m文件
@implementation OperateFactory
+ (Operate *)createOperateWithStr:(NSString *)str{
    if ([str isEqualToString:@"+"]) {
        OperateAdd *operateAdd = [[OperateAdd alloc] init];
        return operateAdd;
    }else if ([str isEqualToString:@"-"]){
        OperateSub *operateSub = [[OperateSub alloc] init];
        return operateSub;
    }else{
        return [[Operate alloc]init];
    }
}
@end
//使用
- (void)simpleFactoryTest{
    Operate *operate = [OperateFactory createOperateWithStr:@"+"];
    operate.numOne = 1;
    operate.numTwo = 2;
    NSLog(@"%f",[operate getResult]);
}

优点:最大的优点在于工厂类中包含了必要的判断逻辑,根据客户端的选择条件动态实例化相关的类,对于客户端而言去除了与具体产品的依赖。有了简单工厂类后,客户端在使用的时候只需要传入“+” 或“-”即可,使用上相对来说简单了很多。
缺点: 试想此时如果想在上述例子的基础上增加乘法或除法操作,除了增加相应的子类之外,开发人员还需要在工厂类中改写 if else 分支,至少要更改两处地方。显然,工厂类的改动违背了开放-封闭原则(对扩展是开放的,对更改是封闭的)。正因如此,才出现了所谓的工厂模式,工厂模式仅仅需要添加新的具体产品和新的具体工厂就能实现,原有代码无需改动。

笔者在实际开发过程中使用过简单工厂模式,具体说来:UICollectionView上有很多可动态配置的模块,本地代码提前写好不同的模块,然后根据后端接口返回的数据所包含的不同模块标志,用工厂类动态创建不同的模块,从而实现模块的动态配置。每个模块实际是一个 UICollectionViewCell ,它们统一继承一个基类,基类中包含一个统一渲染的方法,由于各个不同模块的基本参数配置一直,所以比较适合走统一抽象渲染接口。另外,类簇是简单工厂的应用如:NSNumber 的工厂方法传入不同类型的数据,则会返回不同数据所对应的 NSNumber 的子类。

工厂模式

工厂模式主要由四部分组成。

  • 抽象产品:同简单工厂。
  • 具体产品:同简单工厂。
  • 抽象工厂:声明具体工厂的创建产品的接口。
  • 具体工厂:负责创建特定的产品,每一个具体产品对应一个具体工厂。

上述三个抽象产品和具体产品类无变化,即 Operate、OperateAdd 和 OperateSub 三个类无变化。

//抽象工厂
//OperationFactoryProtocol协议
@class Operate;
@protocol OperationFactoryProtocol <NSObject>
+ (Operate *)createOperate;
@end
//具体工厂1
//AddFactory.h文件
@interface AddFactory : NSObject<OperationFactoryProtocol>
@end
//AddFactory.m文件
@implementation AddFactory
+ (Operate *)createOperate{
    return [[OperateAdd alloc]init];
}
@end
//具体工厂2
//SubFactory.h文件
@interface SubFactory : NSObject<OperationFactoryProtocol>
@end
//SubFactory.m文件
@implementation SubFactory
+ (Operate *)createOperate{
    return [[OperateSub alloc]init];
}
@end

优点

  • 工厂模式相比简单工厂而言,在扩展新的具体产品时候代码改动更小。
  • 用户只需要关心其所需产品对应的具体工厂是哪一个即可,不需要关心产品的创建细节,也不需要知道具体产品类的类名。
    缺点
  • 当系统中加入新产品时,除了需要提供新的产品类之外,还要提供与其对应的具体工厂类。随着类的个数增加,系统复杂度也会有所增加。
  • 简单工厂类只有一个工厂类,该工厂类可以创建多个对象;工厂模式中每个子类对应一个工厂类,每个工厂仅能创建一个对象。

五十八、适配器模式概念及应用

适配器设计模式数据接口适配相关设计模式。实际开发中有个场景特别使用适配器设计模式,一个封装好的视图组件可能在工程中不同的地方使用到,但是不同的地方使用的数据模型并不相同,此时可以借助对象适配器,创建新的适配器模型数据,而不应该在组件内部引入不同的数据模型,依据类型值进行判断,使用不同模型的不同数据。如电商网站中的加减按钮可能在不同的页面中使用到,但不同页面依赖的数据模型不同,此种情况就特别适合使用适配器模式。
两个模型类。

@interface DataModel : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *phoneNumber;
@property (nonatomic, strong)UIColor *lineColor;
@end

@interface NewDataModel : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *phoneNumber;
@end

适配器协议。

@protocol BusinessCardAdapterProtcol <NSObject>
- (NSString *)name;
- (NSString *)phoneNumber;
@end

适配器类。

//.h 文件
@interface ModelAdapter : NSObject<BusinessCardAdapterProtcol>
@property (nonatomic, weak)id data;
- (instancetype)initWithData:(id)data;
@end
//.m 文件
- (instancetype)initWithData:(id)data{
    self = [super init];
    if (self) {
        self.data = data;
    }
    return self;
}
//根据类名适配
- (NSString *)name{
    NSString *name = nil;
    if ([self.data isMemberOfClass:[DataModel class]]) {
         DataModel *data = self.data;
         name = data.name;
    }else if ([self.data isMemberOfClass:[NewDataModel class]]){
        NewDataModel *data = self.data;
        name = data.name;
    }
    return name;
}
- (NSString *)phoneNumber{
    NSString *phoneNumber = nil;
    if ([self.data isMemberOfClass:[DataModel class]]) {
        DataModel *data = self.data;
        phoneNumber = data.phoneNumber;
    }else if ([self.data isMemberOfClass:[NewDataModel class]]){
        NewDataModel *data = self.data;
        phoneNumber = data.phoneNumber;
    }
    return phoneNumber;
}

视图。

//.h 文件
@interface BusinessCardView : UIView
- (void)loadData:(id<BusinessCardAdapterProtcol>)data;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *phoneNumber;
@end

//.m 文件
- (void)loadData:(id<BusinessCardAdapterProtcol>)data{
    self.name = [data name];
    self.phoneNumber = [data phoneNumber];
}
- (void)setName:(NSString *)name{
    _name = name;
    _nameLabel.text = name;
}
- (void)setPhoneNumber:(NSString *)phoneNumber{
    _phoneNumber = phoneNumber;
    _phoneNumberLabel.text = phoneNumber;
}

使用。

- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建UI控件
    cardView = [[BusinessCardView alloc] initWithFrame:CGRectMake(0, 0, 375, 667.5)];
    cardView.center = self.view.center;
    [self.view addSubview:cardView];
    // 初始化两种不同d类型的模型
    model = [[DataModel alloc] init];
    model.name = @"测试一";
    model.phoneNumber = @"电话1";

    newmodel = [[NewDataModel alloc]init];
    newmodel.name = @"测试二";
    newmodel.phoneNumber = @"电话2";
    //设置初始数据
    BusinessCardAdapter *adapter = [[BusinessCardAdapter alloc] initWithData:model];
    [cardView loadData:adapter];
    UISwitch *btn = [[UISwitch alloc]initWithFrame:CGRectMake(50, 340, 50, 20)];
    [btn addTarget:self action:@selector(change:) forControlEvents:UIControlEventValueChanged];
    [self.view addSubview:btn];
}
- (void)change:(UISwitch *)btn{
    //切换数据
    ModelAdapter *adapter;
    if (btn.on == YES) {
       adapter = [[ModelAdapter alloc] initWithData:newmodel];
    }else{
       adapter = [[ModelAdapter alloc] initWithData:model];
    }
    //cardView与适配器连接
    [cardView loadData:adapter];
}

五十九、外观模式概念及应用

外观模式相对比较好理解,主要为子系统中的一组接口提供一个统一的接口。外观模式定义了一个更高层次的接口,这个接口使得这一子系统更加容易使用。以下情况下可以考虑使用外观模式:

  • 设计初期阶段,应该有意识的将不同层分离,层与层之间建立外观模式。
  • 开发阶段,子系统越来越复杂,增加外观模式提供一个简单的调用接口。
  • 维护一个大型遗留系统的时候,可能这个系统已经非常难以维护和扩展,但又包含非常重要的功能,为其开发一个外观类,以便新系统与其交互。

说的再直白一些,外观模式就相当于在客户端和子系统中间加了一个中间层。使用外观模式可以使项目更好的分层,增强了代码的扩展性。另外,客户端屏蔽了子系统组件,使客户端和子系统之间实现了松耦合关系。即使将后来想替换子系统客户端也无需改动。

六十、策略模式概念及应用

策略模式由三部分组成:抽象策略、具体策略以及引入策略的主体。实际开发中有一种场景特别适合使用策略模式,输入框 UITextField 的输入规则可以使用该设计模式,判断是输入电话号码、邮箱等格式是否正确。
抽象策略:

//.h 文件
@interface InputValidator : NSObject
@property (strong, nonatomic)NSString *errorMessage;
- (BOOL)validateInput:(UITextField *)input;
@end
//.m 文件
@implementation InputValidator
- (BOOL)validateInput:(UITextField *)input {
    return NO;
}
@end

两个具体策略:

//邮箱策略
@implementation EmailValidator
- (BOOL)validateInput:(UITextField *)input {
    if (input.text.length <= 0) {
        self.errorMessage = @"没有输入";
    } else {
        BOOL isMatch = [input.text isEqualToString:@"1214729173@qq.com"];
        if (isMatch == NO) {
            self.errorMessage = @"请输入正确的邮箱";
        } else {
            self.errorMessage = nil;
        }
    }
    return self.errorMessage == nil ? YES : NO;
}
@end

//电话号码策略
@implementation PhoneNumberValidator
- (BOOL)validateInput:(UITextField *)input {
    if (input.text.length <= 0) {
        self.errorMessage = @"没有输入";
    } else {
        BOOL isMatch = [input.text isEqualToString:@"15201488116"];
        if (isMatch == NO) {
            self.errorMessage = @"请输入正确的手机号码";
        } else {
            self.errorMessage = nil;
        }
    }
    return self.errorMessage == nil ? YES : NO;
}
@end

引入策略的主体:

//.h 文件
@interface CustomTextField : UITextField
//抽象的策略
@property (strong, nonatomic) InputValidator *validator;
//初始化
- (instancetype)initWithFrame:(CGRect)frame;
//验证输入合法性
- (BOOL)validate;
@end

//.m 文件
@implementation CustomTextField
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}
- (void)setup {
    UIView *leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 5, self.frame.size.height)];
    self.leftView = leftView;
    self.leftViewMode = UITextFieldViewModeAlways;
    self.font = [UIFont fontWithName:@"Avenir-Book" size:12.f];
    self.layer.borderWidth = 0.5f;
}
- (BOOL)validate {
    return [self.validator validateInput:self];
}
@end

外部使用:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self initButton];
    [self initCustomTextFields];
}
- (void)initCustomTextFields {
    self.emailTextField = [[CustomTextField alloc] initWithFrame:CGRectMake(30, 80, Width - 60, 30)];
    self.emailTextField.placeholder = @"请输入邮箱";
    self.emailTextField.delegate = self;
    self.emailTextField.validator = [EmailValidator new];
    [self.view addSubview:self.emailTextField];
    
    self.phoneNumberTextField = [[CustomTextField alloc] initWithFrame:CGRectMake(30, 80 + 40, Width - 60, 30)];
    self.phoneNumberTextField.placeholder = @"请输入电话号码";
    self.phoneNumberTextField.delegate = self;
    self.phoneNumberTextField.validator = [PhoneNumberValidator new];
    [self.view addSubview:self.phoneNumberTextField];
}
#pragma mark - 文本框代理
- (void)textFieldDidEndEditing:(UITextField *)textField {
    CustomTextField *customTextField = (CustomTextField *)textField;
    if ([customTextField validate] == NO) {
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:customTextField.validator.errorMessage preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *alertAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        }];
        [alertController addAction:alertAction];
        [self presentViewController:alertController animated:YES completion:nil];
    }
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.view endEditing:YES];
}

六十一、界面卡顿原因

屏幕成像的过程如下图:



按照60FPS的刷帧率,每隔16ms就会有一次 VSync 到来(垂直同步信号)。VSync 到来意味着要将 GPU 渲染好的数据拿出来显示到屏幕上,但是下图中红色区域中,由于CPU + GPU 的处理时间在 VSync 之后,所以此时红色框右边的时间段显示的始终是上一帧的画面,因此出现卡顿现象。所以实际开发中无论是 CPU 还是 GPU 消耗资源较多都可能造成卡顿现象。


六十二、[UIApplication sharedApplication].delegate.window&& [UIApplication sharedApplication].keyWindow的区别

参考此篇文章,实际开发中要格外留意 [UIApplication sharedApplication].keyWindow 的坑。

六十三、单例注意事项

创建单例的时候除了要考虑对象的唯一性和线程安全之外,还要考虑alloc initcopymutableCopy 方法返回同一个实例对象。关于allocWithZone可看此篇文章

+ (instancetype)sharedInstance {
    return [[self alloc] init];
}
- (instancetype)init {
    if (self = [super init]) {
        
    }
    return self;
}
//当执行 `alloc` 的时候,系统会自动调用分配内存地址的方法`allocWithZone:`。
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    static LogManager * _sharedInstanc = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedInstanc = [super allocWithZone:zone];//最先执行,只执行了一次
    });
    return _sharedInstanc;
}
-(id)copyWithZone:(struct _NSZone *)zone{
    return [LogManager sharedInstance];
}
-(id)mutableCopyWithZone:(NSZone *)zone{
    return [LogManager sharedInstance];
}

六十四、性能优化总结

待更新。。。。。

六十五、内存区域

  • 1、栈:局部变量(基本数据类型、指针变量)作用域执行完毕之后,就会被系统立即收回,无需程序员管理(分配地址由高到低分配)。
  • 2、堆:程序运行的过程中动态分配的存储空间(创建的对象),需要主动申请和释放。
  • 3、BSS 段:没有初始化的全局变量和静态变量,一旦初始化就会从 BSS 段中收回掉,转存到数据段中。
  • 4、(全局区)数据段:存放已经初始化的全局变量和静态变量,以及常量数据,直到程序结束才会被立即收回。
  • 5、代码段:程序编译后的代码内容,直到结束程序才会被收回。

六十六、符号表

iOS 构建时产生的符号表,是内存地址、函数名、文件名和行号的映射表。格式大概是:

<起始地址> <结束地址> <函数> [<文件名:行号>]

Crash 时的堆栈信息,全是二进制的地址信息。如果利用这些二进制的地址信息来定位问题是不可能的,因此我们需要将这些二进制的地址信息还原成源代码种的函数以及行号,这时候符号表就起作用了。利用符号表将原始的 Crash 的二进制堆栈信息还原成包含行号的源代码文件信息,可以快速定位问题。iOS 中的符号表文件(DSYM) 是在编译源代码后,处理完 Asset Catalog 资源和 info.plist 文件后开始生成,生成符号表文件(DSYM)之后,再进行后续的链接、打包、签名、校验等步骤。

六十七、指针和引用

在 C 和 OC 语言中,使用指针(Pointer)可以间接获取、修改某个变量的值,C++中,使用引用(Reference)可以起到跟指针类似的功能。引用相当于是变量的别名,对引用做计算,就是对引用所指向的变量做计算,在定义的时候就必须初始化,一旦指向了某个变量,就不可以再改变从一而终。所以这也是存在的价值之一:比指针更安全、函数返回值可以被赋值。引用的本质就是指针,只是编译器削弱了它的功能,所以引用就是弱化了的指针。

六十八、static & const & extern


  • static修饰局部变量:让局部变量永远只初始化一次。将局部变量的本来分配在栈区改为分配在静态存储区,静态存储区伴随着整个应用,也就延长了局部变量的生命周期。
  • static修饰全局变量:本来是在整个源程序的所有文件都可见,static修饰后,改为只在申明自己的文件可见,即修改了作用域。

  • const:修饰变量主要强调变量是不可修改的。const 修饰的是其右边的值,也就是 const 右边的这个整体的值不能改变。
//如下代码无法编译通过
//const修饰str指针,所以str指针的内存地址无法改变,也即str指针不能改变内存地址指向。
 NSString * const str = @"test";
 //该行代码表示:str指针指向了其它的内存
 str = @"123";

//const修饰 *str,也即str指针指向的内存地址,所以对修改str指针的指向无任何影响。
NSString const *str = @"test";
//该行代码表示:str指针指向了其它的内存
 str = @"123";

一般联合使用static和const来定义一个只能在本文件中使用的,不能修改的变量。相对于用#define来定义的话,优点就在于它指定了变量的类型。

//防止 reuseIdentifier 指针指向其它内存
static NSString * const reuseIdentifier = @"reuseIdentifier";

  • extern:主要是用来引用全局变量,先在本文件中查找,本文件中查找不到再到其他文件中查找。常把 extern 和 const 联合使用在项目中创建一个文件,这个文件中包含整个项目中都能访问的全局常量。

六十九、枚举

枚举的目的只是为了增加代码的可读性。iOS6 中引入了两个宏来重新定义枚举类型 NS_ENUM 与 NS_OPTIONS ,两者在本质上并没有差别,NS_ENUM多用于一般枚举, NS_OPTIONS 则多用于带有移位运算的枚举。

NS_ENUM
typedef NS_ENUM(NSInteger, Test){
    TestA = 0,
    TestB,
    TestC,
    TestD
};
NS_OPTIONS
typedef NS_OPTIONS(NSUInteger, Test) {
    TestA = 1 << 0,
    TestB = 1 << 1,
    TestC = 1 << 2,
    TestD = 1 << 3
};

使用按位或(|)为枚举 变量test 同时赋值枚举成员TestATestBTestC

Test test = TestA | TestB;
test |= TestC;

使用按位异或(^)为枚举 变量 test 去掉一个枚举成员 TestC。ps: 两者相等为0,不等为1。

Test test = TestA | TestB | TestC;
test ^= TestC;

使用按位与(&)判断枚举 变量test 是否赋值了枚举成员 TestA

Test test = TestA | TestB;
if (test & TestA){
    NSLog(@"yes");
}else{
    NSLog(@"no");
}

七十、验证码的作用

待更新。。。。

七十一、帧率优化

Color Blended Layers(red)

png 图片是支持透明的,对系统性能也会有影响的。最好不要设置透明度,因为透明的图层和其他图层重叠在一块的部分,CPU 会做处理图层叠加颜色计算,这种处理是比较消耗资源的。

Color Copied Images (cyan)

苹果的 GPU 只解析 32bit 的颜色格式。
如果一张图片,颜色格式不是 32bit ,CPU 会先进行颜色格式转换,再让 GPU 渲染。 就算异步转换颜色,也会导致性能损耗,比如电量增多、发热等等。解决办法是让设计师提供 32bit 颜色格式的图片。图片颜色科普文章:图片的颜色深度/颜色格式(32bit,24bit,12bit)

Color Misaligned Images 像素对齐(yellow)

iOS设备上,有逻辑像素(point)和 物理像素(pixel)之分,像素对齐指的是物理像素对齐,对齐就是像素点的值是整数。UI 设计师提供的设计稿标注以及中的 frame 是 逻辑像素。GPU在渲染图形之前,系统会将逻辑像素换算成 物理像素。point 和 pixel 的比例是通过[[UIScreen mainScreen] scale] 来制定的。在没有视网膜屏之前,1point = 1pixel;但是2x和3x的视网膜屏出来之后,1point = 2pixel 或 3pixel

逻辑像素乘以 2 或 3 得到整数值就像素对齐了,反之则像素不对齐。像素不对齐会导致 GPU 渲染时,对没对齐的边缘进行插值计算插值计算会有性能损耗。

原图片大小和视图控件大小不一致,图片为了对应在控件的相应的位置就需要做一些计算,然后确定图片的位置,该种情况也比较消耗资源。一般可以通过绘制指定尺寸大小、不透明的图片来优化性能。

Color Off-screen Rendered (yellow)

cornerRadius 属性只应用于 layer 的背景色和边线。将 masksToBounds 属性设置为 YES 才能把内容按圆角形状裁剪。同时设置 cornerRadiusmasksToBounds = YES ,并且屏幕中同时显示的圆角个数过多,就会明显感觉到卡顿和跳帧,只是设置 cornerRadius 并不会触发此种现象。当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。使用离屏渲染的时候会很容易造成性能消耗,因为在 OpenGL 里离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。iOS9 之后系统设置圆角不再产生离屏渲染。设置 shadow***相关阴影属性也会产生离屏渲染,解决方法是设置阴影路径 shadowPath

无法避免离屏渲染的时候可尝试使用光栅化来进一步做优化。光栅化是指将图转化为一个个栅格组成的图象。shouldRasterize = YES 在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer 及其 sublayers 没有发生改变,在下一帧的时候可以直接复用,从而减少渲染的频率。当使用光栅化时,可以在 Core Animation 开启 Color Hits Green and Misses Red 来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处,反之就可以开启。

七十二、内存数据擦除

敏感数据不想一直保留在内存中,可以通过特定的 API 擦除内存中的数据,比如 NSString:

@implementation NSString (MemoryClear)
/**
 内存数据及时擦除
 */
-(void)memoryClearStirng{
    const char*string = (char *)CFStringGetCStringPtr((CFStringRef)self,CFStringGetSystemEncoding());
    memset(&string, 0, sizeof(self));
}
@end

七十三、找不到方法怎么办

交叉替换消息转发机制的 forwardingTargetForSelector: 方法。

@implementation NSObject (Exception)

+ (void)load {
    //防止外部手动调用load方法,固load方法中最好都要写上dispatch_once
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        @autoreleasepool {
           //交叉方法
           [objc_getClass("NSObject") swizzleMethod:@selector(forwardingTargetForSelector:) swizzledSelector:@selector(replace_forwardingTargetForSelector:)];
        }
    });
}
//替换后的消息转发阶段
- (id)replace_forwardingTargetForSelector:(SEL)aSelector{
    //先处理自身能处理的消息(这一块的逻辑和一般的交叉方法有点区别:一般的交叉方法先处理异常,再调用之前的方法,这里相反)
    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];
    if ([self respondsToSelector:aSelector] || signature) {
        return [self replace_forwardingTargetForSelector:aSelector];
    }
    //返回其他消息处理对象,并在内部动态添加方法
    FakeForwardTargetObject *fakeTaget = [[FakeForwardTargetObject alloc] initWithSelector:aSelector];
    return fakeTaget;
}
@end

FakeForwardTargetObject 类。

id fakeIMP(id sender,SEL sel,...){
    return nil;
}
@interface FakeForwardTargetObject : NSObject
- (instancetype)initWithSelector:(SEL)aSelector;
@end

@implementation FakeForwardTargetObject
- (instancetype)initWithSelector:(SEL)aSelector{
    if (self = [super init]) {
        if(class_addMethod([self class], aSelector, (IMP)fakeIMP, NULL)) {
            MCLog(@"add Fake Selector:[instance %@]", NSStringFromSelector(aSelector));
            NSString *string = [NSString stringWithFormat:@"[%s:%d行]",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__];
             showExceptionAlert(string);
        }
    }
    return self;
}
@end

七十四、卡顿代码监测原理

所谓的卡顿一般是在主线程做了耗时操作,卡顿监测的主要原理是在主线程的 RunLoop 中添加一个 observer,检测从 即将处理Source(kCFRunLoopBeforeSources)即将进入休眠 (kCFRunLoopBeforeWaiting) 花费的时间是否过长。如果花费的时间大于某一个阙值,则认为卡顿,此时可以输出对应的堆栈调用信息。具体可以参考此篇文章

七十五、同时实现 set & get

setget 方法单独重写任意一个方法都不会报错,但是同时重写会报错。主要是因为重写 getset 方法之后 @property 默认生成的 @synthesize 就不起作用,也就意味着对应的类不会自动生成成员变量,解决方案是手动添加成员变量。

七十六、main 中的 UIApplicationMain 函数

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

UIApplicationMain 函数主要有以下两个作用:

  • 创建一个应用程序以及创建应用程序代理
  • 建立一个事件循环来捕捉处理用户的行为

UIApplicationMain 函数参数说明:

  • 1、参数 argcargv 是 C 标准的 main 函数的参数。其中, argc 表示参数个数;argv 表示参数指针,是指向指针的指针,也可以替换为 char **argv
  • 2、principalClassName 是应用程序对象所属的类,该类必须继承自 UIApplication 类。如果所属类字符串的值为 nil, UIKit 就缺省使用UIApplication 类。
  • 3、delegateClassName 是应用程序类的代理类,该函数跟据 delegateClassName 创建一个 delegate对象,并将 UIApplication 对象中的 delegate 属性设置为 delegate 对象。

七十七、nil、Nil、NULL、NSNull

  • object = nil 表示把这个对象释放掉,称为“空对象”。对于这种空对象,所有关于 retain 的操作都会引起程序崩溃,例如字典添加键值或数组添加新原素等。
  • NSNullnil 的区别在于,nil 是一个空对象,已经完全从内存中消失了,而如果想表达“我们需要有这样一个容器,但这个容器里什么也没有”的观念时,就用到NSNull,称之为值为空的对象NSNull 继承自 NSObject,并且只有一个 null 类方法。这就说明 NSNull 对象拥有一个有效的内存地址,所以在程序中对它的引用不会导致程序崩溃。
  • nilNil 在使用上是没有严格限定的,也就是说凡是使用 nil 的地方都可以用 Nil 来代替,反之亦然。
  • NULL就是典型 C 语言的语法,它表示一个空指针。如:int *ponit = NULL

七十八、iOS 系统结构

待更新。。。。

七十九、钥匙串访问

UUID 保存到 KeyChain 里面,即使APP删了再装回来,也可以从KeyChain中读取回来,常第三方库SSKeychain。使用keyChain Sharing还可以保证同一个开发商的所有程序针对同一台设备能够获取到相同的不变的UDID。但是刷机或重装系统后 UUID 还是会改变。假如项目 2 想使用项目 1 的 Keychain ,项目 2 要开启Keychain Sharing 且 Keychain Groups 要包含项目 1。

八十、动态规划思路

动态规划算法一般有两种求解方式:1、自顶向下的备忘录法 2、自底向上。比如斐波拉契数列(Fibonacci)问题中,使用动态规划的思路解决问题,可以避免类似递归解决方案的重复计算问题。保留计算结果,避免重复计算这也是动态规划和分治策略的最大区别所在。

八十一、主线程更新 UI 原因

UI 必须放在主线程是因为 UIKit 为了提升性能,压根没加锁,所以 UIKit 不是线程安全的。如果不在主线程里面操作,会出现什么样的 UI ,谁也不敢保证。

八十二、设计原则

待更新。。。。

八十三、@selector、_cmd、SEL

@selector 是指向实际执行的函数指针(function pointer)的一个C字符串。
_cmd 实际上等价于 @selector(getter),两者都是 SEL类型,都是方法选择器,用于在类结构的方法分发表中搜索指定名字的方法实现/地址。

objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)

// 隐式参数 _cmd == @selector(getter)
objc_getAssociatedObject(obj, _cmd)
objc_getAssociatedObject(obj, @selector(getter))

八十四、Clang & LLVM & GCC

LLVM 架构

LLVM 架构

LLVM 架构不同的前后端使用统一的中间代码 LLVM Intermediate Representation (LLVM IR)。如上图,如果需要支持一种新的编程语言(如C、Fortran、Haskell),只需要实现一个新的编译器前端;如果需要支持支持一种新的硬件设备(如X86、PowerPC、ARM),只需要实现一个新的编译器后端即可。而传统的变异架构因为没有LLVM IR,如果需要支持C、Fortran、Haskell三种语言和X86、PowerPC、ARM三种硬件,则需要九个编译器(3*3 = 9,不区分前后端),而在 LLVM 架构中之需要支持三种编译器前端和三种编译器后端即可。

Clang

Clang 是 LLVM中的一个子项目,是基于 LLVM 架构的C/C++/Objective-C编译器前端。

LLVM && Clang

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,可以处理C、C++、OC等。在理解LLVM时,可以认为它包括了一个狭义的LLVM和一个广义的LLVM。

  • 广义的LLVM其实就是指整个LLVM编译器架构,包括了前端(Clang)、后端(LLVM)、优化器、众多的库函数以及很多的模块;
  • 狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库。Clang 就是一个编译前端工具,而 LLVM 则负责后端处理。


    Clang & LLVM
Clang为前端,LLVM为后端

GCC

GCC 是另一个知名工具 GCC(GNU Compile Collection)则是一个套装,包揽了前后端的所有任务,可以处理C、C++。

Clang 相比于 GCC 优势:

  • Clang是一个高度模块化开发的轻量级编译器
  • 编译速度快
  • 占用内存小、已与扩展(非常方便进行二次开发)。

八十五、生命周期

面试过程中,突然有人问到应用生命周期,瞬间懵掉。平时开发很少涉及,面试之前也没看,自然不容易想起来。按照 App 操作,一般分为下面三种情况:

从非运行到前台活跃:

  • [AppDelegate application:didFinishLaunchingWithOptions:]
  • [AppDelegate applicationDidBecomeActive:]

前台活跃到退出:

  • [AppDelegate applicationWillResignActive:]
  • [AppDelegate applicationDidEnterBackground:]
  • [AppDelegate applicationWillTerminate:]

后台到前台:

  • [AppDelegate applicationWillEnterForeground:]
  • [AppDelegate applicationDidBecomeActive:]
顺便补充下控制器生命周期:
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 上消失

八十六、dealloc中取weakSelf

参考

八十七、assign 和 weak

如果以MRC和ARC进行区分修饰符使用情况,可以按照如下方式进行分组:

  • MRC:assign、retain、copy、readwrite、readonly、nonatomic、atomic 等。
  • ARC:assign、strong、weak、copy、readwrite、readonly、nonatomic、atomic 等。
    MRC 和 ARC 都可以用assign,但 weak 只存在于 ARC 中。assign 和weak 的主要区在于:当它们指向的对象释放以后,weak 会被自动设置为nil,而 assign 不会,所以会导致野指针的出现,可能会导致crash。实际面试过面试官可会问把 delegate 的属性修饰符 weak 改为 assign 会有什么影响。

八十八、#import & @class

#import#include 无需多说,二者功能基本相同,不过 #import 避免了重复引用的问题,在引用文件的时候不用自己进行重复引用处理。重点说一下 #import 和 @class 在实际开发中的注意注意事项(提高编译速度、避免编译错误)。

  • @class 只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑。但 #import 会链入该头文件的全部信息,包括实体变量和方法等,但是这样做会对编译效率造成影响。比如有100个类都 #import ClassA.h, 那么在编译的时候这100个类都会去对 ClassA 处理。如果 ClassA 被修改,那么这 100 个类都需要重新进行编译,无疑增加了编译时间不利于开发效率的提升。一般的做法是:头文件中一般只需要知道被引用的类的名称就可以了, 不需要知道其内部的实体变量和方法,所以在头文件中一般使用 @class 来声明这个名称是类的名称; 实现文件中,如果需要引用这个类的实体变量或者方法就用 #import,否则不做任何处理。这种做法可以避免掉一些不必要的 #import 操作,一定程度上可能提高编译效率。

  • 如果有循环依赖关系,如: A–>B , B–>A 这样的相互依赖关系,如果使用 #import 来相互包含,就会出现编译错误;如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。

八十九、IQKeyBoard 原理

待更新。。。。

九十、self.name = 和 name =

待更新。。。。

九十一、ipa 包构成

  • _CodeSignature:文件的 hash 列表。里面有一个文件 CodeResources ,它是一个属性列表,包含 bundle 中所有其他文件的列表。内部是一个字典,key 为文件名,value 是 Base64 格式的散列值。主要用于判断一个应用程序的完整性,可防止修改文件。
  • AppIcon 图片
  • LaunchImage 图片
  • bundle 资源文件
  • Assets.car 资源文件
  • info.plist
  • nib 文件
  • 相应程序的 Unix 代码可执行文件
  • embedded.mobileprovision 描述文件
  • 其它(工程中引入的 xml 文件、iconfont.ttf、gif 图等)

九十二、MRC和ARC的关系

自动引用计数管理中(ARC),内存的申请、使用和释放过程都交给系统自动实现,开发者不用关心里面的过程,事实上还是 MRC 的原理,只是系统帮我们做了管理。MRC 和 ARC 使用的编译器也不相同前者是 GCC 后者是 LLVM 3.0。

ARC 是 LLVM 编译器和 Runtime 相互协作的结果。ARC相对于MRC,不是在编译时添加retain/release/autorelease这么简单。应该是编译期和运行期两部分共同帮助开发者管理内存。

  • 编译时期:ARC 利用LLVM编译器帮助我们自动生成类似MRC 的 release、retain、autorelease 代码。
  • 运行时期:类似弱引用指向的对象释放问题和 Runtime 相关。程序运行过程中,监测到对象销毁的时候,把对象对应的弱引用清空。

九十三、Foundation 和 CoreFoundation

Foundation对象是Objective-C对象,而Core Foundation对象是C对象,二者比较相似, Foundation下的类基本都是NS开头,Core Foundation下的类基本是CF开头的。

这里说一下两者在iOS中的内存管理问题,以前在MRC情况下,都是开发人员手动管理对象内存,二者区别不大。但在ARC情况下,由于Foundation框架是OC对象,所以由系统自动管理内存,而Core Foundation框架是C对象,所以需要开发人员手动管理内存,不然会引起内存泄露。在ARC下,可以对两个框架的类进行相互转换,以NSString为例,有与之对应的CFStringRef,两者之间可以通过__bridge、__bridge_transfer、__bridge_retained转换:

  • __bridge:用于NSString与CFStringRef相互转换,不改变对象的管理权所有者,按照本来对象的内存管理。本来是NSString,转换为CFStringRef类型,依旧由系统管理;本来是CFStringRef,转换为NSString,由开发人员管理。
  • __bridge_transfer:用于CFStringRef转换成NSString,进行管理权移交,由系统自动管理。
  • __bridge_retained:用于NSString转换成CFStringRef,剥夺了ARC管理权,需要开发人员手动管理。

九十四、简单对象 MRC

不借助 autorelease。

Person *person2 = [[Person alloc] init];
[person2 release];//直接释放

借助 autorelease(实际 MRC 开发)。

Person *person1 = [[[Person alloc] init] autorelease];//适当时机自动释放

类似 [NSArray arrayWithObject:@""] 内部一般是有 autorelease 操作。

九十五、成员为对象 MRC 管理

不借助 autorelease 。

//.h文件
@interface Person : NSObject
{
   Dog *_dog;
}
- (void)setDog:(Dog *)dog;
- (Dog *)dog;


//.m文件
//person拥有dog,非autorelease技术,先release之前的再持有现在的
- (void)setDog:(Dog *)dog{
    //如果中途换了dog,不是之前的dog,先释放之前的dog(对现在的dog没影响),再retain现在的dog
    if (_dog != dog) {
        [_dog release];
        //该处的retain 和 dealloc 中的 release 对应,也即引用计数谁+1,最终谁负责-1。
        _dog = [dog retain];
    }
    //如果dog是现在的dog:第一次调用setDog方法时,即dog为nil的时候,已经对dog进行retain操作了,之后就无需再retain,因为第一次的结果导致dog的引用计数大于0,肯定不会被释放,所以之后增加引用计数是无意义的
}
- (Dog *)dog{
    return _dog;
}

//先释放成员变量,在释放父类本身
- (void)dealloc{
    [_dog release];
    _dog = nil;
    //self.dog = nil;
    // 父类的dealloc放到最后
    [super dealloc];
}
Dog *dog = [[Dog alloc] init]; // dog:1
    
    Person *person = [[Person alloc] init];
    [person setDog:dog]; // dog:2
    [dog release]; // dog:1 (对应alloc)
    
    //因为是person操作,不能造成崩溃,所以setDog方法内部需要retain操作
    //如果中途换了dog不是之前的dog,要先释放之前的dog,再retain操作
    [person setDog:dog];// dog:1
    
    //set内部有判断,首次已经retain操作了,
    [person setDog:dog];// dog:1(引用计数不变)
    [person setDog:dog];// dog:1(set内部有判断,引用计数不变) 
    
    [person release]; // dog:0

借助 autorelease(实际 MRC 开发)。

//.h 文件
@interface Person : NSObject
//注意这里的retain属性修饰符
@property (nonatomic, retain) MJDog *dog;
+ (instancetype)person;
@end


//.m 文件
@implementation MJPerson
+ (instancetype)person{
    return [[[self alloc] init] autorelease];
}
- (void)dealloc{
    self.dog = nil;
    [super dealloc];
}
@end
//使用
 MJPerson *person = [MJPerson person];

九十六、UIScrollView 多页面侧滑返回手势冲突

相关参考
参考
参考
iOS 触控事件 UITouch 和手势识别 UIGestureRecognizer

#import "UIScrollView+PanGesture.h"
@implementation UIScrollView (PanGesture)
//location_X可自己定义,其代表的是滑动返回距左边的有效长度
- (BOOL)panBack:(UIGestureRecognizer *)gestureRecognizer {
    //是滑动返回距左边的有效长度
    int location_X =0.15*[MCDevice screenWidth];
    if (gestureRecognizer == self.panGestureRecognizer) {
        UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint point = [pan translationInView:self];
        UIGestureRecognizerState state = gestureRecognizer.state;
        if (UIGestureRecognizerStateBegan == state ||UIGestureRecognizerStatePossible == state) {
            CGPoint location = [gestureRecognizer locationInView:self];
            //允许每个页面都可实现滑动返回
            int temp1 = location.x;
            int temp2 =[MCDevice screenWidth];
            NSInteger XX = temp1 % temp2;
            if (point.x > 0 && XX < location_X) {
                return YES;
            }
            //只允许在第一张时滑动返回生效
            //            if (point.x > 0 && location.x < location_X && self.contentOffset.x <= 0) {
            //                return YES;
            //            }
        }
    }
    return NO;
}
/*
 开始进行手势识别时调用的方法,返回NO则结束识别,不再触发手势,用处:可以在控件指定的位置使用手势识别
 该功能中禁止scrollView 自己的侧滑返回手势,走导航控制器的侧滑返回手势
 */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    if ([self panBack:gestureRecognizer]) {
        return NO;
    }
    return YES;
}
@end

九十七、gestureRecognizer 和 UIControl 的关系

待更新。。。。

九十八、FDFullscreenPopGesture 原理

+ (void)load
{
    // Inject "-pushViewController:animated:"
    Method originalMethod = class_getInstanceMethod(self, @selector(pushViewController:animated:));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_pushViewController:animated:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
        
        // Add our own gesture recognizer to where the onboard screen edge pan gesture recognizer is attached to.
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];

        // Forward the gesture events to the private handler of the onboard gesture recognizer.
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];

        // Disable the onboard gesture recognizer.
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    
    // Handle perferred navigation bar appearance.
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
    
    // Forward to primary implementation.
    [self fd_pushViewController:viewController animated:animated];
}
  • load 方法交叉替换 NavigationController 的 pushViewController:animated: 方法。
    系统中每一个 NavigationController 默认有一个 interactivePopGestureRecognizer,但是这里把系统的 interactivePopGestureRecognizer 设置为禁用方式。创建自定义 pan 手势并添加到 interactivePopGestureRecognizer 对应的View上,interactivePopGestureRecognizer 会操作一个指定的 target 、action(handleNavigationTransition), 通过Runtime动态获取到指定的target 和 action添加到自定义的手势上。

九十九、判断模拟器和真机的坑

//正确的方法
#if TARGET_IPHONE_SIMULATOR  //模拟器

#elif TARGET_OS_IPHONE      //真机

#endif
//错误的方法
#if TARGET_OS_IPHONE      //真机

#endif

上述第二种方法无论是在真机还是模拟器环境,中间代码都会执行,是苹果的一个坑。

一百、内存泄漏检测原理 (待补充)

MLeaksFinder 为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间 (3 秒) 后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接弹框提醒该对象可能存在内存泄漏。当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果 3 秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会弹框提示泄漏;如果它没被释放(泄露了),-assertNotDealloc 就会被调用,具体是遍历基于 UIViewController 的整棵 View-ViewController 树,通过 UIViewController 的 presentedViewController 和 view 属性,UIView 的 subviews 属性等递归遍历,依次调 -willDealloc,若 3 秒后没被释放,则存在泄漏。可参考该篇文章;

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}
例外机制

对于有些 ViewController,在被 pop 或 dismiss 后,不会被释放(比如单例),因此需要提供机制让开发者指定哪个对象不会被释放,这里可以通过重载上面的 -willDealloc 方法,直接 return NO 即可。

手动扩展

MLeaksFinder目前只检测 ViewController 跟 View 对象。为此,MLeaksFinder 提供了一个手动扩展的机制,你可以从 UIViewController 跟 UIView 出发,去检测其它类型的对象的内存泄露。如下所示,我们可以检测 UIViewController 内的 View Model。宏 MLCheck() 做的事就是为传进来的对象建立 View-ViewController stack 信息,并对传进来的对象调用 -willDealloc 方法。

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    MLCheck(self.viewModel);
    return YES;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335