开发小知识(二)
开发小知识(一)
开发小知识(二)
目录
- 五十一、关联对象
- 五十二、TCP 面向连接的本质是什么?TCP 和 UDP 的区别?
- 五十三、高效安全读写方案
- 五十四、死锁
- 五十五、如何理解代理和协议?
- 五十六、MVP && MMVM
- 五十七、简单工厂和工厂模式
- 五十八、适配器模式概念及应用
- 五十九、外观模式概念及应用
- 六十、策略模式概念及应用
- 六十一、界面卡顿原因
- 六十二、[UIApplication sharedApplication].delegate.window&& [UIApplication sharedApplication].keyWindow的区别
- 六十三、单例注意事项
- 六十四、性能优化总结
- 六十五、内存区域
- 六十六、符号表
- 六十七、指针和引用
- 六十八、static & const & extern
- 六十九、枚举
- 七十、验证码的作用
- 七十一、帧率优化
- 七十二、内存数据擦除
- 七十三、内存泄露监测原理
- 七十四、卡顿代码监测原理
- 七十五、同时实现 set & get
- 七十六、main 中的 UIApplicationMain 函数
- 七十七、nil、Nil、NULL、NSNull
- 七十八、iOS 系统结构
五十一、关联对象
关联对象的 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];
});
}
另外提示,如果仅仅只是对写操作加锁,读操作不做任何处理,并不能保证线程安全,仅对写操作加锁仅仅只能保证不会同时出现两个或多个写操作,并不能避免同一时刻既有写操作又有读操作。实现正在进行读操作,此时来了第一个写操作,但是相关锁并没有加锁,所以读写操作可同时进行。
补充:对于线程安全方案,除了加锁之外,还可以借助串行队列确保代码执行的顺序,保证线程安全。
五十四、死锁
在说死锁之前要知道同步和异步主要决定是否具备开启新线程的能力。串行和并发主要决定任务执行的方式。
所谓死锁,通常指有两个线程 T1 和 T2 都卡住了,并等待对方完成某些操作。T1 不能完成是因为它在等待 T2 完成。T2 也不能完成,因为在等待 T1 完成。于是大家都完不成,就导致了死锁(DeadLock),就类似一条比较窄的马路有两辆车相向而行,互相等着对方过了之后再过。
- (void)ViewDidLoad{
NSLog(@"1");// 任务1
dispatch_sync(dispatch_get_main_queue(),^{
NSLog(@"2");// 任务2
});
NSLog(@"3");// 任务3
}
dispatch_sync
中主要传入两个参数,队列和任务回调block。上述代码仅会执行任务 1 。 主线程原本存在任务 1 、sync 、任务3,主队列中原本仅存在viewDidLoad
,当主线程从任务 1 依次执行到 sync 时,此时会往主队列中追加任务 2 。dispatch_sync
有个特点,要求立马在当前线程执行任务,执行完毕才能继续往下执行,但是此时主队列中的viewDidLoad
还没执行完,自然就不能将任务 2 从主队列取出放入主线程执行,意味着 sync 无法完成 ,进而意味着任务 3 无法执行,再进而意味着 viewDidLoad
无法执行完,viewDidLoad
无法执行完,也意味着无法从主队列中取出任务 2 。如此一来造成一个循环,任务3 要等同步线程中热任务2执行完才能执行,而任务 2 排在任务 3 后面需要等待任务 3 执行完 ,最终谁也无法执行完形成死锁。可参照下面两幅图加深理解。
上述只是死锁的一种形式,另一种情况是锁和锁之间产生的冲突。。。。。。。。。
五十五、如何理解代理和协议?
实际面试过程中有问到应试者对协议和代理的理解,个别应试者只知道代理和协议的用法,连协议和代理的意义都说不清楚。举个简单的例子:一位导演很忙,因为他要把主要精力放到电影创作上。因此需要找代理人把重要的琐事分担出去,或者说把重要的琐事让”代理人”去做。其中的代理人就是代码中代理,协议主要是规定了代理人要做的事。 协议的用处还有很多,可看看此篇文章。
五十六、MVP && MMVM
MVPMVP 同 MVC 相比,本质上是将 Controller 的职责给分离出去,按照功能和业务逻辑划分为若干个 Presenter。Controller 中引入 Presenter ,Presenter 中同样也引入 Controller,Presenter 中处理各种业务逻辑,必要的时候再通过代理或 block 等形式回传到 Controller 中。要注意,为了避免循环引用 Presenter 要弱引用 Controller。笔者认为 MPV 存在的一个明显缺点是
@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, 并照搬照抄一份 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 init
、 copy
和 mutableCopy
方法返回同一个实例对象。关于allocWithZone
可看此篇文章。
+ (instancetype)sharedInstance {
return [[self alloc] init];
}
- (instancetype)init {
if (self = [super init]) {
}
return self;
}
+ (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
同时赋值枚举成员TestA
、TestB
、TestC
。
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 才能把内容按圆角形状裁剪。同时设置 cornerRadius
和 masksToBounds = 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
七十三、内存泄露监测原理
待更新。。。。
七十四、卡顿代码监测原理
所谓的卡顿一般是在主线程做了耗时操作,卡顿监测的主要原理是在主线程的 RunLoop 中添加一个 observer,检测从 即将处理Source(kCFRunLoopBeforeSources)
到 即将进入休眠 (kCFRunLoopBeforeWaiting)
花费的时间是否过长。如果花费的时间大于某一个阙值,则认为卡顿,此时可以输出对应的堆栈调用信息。具体可以参考此篇文章。
七十五、同时实现 set & get
set
和 get
方法单独重写任意一个方法都不会报错,但是同时重写会报错。主要是因为重写 get
和 set
方法之后 @property
默认生成的 @synthesize
就不起作用,也就意味着对应的类不会自动生成成员变量,解决方案是手动添加成员变量。
七十六、main 中的 UIApplicationMain 函数
待更新。。。。
七十七、nil、Nil、NULL、NSNull
-
object = nil
表示把这个对象释放掉,称为“空对象”。对于这种空对象,所有关于retain
的操作都会引起程序崩溃,例如字典添加键值或数组添加新原素等。 -
NSNull
和nil
的区别在于,nil
是一个空对象,已经完全从内存中消失了,而如果想表达“我们需要有这样一个容器,但这个容器里什么也没有”的观念时,就用到NSNull
,称之为值为空的对象。NSNull
继承自NSObject
,并且只有一个null
类方法。这就说明NSNull
对象拥有一个有效的内存地址,所以在程序中对它的引用不会导致程序崩溃。 -
nil
和Nil
在使用上是没有严格限定的,也就是说凡是使用nil
的地方都可以用Nil
来代替,反之亦然。 -
NULL
就是典型 C 语言的语法,它表示一个空指针。如:int *ponit = NULL
七十八、iOS 系统结构
待更新。。。。