1、系统版本判断
#define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
#define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)
2、iOS 11后获取导航栏和底部高度的正确姿势
@interface UIWindow (TLCAdd)
/**
获取安全区域底部的高度
@return 安全区域底部的高度
*/
- (CGFloat)TLCBottomSpace;
/**
获取导航栏的高度
@return 导航栏的高度
*/
- (CGFloat)TLCNavigationBarHeight;
@end
#import "UIWindow+TLCAdd.h"
@implementation UIWindow (TLCAdd)
- (CGFloat)TLCBottomSpace {
if (@available(iOS 11.0, *)) {
return self.safeAreaInsets.bottom;
}
return 0;
}
- (CGFloat)TLCNavigationBarHeight {
if (@available(iOS 11.0, *)) {
return MAX(0, self.safeAreaInsets.top-20) + 64;
}
return 64;
}
@end
3、在iOS中如何正确的实现行间距与行高
1、直接设置lineSpacing,
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineSpacing = 10;
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
[attributes setObject:paragraphStyle forKey:NSParagraphStyleAttributeName];
label.attributedText = [[NSAttributedString alloc] initWithString:label.text attributes:attributes];
效果图如下:
红色区域是默认绘制单行文本会占用的区域,可以看到 文字的上下是有一些留白的(蓝色和红色重叠的部分)。
解决方法:我们需要在设置 lineSpacing 时,减去这个系统的自带边距:
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineSpacing = 10 - (label.font.lineHeight - label.font.pointSize);
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
[attributes setObject:paragraphStyle forKey:NSParagraphStyleAttributeName];
label.attributedText = [[NSAttributedString alloc] initWithString:label.text attributes:attributes];
效果图如下所示:
4、iOS使用Instrument-Time Profiler工具排查卡顿问题
1、在TARGETS中将Debug Information Format
设置为DWARF with dSYM File
;
2、将工程跑到真机上;
3、直接运行
instruments
4、运行后,选择主线程,将系统库相关的调用隐藏掉,就可以找到哪个地方的代码调用比较耗时了。
5、添加监听
#import
@interface IMECSServiceCenter : NSObject
@property(nonatomic, strong) NSHashTable *responders;
- (instancetype)initWithNotifyQueue:(dispatch_queue_t)notifyQueue;
- (void)addDelegate:(id)delegate;
- (void)removeDelegate:(id)delegate;
- (void)notifyServiceDelegate:(SEL)aSelector
perform:(void (^)(id responder))perform;
@end
#import "IMECSServiceCenter.h"
#import
@interface IMECSServiceCenter()
@property(nonatomic, strong) dispatch_queue_t notifyQueue;
@end
@implementation IMECSServiceCenter
- (instancetype)init {
self = [super init];
if (self) {
self.responders = [NSHashTable weakObjectsHashTable];
self.notifyQueue = dispatch_get_main_queue();
}
return self;
}
- (instancetype)initWithNotifyQueue:(dispatch_queue_t)notifyQueue {
self = [super init];
if (self) {
self.responders = [NSHashTable weakObjectsHashTable];
self.notifyQueue = notifyQueue;
}
return self;
}
- (void)addDelegate:(id)delegate {
@synchronized(self) {
[self.responders addObject:delegate];
}
}
- (void)removeDelegate:(id)delegate {
@synchronized(self) {
[self.responders removeObject:delegate];
}
}
- (void)notifyServiceDelegate:(SEL)aSelector
perform:(void (^)(id responder))perform {
dispatch_async(self.notifyQueue, ^{
NSArray *responders = self.responders.allObjects;
for (id responder in responders) {
if ([responder respondsToSelector:aSelector]) {
@try {
perform(responder);
}
@catch (NSException *exception) {
MUPLogWarn(@"catch notifyServiceDelegate exception: %@", exception);
}
}
}
});
}
@end
6、关于屏幕旋转 IOS8上面有个坑 枚举值跟9以上不同
NSNumber *value = @(UIDeviceOrientationLandscapeRight);
float systemVersion = [[UIDevice currentDevice].systemVersion floatValue];
if ( systemVersion >= 8.0 && systemVersion < 9 ) { //ios8 遇到的一个坑 要用这个值才能与 UIInterfaceOrientationMaskLandscapeRight匹配
value = @(UIDeviceOrientationLandscapeLeft);
}
[[UIDevice currentDevice] setValue:value forKey:@"orientation"];
7、atomic无法解决线程安全问题
atomic 的内存管理语义是原子性的,仅保证了属性的setter和getter方法是原子性的,是线程安全的,但是属性的其他方法,如数组添加/移除元素等并不是原子操作,所以不能保证属性是线程安全的 如下面的代码会发生异常:
@interface ViewController ()
@property (atomic, strong) NSMutableDictionary *dictionary;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.dictionary = [NSMutableDictionary dictionary];
for (int i = 0; i < 1000000; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.dictionary setObject:@"1" forKey:[NSString stringWithFormat:@"%d",i]];
});
NSString *result = [self.dictionary objectForKey:[NSString stringWithFormat:@"%d",i]];
NSLog(@"thread safe %@", result);
}
8、在block执行过程中正确使用weakSelf、strongSelf
- 用weakSelf保证了Self在Block里不会引发循环引用;
- 在Block内的strongSelf保证在Block运行过程中Self不会被释放掉;
- 如果在block被执行的时候self已经被释放,strongSelf仍然为空,因此仍然需要对strongSelf进行nil判断
很多情况下我们没有对strongSelf判空也没有出现问题并不是因为它不为空,而是即使为空执行方法也不会报错,但仍需要理解,在调用非自己持有的block时,strongSelf并不能保证不为空。
9、危险的UITableView-reloadData
在某些情况下tableView:numberOfRowsInSection可能插在tableView:cellForRowAtIndexPath的中间,当tableView:numberOfRowsInSection的数据变小时,则可能造成tableView:cellForRowAtIndexPath获取数组时越界crash,所以UITableView的刷新是危险的。
解决方式:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row > self.arr.count - 1 || indexPath.row == 0) {
return [UITableViewCell new];
}
//...
NSNumber* content = _arr[indexPath.row];
//...
}
10、网络请求流程
为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。
TCP建立连接为什么是三次握手?
这个问题的本质是, 信道不可靠, 但是通信双发需要就某个问题达成一致. 而要解决这个问题, 无论你在消息中包含什么信息, 三次通信是理论上的最小值. 所以三次握手不是TCP本身的要求, 而是为了满足"在不可靠信道上可靠地传输信息"这一需求所导致的. 请注意这里的本质需求,信道不可靠, 数据传输要可靠. 三次达到了, 那后面你想接着握手也好, 发数据也好, 跟进行可靠信息传输的需求就没关系了. 因此,如果信道是可靠的, 即无论什么时候发出消息, 对方一定能收到, 或者你不关心是否要保证对方收到你的消息, 那就能像UDP那样直接发送消息就可以了.”
通俗大白话来理解TCP协议的三次握手和四次分手
11、网络安全
一条网络请求需要经过的流程
1、DNS 解析,请求DNS服务器,获取域名对应的 IP 地址。
2、通过ARP解析,获取对应IP的mac地址。
3、与服务端建立连接,包括 tcp 三次握手,安全协议同步流程。
4、连接建立完成,发送和接收数据,解码数据。
DNS劫持
-
劫持流程:DNS劫持又称域名劫持,是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。其实本质就是对DNS解析服务器做手脚,或者是使用伪造的DNS解析服务器可以通过下图来展示。
- 解决办法:我们可以不用运营商的DNS解析而使用自己的解析服务器或者是提前在自己的App中将解析好的域名以IP的形式发出去就可以绕过运营商DNS解析,这样一来也避免了DNS劫持的问题。可以通过下图来展示
iOS网络请求优化之DNS映射
HTTP内容劫持
-
劫持流程:以举例子来说,A给B写信,写完邮寄给B,但是在邮寄的过程中,邮递员可能会偷偷的把信取出来看完后再放回去,收发室大爷可能也会偷偷取出来看一眼,B收到信的时候,可能在传输过程中已经被很多人看过了。如果信件中有一些银行卡账号密码什么的,可能在传输过程中已经被别人悄悄记下来了。可以通过下图来展示:
- 解决办法:A和B发现了这个问题,于是他们约定了下次写信要加密,A写信的时候加密,B收到信再解密,加密解密的方法只有A和B知道。同样在寄信的过程中,邮递员、收发室大爷偷偷看了信,但是完全看不懂,信件的内容不会泄露,就安全的多。即HTTPS。
-
HTTPS = HTTP+加密+认证+完整性保护
- 客户端发出握手请求(Client Hello),包含以下信息:
- 支持的协议版本,比如TLS 1.0版。
- 一个客户端生成的随机数(random_1),这个随机数既需要客户端保存又需要发送给服务器。
- 支持的加密方法,比如RSA公钥加密。
- 支持的压缩方法。
- 服务器回复(Server Hello),包含以下信息:
- 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
- 一个服务器生成的随机数(random_2)。
- 确认使用的加密方法,比如RSA公钥加密。
- 服务器证书。
- 如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供”客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。
- 客户端回应,包含以下步骤:
- 验证服务器证书的合法性,证书合法性包括:证书是否过期,发行服务器证书的 CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;
- 客户端使用一些加密算法(例如:RSA,Diffie-Hellman)产生一个48个字节的Key,这个Key叫PreMaster Secret。该PreMaster Secret用服务器公钥加密传送,防止被窃听。
- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
- 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。
- 如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。
- 服务器回应,服务器通过上面的三个随机数(random1,random2,PreMaster Secret),计算出本次会话的『会话密钥(session secret)』,然后向客户端发送下面信息
- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
-
服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
- AFNetworking对服务端的身份进行认证有三种可选方案:
- AFSSLPinningModeNone(Default):客户端自己不会对服务端证书进行自主校验,而是直接默认信任,将证书交给系统来进行校验,判断返回的证书是否是官方机构颁发的,如果是则信任,这个应该也是普通开发者采用最多的方案。这种认证方案如果被劫持后返回的证书也是官方机构颁发的,那么客户端就会正常进行网络访问,但是返回的数据就不是想要的数据,比如被塞广告等现象的出现。
- AFSSLPinningModePublicKey:代表客户端会将服务器端返回的证书与本地保存的证书中的 PublicKey 部分进行自主校验。
- AFSSLPinningModeCertificate:代表客户端会将服务器端返回的证书和本地保存的证书中的所有内容,包括 PublicKey 和证书部分全部进行自主校验。如果校验成功,才继续进行系统验证等后续行为。
12、Responder chains in an app
13、优化tableview滑动性能
避免图层混合
1、确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明
2、如无特殊需要,不要设置低于1的alpha值
3、确保UIImage没有alpha通道避免图片的临时转换
1、确保图片大小和frame一致,不要在滑动时缩放图片
2、确保图片颜色格式被GPU支持,避免频繁CPU转换慎用离屏渲染
1、绝大多数时候离屏渲染会影响性能
2、重写drawRect方法,设置圆角、阴影、模糊效果,光栅化都会导致离屏渲染
3、设置阴影效果是加上阴影路径
imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).cgPath;
4、滑动时若需要圆角效果,开启光栅化。
// 设置圆角
label.layer.masksToBounds = true
label.layer.cornerRadius = 8
label.layer.shouldRasterize = true
label.layer.rasterizationScale = layer.contentsScale
14、OC中的load和initialize
1、load和initialize方法都会在实例化对象之前调用,以main函数为分水岭,前者在main函数之前调用,后者在之后调用。这两个方法会被自动调用,不能手动调用它们。
2、load和initialize方法都不用显式的调用父类的方法而是自动调用,即使子类没有initialize方法也会调用父类的方法,而load方法则不会调用父类。
3、load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。
4、load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。
15、后台保活
利用后台下载任务实现。
关键在于:在handleEventsForBackgroundURLSession:completionHandler:
方法不要执行completionHandler()
。
- (NSURLSession *)backgroundURLSession {
__weak typeof(self) weak_self = self;
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *identifier = @"io.objc.backgroundTransferExample";
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:weak_self
delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
- (void)didEnterBackgroundNotification:(NSNotification *)notification {
NSString *downloadURLString = @"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3588772980,2454248748&fm=27&gp=0.jpg";
NSURL* downloadURL = [NSURL URLWithString:downloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithRequest:request];
task.taskDescription = [NSString stringWithFormat:@"Podcast Episode %d", 123];
[task resume];
}
在AppDelegate.m
中实现以下代理方法
- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(nonnull void (^)(void))completionHandler {
// 不要调用completionHandler!
}
此方案的优点:
1、不受系统权限控制;
2、可以运行很久,除非app被杀死。