开发小知识(一)
开发小知识(二)
仅做笔记使用。。。。。。。
目录
- 一、UIButton 继承链
- 二、UIStackView
- 三、中文 log 乱码问题
- 四、锚点
- 五、线程锁
- 五、动画分类
- 六、动画曲线
- 七、Xcode中other linker flags 的作用
- 八、GCD
- 九、临界资源概念
- 十、线程锁
- 十一、线程不安全是否会导致崩溃?
- 十二、全局并发队列和手动创建的并发队列
- 十三、子线程中能否运行定时器
- 十四、atomic 和线程安全
- 十五、内存管理
- 十六、多级缓存的意义(寄存器、内存、磁盘)
- 十七、ARC 帮我们做了什么操作
- 十八、iOS 如何进行网络测速
- 十九、泛型
- 二十、局部性原理
- 二十一、汇编语言种类
- 二十二、常用汇编指令
- 二十三、Swift 中结构体都存在占空间吗?
- 二十四、dSYM解析
- 二十五、布隆过滤器
- 二十六、iOS 小组件widget开发
- 二十七、应用程序生命周期 watch dog 时间
- 二十八、捕获不到的异常
- 二十九、为何需要解压缩图片
- 三十、IQKeyboard 原理
- 三十一、Symbol
- 三十二、查询所有应用bundle id 和 udid 命令
- 三十三、本地 commit 的代码撤销
- 三十四、查找静态库是否包含特定字符串
- 三十五、curl 、postman
- 三十六、DYSM 解析流程记录
- 三十七、SDWebImage性能优化
- 三十八、打包原理及脚本自动打包的实现
- 三十九、 安装 ipa 包
- 四十、import "" 和 import <>
- 四十一、抓包原理
- 四十二、iOS是否支持重载
- 四十三、js里继承的实现原理;同样是面向对象的语言,和iOS有啥区别
- 四十四、Mach-O
- 四十五、代码混淆
- 四十六、 FPS 帧率监测原理
- 四十七、layoutSubviews、setNeedsLayout、layoutIfNeeded
- 四十八、子线程修改 frame 会不会生效?
- 四十九、视图是如何显示到界面上的
- 五十、SEL 和 IMP 的区别
- 五十一、设计的6大原则
- 五十二、isKindOfClass 和 isMemberOfClass
- 五十三、成员变量和属性
- 五十四、通用缓存设计
- 五十五、@synchronized 锁
- 五十六、RN 和原生通信原理
- 五十七、RN 和 Flutter
- 五十八、AOT 和 JIT
- 五十九、离屏渲染
- 六十、NSString用copy还是strong修饰?
- 六十一、动态链接库和静态链接库
- 六十二、引用计数加减
- 六十三、OC 的动态性体现
- 六十四、强类型和弱类型语言
- 六十五、UIWebView 和 WKWebView 的区别
- 六十六、ARC 上可能出现野指针的情况
- 六十七、NSHashTable(NSMutableArray) 和 NSMapTable(NSMutableDictory)
- 六十八、UITableView 多图下载
- 六十九、井盖为什么是圆的
- 七十、DNS(Domain Name System) 解析过程
- 七十一、DNS 劫持
- 七十二、URL 和 URI 的区别
- 七十三、.a 和 frameWork 区别
- 七十四、数据库知识
- 七十五、IM SDK
- 七十六、去除实际业务中数组中重复元素
- 七十七、ABTest SDK
- 七十八、YYCache
- 七十九、YYModel
- 八十、ASyncDisplayKit
- 八十一、AFNetworking 2.0 和 3.0
- 八十二、OC 和 C 对比
- 八十三、对OC 的理解
- 八十四、数组和链表区别
- 八十五、线程最大并发数实现方法(GCD)
- 八十六、多个网络请求完成后执行下一步
- 八十七、项目中分类同名怎么处理
- 八十八、按钮点击防抖处理
- 八十九、通知实现原理
- 九十、instance和id的区别
- 九十一、指出如下代码不合理的地方
- 九十二、流量监控
- 九十三、import 和 include
- 九十四、为什么有类对象和元类对象
- 九十五、垃圾回收和自动引用计数
- 九十六、串行队列
- [九十七、convertPoint: 与 convertRect:]
- 九十八、Lottie实现原理
- 九十九、category和extension
- 一百、YYModel原理
一、UIButton 继承链
UIButton > UIControl > UIView > UIResponder > NSObject
如果 UIButton 上的自带的 UIImageView 和 UILable 无法满足界面要求,且还想添加类似 UIButton 的点击事件,此时封装的视图组件可继承于 UIControl。初始化组件时可添加点击事件:
[self addTarget:self action:@selector(clickedAction:) forControlEvents:UIControlEventTouchUpInside];
补:
- UIResponder:并不是任何对象都能处理事件,只有继承了
UIResponder
的对象才能接受并处理事件,称之为响应者对象
。UIApplication
、UIViewController
、UIView
都继承自UIResponder
。UIResponder
内部提供了 touch 系列事件、motion 系列事件、remoteControl 系列事件。分别对应如下系统三种事件分类。 - 系统三种事件分类:触摸事件、加速器事件、远程控制事件(如耳机控制音乐)。
- 不能接受触摸事件的三种情况:
hidden = YES;
alphe = 0.0 ~ 0.01;
userInteractionEnabled = NO;
二、UIStackView
在iOS9中苹果在UIKit框架中引入了一个新的视图类 UIStackView。UIStackView 类提供了一个高效的接口用于平铺一行或一列的视图组合。Stack视图管理着所有在它的 arrangedSubviews 属性中的视图的布局。这些视图根据它们在 arrangedSubviews 数组中的顺序沿着 Stack 视图的轴向排列。虽然 UIStackView 是继承与 UIView,但是却没有继承 UIView 的渲染功能,UIStackView 是没有 UI的,也就是不显示本身。类似backgroundColor
的界面属性无效,重写layerClass
, drawRect:
甚至drawLayer:inContext:
都是无效的。UIStackView 是一个纯粹的容器 View。参考文章:
三、中文 log 乱码问题
在网络请求的返回数据中,默认的 NSLog 打印的字典中汉字是因 UTF8 编码显示的"乱码",对调试不是很友好。此时可以在分类中重写 descriptionWithLocale
方法,数组也是如此。
@implementation NSDictionary (Log)
/**
分类中重写方法解决字典输出中文乱码的问题
@return 输出结果
*/
- (NSString *)descriptionWithLocale:(id)locale {
//以下两种方法均能实现目的,第二中方法排版视觉效果更好
// NSMutableString *string = [NSMutableString stringWithString:@"{\n"];
// [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// [string appendFormat:@"\t%@ = %@;\n", key, obj];
// //去除转义字符
// [string stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
// }];
// [string appendString:@"}\n"];
// return string;
NSString *output;
@try {
output = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding];
output = [output stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]; // 处理\/转义字符
} @catch (NSException *exception) {
output = self.description;
} @finally {
}
return output;
}
@end
@implementation NSArray (Log)
/**
分类中重写方法解决数组输出中文乱码的问题
@return 输出结果
*/
- (NSString *)descriptionWithLocale:(id)locale {
NSMutableString *string = [NSMutableString stringWithString:@"(\n"];
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[string appendFormat:@"\t%@,\n", obj];
//去除转义字符
[string stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
}];
if ([string hasSuffix:@",\n"]) {
[string deleteCharactersInRange:NSMakeRange(string.length - 2, 1)]; // 删除最后一个逗号
}
[string appendString:@")\n"];
return string;
}
@end
四、锚点
- UIView 有 frame、bounds 和 center三个属性。CALayer 有类似的属性,分别为 frame、bounds、position、anchorPoint。
- anchorPoint
想象一下,把一张A4白纸用图钉订在书桌上,如果订得不是很紧的话,白纸就可以沿顺时针或逆时针方向围绕图钉旋转,这时候图钉就起着支点的作用。此时图钉就相当于 anchorPoint ,它主要的作用就是用来作为变换的支点(如transform 缩放、位移动画的起始点)。anchorPoint 值是相对 bounds 比例确定的,在白纸的左上角、右下角 anchorPoint 分别为 (0,0)、(1, 1),由此可知中心点的 anchorPoint 为 (0.5 , 0.5)。实际创建出来的 CALayer 的 anchorPoint 默认为 (0.5 , 0.5)。 - position
实际创建出来的 CALayer 的 position 默认为中心点。CAlyer 中的 position 和 UIView 中的 center 对应。和 center 一样 position 是相对superLayer 的,但 anchorPoint 是相对 layer 的。//anchorPoint 默认为 (0.5 , 0.5) position.x = frame.origin.x + 0.5 * bounds.size.width; position.y = frame.origin.y + 0.5 * bounds.size.height;
- anchorPoint、position、frame 关系
上面的公式可以转为如下,从而说明了为何更改 anchorPoint 后,视图会有所移动 (frame 的 origin 会发生变化)。同理修改 position 后,视图也会有所移动 (frame 的 origin 会发生变化)。总的来说,anchorPoint 和 position 两者并不会相互影响,修改两者中的任何一个只会改变 frame.origin。position.x = frame.origin.x + anchorPoint.x * bounds.size.width; position.y = frame.origin.y + anchorPoint.y * bounds.size.height;
如果想修改anchorPoint而不想移动layer,在修改anchorPoint后再重新设置一遍frame就可以达到目的,这时position就会自动进行相应的改变。
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;
额外补充:
CGAffineTransformMakeScale是对单位矩阵进行缩放。
CGAffineTransformScale是对第一个参数的矩阵进行缩放。
默认是基于视图的中心去缩放。
_v.bounds = CGRectMake(0, 0, 100, 100);
_v.center = CGPointMake(100, 400);
//此时:(origin = (x = 50, y = 350), size = (width = 100, height = 100))
_v.transform = CGAffineTransformMakeScale(2, 2);
//此时:(origin = (x = 0, y = 300), size = (width = 200, height = 200))
五、动画分类
注意: CGAffineTransform 系列不同于 CAAnimation 系列,CGAffineTransform 会改变视图的 frame 相关属性,CAAnimation 只是制造假象。
参考
六、动画曲线
- 对于 UIViewAnimationCurve 系列动画曲线,主要是线条的斜率变化。
UIViewAnimationCurveEaseInOut, // slow at beginning and end
UIViewAnimationCurveEaseIn, // slow at beginning
UIViewAnimationCurveEaseOut, // slow at end
UIViewAnimationCurveLinear,
- 对于弹簧动画,主要注意距离终点的距离。随着时间的推移,距离终点的波动范围越来越小。
参考
七、Xcode中other linker flags 的作用
八、GCD
注意:实际面试过程中有人会问到队列和线程的关系,实际回答过程中就针对下方的表达做回答即可。
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
和dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
的前两个参数分别为队列和要执行的任务。
- sync 和 async(主要是用于描述任务):
同步:在当前线程中执行任务,不具备开启新线程的能力。
异步:在新的线程中执行任务,具备开启新线程的能力。
- 队列(理解为数据结构中的对列):
并发队列、手动创建的串行队列、主队列(是一种特殊的串行队列)。dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
为全局并发队列。并发是指多个任务并发(同时)执行;串行是指一个任务执行完毕后,再执行下一个任务。
通过上图可以看出 2 条重要信息:
- 只有异步+并发队列(可开启多个线程) 和 异步+手动创建的串行队列(最多开启一个线程)才能开启新线程;
- 只有异步+并发队列 才能并发执行任务;
另外还有一个注意点:
sync 函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)。比如下面代码,任务1、dispatch_sync、以及任务 3 在主队列中,此时又往主队列中添加任务2,当前队列指的就是主队列,且主队列本身是串行队列。
- (void)ViewDidLoad{
NSLog(@"1");// 任务1
dispatch_sync(dispatch_get_main_queue(),^{
NSLog(@"2");// 任务2
});
NSLog(@"3");// 任务3
}
九、临界资源概念
临界资源指的是一些虽作为 共享资源却又无法同时被多个线程共同访问的资源 。当有进程在使用临界资源时,其他进程必须依据操作系统的同步机制等待占用进程释放共享资源才可重新竞争使用共享资源。
十、线程锁
十一、线程不安全是否会导致崩溃?
先用一幅图说明什么是线程不安全问题,如两个线程同时往里面写数据,就有可能存在互相覆盖的情况。
线程不安全自身是不会导致崩溃问题的,但是线程不安全引发的数据或逻辑上的错误可能引发崩溃问题。参照上图逻辑顺序,子线程1先删除了数组中的部分元素,但是子线程2并不知道,此时还是拿删除之前的数组长度去访问最后一个元素会发生崩溃。
十二、全局并发队列和手动创建的并发队列
dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue3 = dispatch_queue_create("queu3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue4 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);
//虽然第一个参数和queue4 对象一样,但最后创建出来也是不同的队列对象
dispatch_queue_t queue5 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);
全局并发队列可以理解为一个全局对象,在整个应用内任何地方获取的都是同一个队列对象。手动创建的并发队列每次都是创建不同的对象,即使第一个参数一样(实际开发中不建议这样做),最终返回也是不同的队列对象。
十三、子线程中能否运行定时器
正常情况下来说子线程无法运行定时器,因为一旦出了子线程的生命周期,子线程已经被销毁,之后的代码将无法正常运行。但通过线程保活技术可以继续运行定时器。
十四、atomic 和线程安全
一般的所可以针对特定的代码片段去添加,保证对应代码片段的线程安全。而 atomic 仅用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁。但它并不能保证使用属性的过程是线程安全的。如 arr 是obj 对象中的 atomic 修饰的属性,当执行 [obj.arr addObject:@"1"];
这行代码时,obj.arr 会调用 arr 的 getter 方法,是线程安全的。但 addObject 并不会触发 arr 的 getter 或 setter 方法,此时不是线程安全的。
因为 atomic对每一次 setter 和 getter 方法调用都加锁, 比较消耗性能,所以一般在 iOS 设备上很少使用,Mac 上偶尔会使用到。
十五、内存管理
- 虚拟内存:虚拟内存会给每个程序创建一个单独的执行环境,也就是一个独立的虚拟空间,这样每个程序就只能访问自己的地址空间(Address Space),程序与程序间也就能被安全地隔离开了。另外,程序、数据和堆栈的总大小可能超过可用的物理内存的大小。由操作系统把程序当前使用的那些部分保留在主存中,而把其他部分保存在磁盘上。
- 分段:分段就是将进程里连在一起的代码段、数据段、栈、堆分开成独立的,段之间不连续。这样,内存的空间管理 MMU 就可以更加灵活地进行内存管理。每个段的大小不一样,在申请的内存被释放后,容易产生碎片,这样在申请新内存时,很可能就会出现所剩内存空间够用,但是却不连续,于是造成无法申请的情况。这时,就需要暂停运行进程,对段进行修改,然后再将内存拷贝到连续的地址空间中。但是,连续拷贝会耗费较多时间。所以可以继续使用分页技术。
- 分页:可以使用比段粒度更小的空间管理技术,也就是分页。分页就是把地址空间切分成固定大小的单元,这样我们就不用去考虑堆和栈会具体申请多少空间,而只要考虑需要多少页就可以了。对于操作系统管理来说也会简单很多,只需要维护一份页表(Page Table)来记录虚拟页(Virtual Page)和物理页(Physical Page)的关系即可。
多级页表的原理
十六、多级缓存的意义(寄存器、内存、磁盘)
。。。。。。。
十七、
。。。。。。。
十八、iOS 如何进行网络测速
https://www.jianshu.com/p/9f67b7716b9d
http://www.cocoachina.com/articles/27654
通过调研发现,目前常见的网络测速方案只有两种:
方案1:通过上传和下载数据包,使用 TotalSize / TotalTime 来计算真实的上传和下载速率是多少
方案2:通过读取网卡数据来计算(可以通过系统API 获取),读取上一秒的整体流量消耗 T1,然后读取当前的流量消耗 T2,那么 T2 - T1 其实可以表示为当前的一个网速情况。同时这个流量数据是可以区分蜂窝网络、Wi-Fi的,也可以区分哪些是上行流量,那些是下行流量。
两种方案各有优劣,可以在合适的场合来选择对应的方案
第一种方案感觉是比较准确,这个时候是真实的在下载或上传数据,比较充分的利用了当前的带宽,计算的网速也比较接近真实的网速值。但是蜂窝网络下,会消耗用户的少量流量。
第二种方案在下载和上传东西时,计算的值和第一种方案比较接近。但是如果当前系统内没有 App 在被使用,处于静止状态的话,其实当前读取的流量值是比较小的,无法反映出网速情况,但是可以实时反映流量消耗状况。
十九、泛型
iOS 强大的泛型
[iOS][OC] 理解并应用OC的泛型提高代码质量
http://blog.sunnyxx.com/2015/06/12/objc-new-features-in-2015/
二十、局部性原理
二十一、汇编语言种类
汇编语言的种类:
- 8086 汇编(16bit)
- x86 汇编(32bit)
- x64 汇编 (64bit)
- ARM 汇编 (嵌入式、移动设备)
其中 x86 和 x64 汇编根据编译器的不同,有 2 中书写形式 Intel(Windows 派系) 和 AT&T(Unix 派系)。
iOS 开发者主要涉及两种汇编语言 AT&T 汇编(模拟器)和 ARM 汇编(真机)。
二十二、常用汇编指令
二十三、Swift 中结构体都存在占空间吗?
腾讯课堂视频第六节 1:00分前后,Class 内存一定存在堆空间,但是Class 对象可能存在栈区
二十四、dSYM解析
二十五、布隆过滤器
二十六、iOS 小组件widget开发
二十七、应用程序生命周期 watch dog 时间
- 启动(Launch):20s;
- 恢复(Resume):10s;
- 挂起(Suspend):10s;
- 退出(Quit):6s;
- 后台(Background):3min(在 iOS 7 之前,每次申请 10min; 之后改为每次申请 3min,可连续申请,最多申请到 10min)。
二十八、捕获不到的异常
KVO 问题、NSNotification 线程问题、数组越界、野指针等崩溃信息,是可以通过信号捕获的。但是,像后台任务超时、内存被打爆、主线程卡顿超阈值等信息,是无法通过信号捕捉到的。
- 后台任务超时:在你的程序退到后台以后,只有几秒钟的时间可以执行代码,接下来就会被系统挂起。进程挂起后所有线程都会暂停,不管这个线程是文件读写还是内存读写都会被暂停。但是,数据读写过程无法暂停只能被中断,中断时数据读写异常而且容易损坏文件,所以系统会选择主动杀掉 App 进程。此时的异常信息是无法被捕捉到的。解决方案是请求更多后台执行时间,但是最多只能请求 180s 后台时间。也可以严格控制后台数据的读写操作,如果数据过大,也就是在后台限制时间内或延长后台执行时间后也处理不完的话,可以考虑在程序下次启动或后台唤醒时再进行处理。
- 内存被打爆:
- 主线程卡顿超阈值:通过 RunLoop 监测卡顿,被收集相关堆栈信息。
二十九、为何需要解压缩图片
三十、IQKeyboard 原理
IQKeyboardManager 在需要解决键盘遮挡时会去递归找可滚动的父视图进行偏移,如果没有就对 window 的 frame 做文章。核心方法是 adjustFrame,通过它解决键盘遮挡。IQKeyboardManager 考虑的非常全面,以至于里面很多的判断语句,主要是为了能使用于目前发现的任何情况
三十一、Symbol
知识小集 :深入理解
参考
三十二、查询所有应用bundle id 和 udid 命令
ideviceinstaller -l
instruments -s
三十三、本地 commit 的代码撤销
git reset HEAD~
三十四、查找静态库是否包含特定字符串
./findlib.sh ~/Desktop/iphone_ive_cook/SRC "_DriftsandEn"
findlib.sh 内部代码
#!/bin/sh
function func_log_print(){
local level="$1"
local msg="$2"
case $level in
error) echo -e "\033[41;30;1m${msg}\033[0m";;
warn) echo -e "\033[43;30;1m${msg}\033[0m";;
info) echo -e "\033[47;30;1m${msg}\033[0m";;
concern) echo -e "\033[42;30;1m${msg}\033[0m";;
*) echo "${msg}";;
esac
}
## 暂时缺对framework的查询
find $1 -name '*.a' > .ddddtmp.bat
for line in `cat .ddddtmp.bat`
do
aa=`strings ${line} | grep $2 -I`
if [ -n "${aa}" ] ;then
func_log_print warn "${line}"
strings ${line} | grep $2 -I
fi
done
rm .ddddtmp.bat
三十五、curl 、postman
三十六、DYSM 堆栈解析流程记录
1、DYSMTools 解析
参考
mac 自带进制计算器使用
涉及进制计算,需要使用mac 自带计算器进行进制计算。
2、手机内部崩溃信息解析
1.首先在桌面创建个文件夹,如:crash
2.通过终端指令:find /Applications/Xcode.app -name symbolicatecrash -type f找到Xcode自带解析工具symbolicatecrash的路径,之后把这个文件复制到桌面crash文件夹中。
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
3.在Archives下找到打包文件,右键进入finder打开显示包内容,之后找到xx.app和xx.app.dSYM文件,xx代表项目名
4.把xx.app.dSYM复制到桌面crash文件夹中
5.在Crashes中找到对应xx.app.dSYM的crash信息,右键把.crash文件复制到桌面crash文件夹中
- .app.dSYM和.crash文件的UUID要一致
7.查看xx.app.dSYM的UUID,先终端cd 到放置xx.app.dSYM的文件夹,之后 dwarfdump --uuid xx.app.dSYM/ (xx代表App的项目名)
8.查看xx.crash文件的UUID,先终端cd 到放置xx.crash的文件夹, 之后 grep "AppName arm64" xx.crash
9.查看xx.app的UUID,先终端cd 到放置xx.app的UUID的文件夹,之后dwarfdump --uuid xx.app/xx (xx代表App的项目名)
10.开始进行解析工作 cd 到桌面crash文件夹中 - 输入命令(解析核心命令): ./symbolicatecrash ./.crash ./.app.dSYM > symbol.crash
sfit-macmini-for-test:crash user$ ./symbolicatecrash CrashDemo0809-2018-08-09-143458.ips CrashDemo0809.app.dSYM > out.crash
sfit-macmini-for-test:crash user$
- 如果不成功用xcode-select -print-path 检查一下环境变量,正确返回/Applications/Xcode.app/Contents/Developer/ 如果返回的不是这个,用export DEVELOPER_DIR=/Applications/XCode.app/Contents/Developer 设置导出一下环境变量
参考
三十七、SDWebImage性能优化
SDWebImage有个性能优化(不是问缓存流程那一套),咋优化的;
图片解析那块,就是先在子线程把解析的数据转成CGImage
1、SDWebImage提供了对图片进行了缓存,主要由SDImageCache完成。该类负责处理内存缓存以及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,不会对UI造成影响。
- 内存缓存的处理由NSCache对象实现,NSCache类似一个集合的容器,它存储key-value对,类似于NSDictionary类,可以设置缓存限额,当超过限额的时候,会自动移除之前的记录,然后添加新的记录。如果内存紧张就会自动释放,通常在使用 NSCache 的时候可以在didReceiveMemoryWarning 收到内存警告方法中调用 [self.cache removeAllObjects]; 这句代码。另外,NSCache 是线程安全的。
- 磁盘缓存的处理使用NSFileManager对象实现,图片存储的位置位于cache文件夹,另外 SDImageCache 还定义了一个串行队列来异步存储图片。
2、图片解压缩
SDWebImage 在下载完图片之后,会调用decodedImageWithImage:image
方法异步对图片进行解压缩后返回。
在我们使用 UIImage 的时候,创建的图片通常不会直接加载到内存,而是在渲染的时候再进行解压并加载到内存。这就会导致 UIImage 在渲染的时候效率上不是那么高效。为了提高效率通过 decodedImageWithImage方法把图片提前解压加载到内存,这样这张新图片就不再需要重复解压了,提高了渲染效率。这是一种空间换时间的做法。
图片的渲染流程分为 3个阶段 :
- 加载(Load):从磁盘中加载资源
- 解码(Decoder):在 SDWebImage 中,图片加载完成后sd_imageFormatForImageData 方法中通过 DataBuffer 的 第一个字节来判断图片的格式的。 接着,需要将 Data Buffer 的 JPEG、PNG或其他编码的数据 ,转换为 每个像素 的 图像信息 ,这个过程称为 Decoder(解码) ,然后将像素信息存放在 ImageBuffer 。
- 渲染(Render):显示
三十八、打包原理及脚本自动打包的实现
我们写出的代码经过llvm进行build, 编译完成后会生成.app文件, 之后进行Archive归档, 然后进行Export导出, 这就是最基本的原理。打包脚本可以借助苹果官方的 xcodebuild 命令。总结为如下三步:
- 一、编译
1、xcode 中首先配置好证书。
2、执行如下命令:
xcodebuild -workspace "/Users/sam/Desktop/TestPackage-workspace/TestPackage.xcworkspace" -scheme "TestPackage" -configuration "Debug"
3、xcode 缓存路径下生成 .app 文件,显示包内容,会发现里面包含签名文件。说明应用签名成功。
- 二、归档
归档是 xcode 记录打包结果的一种手段,每次打包都会生成对应的归档文件。归档文件内部包含了打包时间、版本、dsym 等信息。
执行如下命令,最终生成一个 .xcarchive 文件。
xcodebuild -workspace "/Users/sam/Desktop/TestPackage-workspace/TestPackage.xcworkspace" -scheme "TestPackage" -configuration "Debug" -archivePath "/Users/sam/Desktop/TestPackage.xcarchive" archive
- 三、导出
执行如下命令导出,最终生成 .ipa 文件。
-exportOptionsPlist "/Users/sam/Desktop/TestPackage 2018-12-07 17-29-35/ExportOptions.plist"
- 四、上传 AppStore
三十九、 安装 ipa 包
四十、import "" 和 import <>
四十一、抓包原理
https://www.jianshu.com/p/ca3204cb793a
四十二、iOS是否支持重载
概念:所谓方法的重写是指子类中的方法和父类中继承的方法有完全相同的返回值类型、方法名、参数个数和参数类型。这样就可以实现对父类方法的覆盖。只能发生在父类和子类之间,又叫函数覆盖。
所谓的方法重载是指函数名相同,函数的参数列表不同(包括参数个数和参数类型),至于返回类型可同可不同。重载既可以发生在同一个类的不同函数之间,也可发生在父类子类的继承关系之间,其中发生在父类子类之间时要注意与重写区分开。
结论:按照重载严格定义说OC不支持重载,但事实上OC支持参数个数不同的函数重载。
描述:
//参数个数不同的函数重载。本质上是方法名不同,函数参数多少回影响到方法名。但在js 中不会出现此种的情况。
- (void)test:(int)one {
NSLog(@"one parameter!");
}
- (void)test:(int)one andTwo:(int)two {
NSLog(@"two parameters!");
}
//参数个数相同的函数不支持重载,后三个方法会报错。即使方法返回值不同,也依然报错,OC中只认方法名
- (void)testTwo:(int)two{}
- (void)testTwo:(NSInteger)two2{}
- (BOOL)testTwo:(NSInteger)two{}
- (BOOL)testTwo:(NSInteger)two{}
四十三、js里继承的实现原理;同样是面向对象的语言,和iOS有啥区别
https://juejin.im/post/5c60ca2ae51d45719046d73f
四十四、Mach-O
Mach-O 类型
- MH_OBJECT
目标文件(.o)
静态库文件(.a),静态库其实就是N个.o合并在一起 - MH_EXECUTE:可执行文件
.app/xx - MH_DYLIB:动态库文件
.dylib
.framework/xx - MH_DYLINKER:动态链接编辑器
/usr/lib/dyld - MH_DSYM:存储着二进制文件符号信息的文件
.dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
通用二进制文件
- 同时适用于多种架构的二进制文件,包含了多种不同架构的独立的二进制文件,如真机 arm64 和 armv7 架构。
- 因为需要储存多种架构的代码,通用二进制文件通常比单一平台二进制的程序要大。
- 由于两种架构有共同的一些资源,所以并不会达到单一版本的两倍之多。
- 由于执行过程中,只调用一部分代码,运行起来也不需要额外的内存。
- 因为文件比原来的要大,也被称为“胖二进制文件”(Fat Binary)。
- 通过 lipo 指令可以拆分或合并不同架构的二进制文件。
Mach-O 结构
一个Mach-O文件包含3个主要区域,通过 MachOView图像可视化工具可以分析Mach-O 结构。
- Header
包含文件类型、目标架构类型等。 - Load commands
描述文件在虚拟内存中的逻辑结构、布局信息。一般是指内存的分段(数据段、代码段等)信息,如分段的大小、开始和结束位置等信息。 - Raw segment data
在Load commands中定义的 分段(Segment)的原始数据。逆向过程中,一般是先找到对应的分段的位置,然后修改分段的内部信息,最终达到逆向串改的目的。
四十五、代码混淆
加固方式
加固是为了增加应用的安全性,防止应用被破解、盗版、二次打包、注入、反编译等。常见的加固方式有:
- 数据加密(字符串、网络数据、敏感数据等)。
- 应用加壳(二进制加密),上传到 AppStore 的应用,默认二进制已经被加密。
- 代码混淆(类名、方法名、代码逻辑等)。
代码混淆
iOS 程序可以通过class-dump、Hopper、IDA等工具获取类名、方法名、以及分析程序的执行逻辑,方便别人逆向自己的应用。如果进行代码混淆,可以加大别人的分析难度。
代码混淆方案:
- 源码的混淆(类名、方法名、协议名等),更为推荐此种方案。
- LLVM中间代码 IR 的混淆(容易产生BUG)
可以自己编写Pass
也可以参考 ollvm:https://github.com/obfuscator-llvm/obfuscator 源码
源码混淆方案
通过宏定义替换方法名、类名等,宏对应的值是无规律的字母符号,如方法 test 定义为
#define test qwer_asd_zxc
。
1、不能混淆系统方法。
2、不能混淆 init 开头的等初始化方法,苹果规定初始化方法需要用 init
3、混淆属性时需要额外注意 set 方法,由于属性名被宏替换了,但是 set 方法没有用宏替换,可能导致 set 方法无效。
4、如果xib、storyboard中用到了混淆的内容,需要手动修正
5、可以考虑把需要混淆的符号都加上前缀,跟系统自带的符号进行区分。
6、混淆过多可能会被AppStore拒绝上架,需要说明用途。建议值混淆核心业务代码。第三方工具 ios-class-guard
1、链接 https://github.com/Polidea/ios-class-guard
2、它是基于class-dump的扩展,用class-dump扫描出可执行文件中的类名、方法名、属性名等并通过宏做替换,会更新xib和storyboard的名字等等
3、最终还会生成文件映射表,当应用崩溃时,通过dysm分析获取混淆后的方法名和类名,最终需要通过文件映射表和混淆后的类名方法名定位具体崩溃的方法。
字符串加密
可执行文件中的字符串信息,对破解者来说,非常关键,是破解的捷径之一。为了加大破解、逆向难度,可以考虑对字符串进行加密。字符串的加密技术有很多种,可以根据自己的需要自行制定算法。
如:对每个字符进行异或()处理,当需要使用字符串时,对异或()过的字符再进行一次异或(^),就可以获得原字符。主要原理是两次异或操作最终得到其本身。
20^5//得到的不是20
20^5^5//最终得到20
四十六、 FPS 帧率监测原理
CADisplayLink 的调用频率始终和屏幕刷新的频率一致。所以 FPS = 刷新次数 / 间隔时间。
四十七、layoutSubviews、setNeedsLayout、layoutIfNeeded
UI 刷新时机
如果打印App启动之后的主线程 RunLoop 可以发现另外一个 callout 为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay / setNeedsLayout之后就会将这些操作提交到全局容器。而这个 Observer 监听了主线程 RunLoop 的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的 UI 更新并提交进行实际绘制更新。
layoutSubviews
继承于UIView的子类重写,进行布局更新,刷新视图。如果某个视图自身的 bounds 或者子视图的 bounds 发生改变,那么这个方法会在当前runloop结束的时候被调用。为什么不是立即调用呢?因为渲染比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个 RunLoop 运行周期,所有UI控件在布局上的变动统一生效并且在视图上更新,通过这种高性能的机制保障了视图渲染的流畅性。setNeedsLayout(仅仅标记是否需要从新布局,layoutIfNeeded 内部负责调用刷新逻辑)
标记为需要重新布局,异步调用 layoutIfNeeded 刷新布局,不立即刷新,在下一轮 runloop 结束前刷新,对于这一轮 runloop 之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用。 相比于 layoutIfNeeded ,该方法更常用。使用场景:layoutSubviews 方法内部布局 UI ,外部手动更新完 UI 之后,再手动调用 setNeedsLayout 方法, 在当前 runloop 即将进入休眠时刷新。layoutIfNeeded
如果有需要刷新的标记(视图本身发生变化或手动调用setNeedsLayout 方法产生的标记),立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。
四十八、子线程修改 frame 会不会生效?
主线程刷新 UI 原因
像UIKit这样大的框架上确保线程安全是一个重大的任务,会带来巨大的成本。UIKit不是线程安全的,假如某一个线程中遍历找寻某个subView,然而在另一个线程中删除了该subView,那么就会造成错乱。apple有对大部分的绘图方法和诸如UIColor等类改写成线程安全可用,可还是建议将UI操作保证在主线程中。
答:不会生效。
- 在子线程中是不能进行UI更新的,而可以更新的结果只是一个幻像:因为子线程代码执行完毕了,又自动进入到了主线程,执行了子线程中的UI更新的函数栈,这中间的时间非常的短,就让大家误以为分线程可以更新UI。
- 如果子线程一直在运行,则子线程中的UI更新的函数栈主线程无法获知,即子线程无法更新 UI。
参考
四十九、视图是如何显示到界面上的
CALayer 和 UIView
CALayer 里有个 id <CALayerDelegate> delegate
属性,实际代码运行中,这个 delegate 被赋值为 UIView 类,即 UIView 是 CALayer 的代理。UIView控制大小和事件处理,CALayer控制渲染和内容展示。
@property(nullable, weak) id <CALayerDelegate> delegate;
UIView主要是对显示内容的管理而 CALayer 主要侧重显示内容的绘制。
渲染过程
注意 drawRect 的消耗
首先要清楚,在 drawRect 中所调用的重绘功能是基于 Quartz 2D 实现的,Quartz 2D是CoreGraphics框架的一部分,因此其中的相关类及方法都是以CG为前缀。在drawRect重绘过程中最常用的就是CGContext类。CGContext又叫图形上下文,相当于一块画板,以堆栈形式存放,只有在当前context上绘图才有效。
CAShapeLayer 与 drawRect 重绘的一些比较:
- 1、CAShapeLayer配合贝赛尔曲线使用时,绘图形状更灵活,而drawRect只是一个方法而已,在其中更适合绘制大量有规律的通用的图形;
- 2、CALayer的属性变化默认会有动画,drawRect绘图没有动画;
- 3、CALayer绘制图形是实时的,drawRect多次重绘需要手动调用setNeedsLayout;
- 4、性能方面,CAShapeLayer 使用了硬件加速,绘制同一图形会比用 Core Graphics 快很多,CAShapeLayer 属于 CoreAnimation 框架,动画渲染直接提交给手机GPU,而 Core Graphics会消耗大量的 CPU 资源。要知道在图像渲染方面,GPU 比 CPU 有很大的优势,毕竟 GPU 主要是的功能就是用于图像计算。
CPU 内部流水线结构拥有并行计算能力,一般用于显示内容的计算。而 GPU 的并行计算能力更强,能够通过计算将图形结果显示在屏幕像素中。内存中的图形数据,经过转换显示到屏幕上的这个过程,就是渲染。而负责执行这个过程的,就是 GPU。渲染的过程中,GPU 需要处理屏幕上的每一个像素点,并保证这些像素点的更新是流畅的,这就对 GPU 的并行计算能力要求非常高。
五十、SEL 和 IMP 的区别
每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址。
五十一、设计的6大原则
- a. 单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。避免一个类负责多个功能的实现,当发生更改时影响其他功能而致使复用成为不可能。
- b. 里氏替换原则:派生类(子类)对象能够替换其基类(父类)对象被调用。即在程序中,任何调用基类对象实现的功能,都可以调用派生类对象来替换。
- c. 依赖倒置原则:程序设计应该依赖抽象接口,而不应该依赖具体实现。即接口编程思想,接口是稳定的,实现是不稳定的,一旦接口确定,就不应该再进行修改了。根据接口的实现,是可以根据具体问题和情况,采用不同的手段去实现。
- d. 接口隔离原则:使用多个隔离接口,比使用单个接口要好。经常提到的降低耦合,降低依赖,主要也是通过这个原则来达到的。
- e. 迪米特法则:一个实体应当尽可能少的与其他实体之间发生相互作用。
- f. 开闭原则:程序的设计应该是不约束扩展,即扩展开放,但又不能修改已有功能,即修改关闭。
五十二、isKindOfClass 和 isMemberOfClass
isKindOfClass 和 [isMemberOfClass 内部实现逻辑。说明: object_getClass 方法如果传入实例对象则返回类对象;如果传入类对象则返回元类对象。
//object_getClass()取得的是对象的isa指针指向的对象,也就是判断传入的类对象的元类对象是否与传入的这个对象相等,所以这个cls应该是元类对象才有可能相等
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
//循环判断传入的类对象的元类对象及其父类的元类对象是否等于传入的cls
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
//判断传入的实例对象的类对象是否与传入的对象相等,所以cls只有可能是类对象才有可能相等
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
//循环判断实例对象的父类的类对象是否等于传入的对象cls,也就是判断实例对象是否是cls及其子类的一种
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
[[NSObject class] isKindOfClass:[NSObject class]];//1
[[Person class] isKindOfClass:[NSObject class]];//1
[[NSObject class] isMemberOfClass:[NSObject class]];//0
[[NSObject class] isMemberOfClass:object_getClass([NSObject class])];//1
[[Person class] isKindOfClass:[Person class]];//0
[[Person class] isKindOfClass:object_getClass([Person class])];//1
[[Person class] isMemberOfClass:[Person class]];//0
[[Person class] isMemberOfClass:object_getClass([Person class])];//1
按照 isKindOfClass 和 isMemberOfClass 实现逻辑,第一个和第二的打印结果让人费解。方法调用者是类对象,传入的参数也是类对象,理论有只有方法调用者为类对象,参数为元类对象时才返回 YES,其余情况为 NO。NSObject 能匹配成功,是因为 NSObject 元类(根元类)对象的 superClass 指针比较特殊,NSObject 元类的 superClass 指向 NSObject 的类对象,方法调用者和传入的参数都是 NSObject 类,所以最终会匹配成功。
扩展问题:下面代码打印结果
//类方法和实例方法声明和实现反过来不行,此种情况会崩溃
@interface NSObject (Test)
+(void)test;
@end
@implementation NSObject (Test)
-(void)test {
NSLog(@"test");
}
@end
请问调用 [NSObject test]; 什么结果?答案是输出 test 。主要原因在于:NSObject 元类的 superClass 指针指向 NSobject 类,方法调用的过程中如果当前类没有找到方法会尝试到父类中查找。虽然一个是类方法一个是对象方法,但方法名都是一致的。所以能正常调用。结论:NSObject 的类方法和实例方法是可以用 任意实例对象 或 任意类 随意调用的。如在ViewController类中调用上述分类 test 方法,结果也是一样的。[ViewController test];
五十三、成员变量和属性
1、成员变量是不与外界接触的变量,应用于类的内部,所以当用于类内部,属性为private时,就可以将变量定义为成员变量;
2、属性变量可以让其他对象访问,可以设置只读、只写等属性,当属性为public 时,定义属性在.h中;
五十四、通用缓存设计
一、磁盘+内存组合优化
- APP会优先请求内存缓冲中的资源。如果内存缓冲中有,则直接返回资源文件, 如果没有的话,则会请求资源文件,这时资源文件默认资源为本地磁盘存储,需要操作文件系统或数据库来获取。
- 获取到的资源文件,先缓存到内存缓存,方便以后不再重复获取,节省时间。然后就是从缓存中取到数据然后给app使用。
这样就充分结合两者特性,利用内存读取快特性减少读取数据时间。
二、磁盘优化
四种磁盘缓存方案:
- NSKeyedArchiver: 采用归档的形式来保存数据, 该数据对象需要遵守NSCoding协议, 并且该对象对应的类必须提供encodeWithCoder:和initWithCoder:方法。缺点:归档的形式来保存数据,只能一次性归档保存以及一次性解压。所以只能针对小量数据。如果想改动数据的某一小部分,需要解压整个数据或者归档整个数据。
- NSUserDefaults: 用来保存应用程序设置和属性、用户保存的数据。
- 写入文件方式:永久保存在磁盘中。
- SQLite:采用SQLite数据库来存储数据。
优化方案
- sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。
- 写入文件方式: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。
所以可以两者配合,灵活的存储以提高性能,YYCache 底层也是按照此种实现方式。inlineThreshold >= 20k 时使用写文件方式保存,否则使用 sqlite 方式保存。
三、内存优化
提高内存命中率(LRU)
LRU: 可以将链表看成一串数据链,每个数据是这个串上的一个节点,经常访问的数据移动到头部,等数据超出容量后从链表后面的一些节点销毁,这样经常访问数据在头部位置,还保留在内存中。
四、磁盘和内存读取线程安全
- OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
- dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
关于信号量:
@interface dispatch_semaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation dispatch_semaphoreDemo
- (instancetype)init{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)otherTest{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
- (void)test{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@end
- semaphore 叫做信号量。
- 信号量的初始值,可以用来控制线程并发访问的最大数量。
- 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。
五、YYCache 使用
YYCache *cache = [YYCache cacheWithName:@"ResponseCache"];
if ([cache containsObjectForKey:url] && networkErrow) { // 如果有缓存且网络有问题
id response = [cache objectForKey:url];
success(response);
return;
}
[[MNNetworkTool shareService] GET:url parameters:param progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if (mnnetSet.saveCache) { // 如果需要缓存,进行缓存
[cache setObject:dic forKey:url];
}
success(responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
failed();
}];
//清除缓存
YYCache *cache = [YYCache cacheWithName:@"ResponseCache"];
[cache removeAllObjects]; // 移除所有缓存
六、YYCache 细节问题
1、整体结构
2、磁盘读写问题
- 写入:YYKVStorage 属性 inlineThreshold >= 20k 时使用写文件方式保存,否则使用 sqlite 方式保存。
- 读取:在通过 key 查找某个缓存时,首先去数据库中找到 key 对应的记录,把去出来的数据转换成 YYKVStorageItem 类对象,判断 filename 字段是否为空,如果为空,说明数据存在数据空,则直接使用这个对象的 value 字段,如果不为空,则说明缓存实体数据存在系统文件中,通过 filename 去系统文件中找到对应文件,并赋值给这个对象的 value 字段。
五十五、@synchronized 锁
@synchronized 参数问题
@synchronized (self) 这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,self 对应的锁就释放了。这样做的优点是:不需要在代码中显式的创建锁对象,便可以实现锁的机制。但是,滥用 @synchronized (self) 则会降低代码效率,因为公用同一个锁的那些同步块,都必须按顺序执行。若是在 self 对象上频繁加锁,程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样效率就低了。
注:因为 @synchronized (self) 方法针对self只有一个锁,相当于对于 self 的所有用到同步块的地方都是公用同一个锁,所以如果有多个同步块,则其他的同步块都要等待当前同步块执行完毕才能继续执行。对于同一个类中要求出现多个锁的情况, @synchronized 中应该传入其它对象。
@synchronized 是递归锁
@synchronized 是对 mutex 递归锁的封装,@synchronized(obj) 内部会生成 obj 对应的递归锁,然后进行加锁、解锁操作。相比于互斥锁,递归锁需要多做一些额外操作,所以其开销会比互斥锁更大一些。
关于递归锁:
- (void)sellingTickets{
pthread_mutex_lock(&_ticketMutex);
[self sellingTickets2];
pthread_mutex_unlock(&_ticketMutex);
}
- (void)sellingTickets2{
pthread_mutex_lock(&_ticketMutex);
NSLog(@"%s",__func__);
pthread_mutex_unlock(&_ticketMutex);
}
上述代码 sellingTickets 和 sellingTickets2 共用了同一把锁,且 sellingTickets 调用了 sellingTickets2 方法,如果此锁只是一把普通的互斥锁,就会出现死锁,主要原因在于: 方法 sellingTickets 的结束需要 sellingTickets2 解锁,方法 sellingTickets2 的结束需要 sellingTickets 解锁,相互引用造成死锁。
但是上述代码中的 pthread_mutex 是递归锁,所以类似递归调用的场景不会出现死锁的情况。递归锁的定义是允许同一个线程对同一把锁进行重复加锁
。
五十六、RN 和原生通信原理
1、整体结构
从上图可知,在 RN 中 C++与JS部分的实现为多平台共用,在此基础上再分化为各平台实现。其中,iOS 平台特有的实现(Objective-C、C++)主要集中在 React 下,以C++语言实现的共有部分在 ReactCommon 下:
OC 和 JS 两端都保存了一份配置表,里面标记了所有 OC 暴露给 JS 的模块和方法。这样,无论是哪一方调用另一方的方法,实际上传递的数据只有 ModuleId、MethodId 和 Arguments。这三个元素,它们分别表示类、方法和方法参数,当 OC 接收到这三个值后,就可以通过 runtime 唯一确定要调用的是哪个函数,然后调用这个函数。
既然说到函数互调,那么就不得不提到回调了。对于 OC 来说,执行完 JS 再执行 Objective-C 回调毫无难度,难点依然在于 JS 代码调用 OC 之后,如何在 OC 的代码中,回调执行 JS 代码。目前的做法是:JS 调用 OC 时,JS 提前注册要回调的 Block,并且把 BlockId 作为参数发送给 OC,OC 收到参数时会创建 Block,调用完 OC 函数后就会执行这个刚刚创建的 Block。
2、JS 调 Native
通过 Apple 推出的 JavaScriptCore,要实现 JS to Native 的通信并非难事,主要有两条途径:
- block: 在 RN 这样复杂的应用场景中block显得有些力不从心。
- JSExport协议: 通过实现该协议可以向 JS 曝露 Native 接口(但不具备跨平台能力)。
因此,RN 并没有使用 JavaScriptCore提供的这两种方式,而是自己实现了一套通信机制。
JS 端通信部分的核心代码不到两百行,主要由 NativeModules.js、MessageQueue.js、BatchedBridge.js三个文件构成。BatchedBridge 对象提供了 JS 触发 Native 调用的方法:
var enqueueNativeCall = function(moduleID, methodID, params, onFail, onSuccess) {
if (onFail || onSuccess) {
// 如果存在 callback 回调,添加到 callbacks 字典中
// OC 根据 callbackID 来执行回调
if (onFail) {
params.push(this.callbackID);
this.callbacks[this.callbackID++] = onFail;
}
if (onSuccess) {
params.push(this.callbackID);
this.callbacks[this.callbackID++] = onSuccess;
}
}
// 将 Native 调用存入消息队列中
this.queue[MODULE_INDEX].push(moduleID);
this.queue[METHOD_INDEX].push(methodID);
this.queue[PARAMS].push(params);
// 每次都有 ID
this.callID++;
const now = new Date().getTime();
// 检测原生端是否为 global 添加过 nativeFlushQueueImmediate 方法
// 如果有这个方法,并且 5ms 内队列还有未处理的调用,就主动调用 nativeFlushQueueImmediate 触发 Native 调用
if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
global.nativeFlushQueueImmediate(this.queue);
// 调用后清空队列
this.queue = [[], [], [], this.callID];
}
}
该函数将调用 Native 实例模块 ID,方法 ID,以及回调 ID 分别存入三个队列中,this.queue 持有这三个队列。nativeFlushQueueImmediate 函数是关键,原生中提前中提前注册好 nativeFlushQueueImmediate 方法,js 执行 nativeFlushQueueImmediate 时会触发原生端 block 的调用,并传入参数 queue,触发 native 调用。。
self->_context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls) {
AHJSExecutor *strongSelf = weakSelf;
[strongSelf->_bridge handleBuffer:calls];
};
每个模块和方法,都会关联一个 ID,这个 ID 其实是模块和方法在各自列表中所处的下标。发起调用时,JS 端将 ID 存入消息队列,Native 拿到 ID,在 Native 端的配置表中,找到对应的原生类(实例)和方法,并进行调用。
对象和方法约定好了,还需要约定参数,将参数值按顺序放入一个数组中。使用者需要注意参数的个数和顺序,保持与 Native 端的方法匹配,否者会报错。
最后是 JS callback 的处理,JS 和 Native 通信是无法传递事件的,所以选择将事件序列化,给每个 callback 一个 ID,自己存一份,再将 ID 传给 Native,当 Native 要执行这个回调时,通过 invokeJSCallback 函数把这个 ID 回传给 JS,JS 再根据 ID 查找对应的 callback 并执行。
3、Native 调 JS
Native 调用 JS 依赖于 JavaScriptCore,该框架提供创建 JS 上下文环境,以及执行 JS 代码的接口,相对来说简单很多。
同步调用的场景非常少,因为它仅限于在 JS 线程调用,而实际情况是,Native 和 JS 的通信几乎都是跨线程的。因为页面刷新和事件回调都发生在主线程。对于 Native 端来说,JS 线程是普通的一个线程,跟其他线程没有区别,只不过是用这个线程来初始化 JS 的上下文环境,以及执行 JS 代码。
Native 异步调用 JS 主要由callFunctionReturnFlushedQueue函数分发,该函数定义在BatchedBridge对象,并由 global 的__batchedBridge属性所持有。
var callFunctionReturnFlushedQueue = function(module, method, args) {
this.__callFunction(module, method, args);
return this.flushedQueue();
}
为什么 JS 和 Native 的通信只能是异步?
本质原因是 JS 是单线程执行的。而 Native 端负责 UI 展示的又只能是主线程,跨线程通信如果使用同步会阻塞 UI。
为什么 JS 调 Native 要设计一个消息队列,等待 Native 调用时才执行,而不像 Native 调 JS 每次调用都直接去执行呢?
为了提高性能,批量处理 JS 的 Native 调用,可以减少 JS 与 Native 通信开销。
五十七、RN 和 Flutter
RN 和 Flutter 对比
RN
- JavaScript 的历史和流行程度都远超 Dart ,生态也更加完善,开发者也远多于 Dart 程序员。
- 从页面框架和自动化工具的角度来看,React Native 也要领先于 Flutter。这,主要得益于 Web 技术这么多年的积累,其工具链非常完善。
除了编程语言、页面框架和自动化工具以外,React Native 的表现就处处不如 Flutter 了。相比RN,Flutter 的优势最主要体现在性能、开发效率和体验这两大方面。
Flutter
- 性能:Flutter 却不一样。它一开始就抛弃了历史包袱,使用全新的 Dart 语言编写,同时支持 AOT 和 JIT 两种编译方式,而没有采用 HTML/CSS/JavaScript 组合方式开发,在执行效率上明显高于 JavaScriptCore 。
- UI 框架:它重写了 UI 框架,从 UI 控件到渲染,全部重新实现了,依赖 Skia 图形库和系统图形绘制相关的接口,保证了不同平台上能有相同的体验。
- 开发效率和体验:凭借热重载(Hot Reload)这种极速调试技术,极大地提升了开发效率,因此 Flutter 吸引了大量开发者的眼球。同时,Flutter 因为重新实现了 UI 框架,可以不依赖 iOS 和 Android 平台的原生控件,所以无需专门去处理平台差异,在开发体验上实现了真正的统一。
WebView渲染和 RN 渲染对比
对于第一类 WebView 的大前端渲染,主要工作在 WebKit 中完成。WebKit 的渲染层来自以前 macOS 的 Layer Rendering 架构,而 iOS 也是基于这一套架构。所以,从本质上来看,WebKit 和 iOS 原生渲染差别不大。
第二类的类 React Native 更简单,渲染直接走的是 iOS 原生的渲染。
为什么 WebView 比 RN 慢?
- 从第一次内容加载来看:大前端要比原生多出脚本代码解析的工作。WebView 需要额外解析 HTML + CSS + JavaScript 代码,而类 React Native 方案则需要解析 JSON + JavaScript。HTML + CSS 的复杂度要高于 JSON,所以解析起来会比 JSON 慢。也就是说,首次内容加载时, WebView 会比类 React Native 慢,且两者都比原生慢。
- WebView 的单独渲染进程还无法访问 GPU 的 context,这样两个进程就没有办法共享纹理资源。纹理资源无法直接使用 GPU 的 Context 光栅化,那就只能通过 IPC 传给 GPU 进程,这也就导致 GPU 无法发挥自身的性能优势。由于 WebView 的光栅化无法及时同步到 GPU,滑动时容易出现白屏,就很难避免了。
Flutter 原理
以往最早的Hybrid开发,主要依赖于WebView。但是WebView是一个很重的控件,很容易产生内存问题,而且复杂的UI在WebView上显示的性能不好。react-native技术抛开了WebView,利用JavaScriptCore来做桥接,将js调用转为native调用,只牺牲了小部分性能获取的跨平台开发,这是一大进步。所以现在react-native很流行的原因。
Flutter实现跨平台采用了更为彻底的方案。它既没有采用WebView也没有采用JavaScriptCore,而是自己实现了一台UI框架,然后直接系统更底层渲染系统上画UI。所以它采用的开发语言不是JS,而Dart。据称Dart语言可以编译成原生代码,直接跟原生通信。
https://blog.csdn.net/wolfking0608/article/details/80769567
五十八、AOT 和 JIT
JIT,即Just-in-time,动态(即时)编译,边运行边编译;AOT,Ahead Of Time,指运行前编译,是两种程序的编译方式。
JIT优点:
可以根据当前硬件情况实时编译生成最优机器指令(ps. AOT也可以做到,在用户使用是使用字节码根据机器情况在做一次编译)
可以根据当前程序的运行情况生成最优的机器指令序列
当程序需要支持动态链接时,只能使用JIT
可以根据进程中内存的实际情况调整代码,使内存能够更充分的利用
JIT缺点:
编译需要占用运行时资源,会导致进程卡顿
由于编译时间需要占用运行时间,对于某些代码的编译优化不能完全支持,需要在程序流畅和编译时间之间做权衡
在编译准备和识别频繁使用的方法需要占用时间,使得初始编译不能达到最高性能
AOT优点:
在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗
可以在程序运行初期就达到最高性能
可以显著的加快程序的启动
AOT缺点:
在程序运行前编译会使程序安装的时间增加
牺牲Java的一致性
将提前编译的内容保存会占用更多的外
五十九、离屏渲染
离屏渲染需要在屏幕外开辟内存空间,提前使用 CPU 渲染复杂的视图,保证视频控制器能够及时地从缓存区读到新的渲染结果。它在 GPU 面临性能瓶颈时,将压力转移一部分给比较空闲的 CPU,然而 CPU 的渲染能力远没有 GPU 高效,有点杀鸡出牛刀的意思。视频控制器要读取离屏渲染的结果,需要把渲染上下文从当前屏幕缓存区切到屏幕外缓存区,当要显示非离屏渲染视图的时候又要切换回来,然而不可能在一屏上所有的元素都是离屏渲染的,所以视频控制器上下文需要不停地来回切换。而这种上下文切换的代价非常昂贵。
离屏渲染并不是一无是处的,虽然会造成很多额外的开销,但也是为了充分利用设备的资源来保证界面的流畅。发生离屏渲染时,是为了引起开发者对性能的关注,减少不必要的透明视图层级。如果不可避免的要触发离屏渲染,并且发生离屏渲染视图内容不会频繁的变化,可以利用 CALayer.shouldRasterize 开启光栅化,将离屏渲染的内容以位图的形式缓存,减少复杂视图频繁渲染的开销。然而,这个缓存的时效是 100ms,也就是刷新 6 帧的时间,如果视图内容更新频繁,缓存就会不停的刷新,导致无法命中,开启光栅化并没有什么作用。
重点参照此文,此文了解后基本都完全了解了
重点参考此文
https://www.jianshu.com/p/24dac847cfc4
https://zhuanlan.zhihu.com/p/72653360
经典::::::::::::::
https://mp.weixin.qq.com/s/RFIiwvEhdSBub7HFm3C1Ug
面试必考题:为什么圆角和裁剪后iOS绘制会触发离屏渲染? 答:默认情况下每个视图都是完全独立绘制渲染的。而当某个父视图设置了圆角和裁剪并且又有子视图时,父视图只会对自身进行裁剪绘制和渲染。当子视图绘制时就要考虑被父视图裁剪部分的绘制渲染处理,因此需要反复递归回溯和拷贝父视图的渲染上下文和裁剪信息,再和子视图做合并处理,以便完成最终的裁剪效果。这样势必产生大量的时间和内存的开销。解决的方法是当父视图被裁剪和有圆角并且有子视图时,就单独的开辟一块绘制上下文,把自身和所有子视图的内容都统一绘制在这个上下文中,这样子视图也不需要再单独绘制了,所有裁剪都会统一处理。当父视图绘制完成时再将开辟的缓冲上下文拷贝到屏幕上下文中去。这个过程就是离屏渲染!!所以离屏渲染其实和我们先将内容绘制在位图内存上下文然后再统一拷贝到屏幕上下文中的双缓存技术是非常相似的。使用离屏渲染主要因为iOS内部的视图独立绘制技术所导致的一些缺陷而不得不才用的技术。
- shadow 离屏渲染: 其原因在于,虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方(这是重点),因此根据画家算法必须被渲染在先。但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去
- mask 离屏渲染:mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。
小结:group opacity、mask、圆角裁剪都是运用在所有子视图上,因此产生离屏渲染的原因类似。shadow其实也需要保持在所有layer的下方,因此和圆角裁剪类似,都要所有资源绘制完成后再做最后一步处理。
六十、NSString用copy还是strong修饰?
- 当我们十分确定,要给属性NSString赋一个不可变的值时,用strong。如果使用copy来修饰属性,在进行赋值的时候,会先做一个类型判断,如果赋的值是一个不可变的字符串,则走strong的策略,进行的是浅拷贝;如果是可变的字符串,则进行深拷贝创建一个新的对象。所以如果我们确定是给属性赋值一个不可变的值,就不用copy再多去判断一遍类型,因为如果是很多的NSString属性需要赋值,会极大的增加系统开销。所以用strong在此情况下可以提升性能。
- 但是很多情况下我们并不能确定要赋的值是什么类型的,所以我们还是使用copy来修饰,这样保证了安全性。因为如果赋值的可变的字符串,当它发生变化时,用strong修饰的属性的值也会跟着变化;而copy修饰的属性,则因为是深拷贝而不会变化。
六十一、动态链接库和静态链接库
- 静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。
- 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。
在 iOS 8 之前,iOS 平台不支持开发者使用用户自己的动态 Framework,appstore不能上架,因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码。但是,iOS 8/Xcode 6 推出之后,因为 Extension 和 App 是两个分开的可执行文件,同时需要共享代码,iOS添加了对动态库的支持。
六十二、引用计数加减
谁创建谁release ,谁retain谁release。需要注意的是:release并不代表销毁\回收对象,仅仅是计数器 -1。
- 如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么你必须调用release或autorelease。
- 只要你调用了retain,就必须调用一次release。
一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用
六十三、OC 的动态性体现
动态类型
动态类型是跟静态类型相对的。像内置的明确的基本类型都属于静态类型(int、NSString等)。静态类型在编译的时候就能被识别出来。所以,若程序发生了类型不对应,编译器就会发出警告。而动态类型就编译器编译的时候是不能被识别的,要等到运行时(run time),即程序运行的时候才会根据语境来识别。
动态绑定
OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法。最终形成的这个SEL变量就代表一个方法的引用。SEL变量只是一个整数,他是该方法的ID。动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。
动态加载
根据需求加载所需要的资源,这个很容易理解;做个最简单的说明。因为 Retina 屏幕的出现, 图片经常要准备 @2x ,@3x,但是我们在使用的时候,并没有代码要求使用 2倍 3倍图,系统会动态自动完成加载。
六十四、强类型和弱类型语言
强类型语言
一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。强类型定义语言是类型安全的语言。
弱类型语言
数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。像vb,php,js 等就属于弱类型语言
六十五、UIWebView 和 WKWebView 的区别
在 iOS8 之后,使用 WKWebView 来取代 UIWebView,之后上架的应用不能使用 UIWebView。
- WKWebView 加载速度更快。
- WKWebView 消耗内存更小。同样加载一个百度首页,UIWebView 占用 35M 左右,WKWebView 仅10M 左右。
六十六、ARC 上可能出现野指针的情况
- 属性通过 assign 修饰。当对象释放后,属性指针去访问会产生野指针,因为不会像 weak 那样,对象释放后自动把属性指针置为 nil。
- C++ 或 C 的内存操作
六十七、NSHashTable(NSMutableArray) 和 NSMapTable(NSMutableDictory)
@interface ViewController ()
@property(nonatomic,strong)NSMutableArray *mArr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.mArr = [NSMutableArray array];
[self.mArr addObject:self];
}
上述代码会出现内存泄漏的情况,主要原因在于 mArr 内部强持有控制器。控制器又强持有 mArr。
NSHashTable
NSHashTable 类似 Foundation 框架中的 NSMutableArray,初始化时可以指定引用策略。即被添加的对象可以被 NSHashTable 强持有,也可以弱持有。
NSHashTable *hashTable = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
[hashTable addObject:human];
NSMapTable
NSMapTable 类似 Foundation 框架中的 NSMutableDictory。用法同 NSHashTable 类似。
六十八、UITableView 多图下载
实际用 SDWebImage 就可以实现,但是也可自己实现,主要考擦思路。
https://www.jianshu.com/p/32dd2c605d95
六十九、井盖为什么是圆的
- 圆的受力更均匀不容易碎裂和塌陷
- 圆形井盖从任何方向都不会掉落井下,也方便搬运和操作
- 相对节省生成材料成本
七十、DNS(Domain Name System) 解析过程
- 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址
- 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
- 权威 DNS 服务器 :返回相应主机的 IP 地址
- 1、电脑客户端会发出一个 DNS 请求,问 www.163.com 的 IP 是啥啊,并发给本地域名服务器 (本地 DNS)。那本地域名服务器 (本地 DNS) 是什么呢?如果是通过 DHCP 配置,本地 DNS 由你的网络服务商(ISP),如电信、移动等自动分配,它通常就在你网络服务商的某个机房。
- 2、本地 DNS 收到来自客户端的请求。你可以想象这台服务器上缓存了一张域名与之对应 IP 地址的大表格。如果能找到 www.163.com,它就直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大,能告诉我 www.163.com 的 IP 地址吗?”根域名服务器是最高层次的,全球共有 13 套。它不直接用于域名解析,但能指明一条道路。
- 3、根 DNS 收到来自本地 DNS 的请求,发现后缀是 .com,说:“哦,www.163.com 啊,这个域名是由.com 区域管理,我给你它的顶级域名服务器的地址,你去问问它吧。
- 4、”本地 DNS 转向问顶级域名服务器:“老二,你能告诉我 www.163.com 的 IP 地址吗?
-5、”顶级域名服务器就是大名鼎鼎的比如 .com、.net、 .org 这些一级域名,它负责管理二级域名,比如 163.com,所以它能提供一条更清晰的方向。顶级域名服务器说:“我给你负责 www.163.com 区域的权威 DNS 服务器的地址,你去问它应该能问到。 - 6、”本地 DNS 转向问权威 DNS 服务器:“您好,www.163.com 对应的 IP 是啥呀?”163.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
- 7、权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
- 8、本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。
七十一、DNS 劫持
1、DNS劫持和HTTP 劫持概念
2、如何防止DNS 劫持
- 方案一:简单粗暴投诉运营商。因为黑产与各地运营商的一些“合作”会导致 DNS 劫持。
- 方案二:HTTP 场景 IP 直连。通过IP直接访问网站,可以解决 DNS 劫持问题。
- 方案三:使用基于 HTTP 的 DNS 解析方案,绕过运营商直接连可以信任的第三方服务
对于服务器IP经常变的情况,可能需要使用第三方服务,比如DNSPod、httpDNS。默认的 DNS 是基于 UDP,改用 HTTP 协议进行域名解析,代替现有基于 UDP 的 DNS 协议,域名解析请求直接发送到指定的第三方 DNS 解析服务器,从而绕过运营商的 Local DNS,能够避免 Local DNS 造成的域名劫持问题和调度不精准问题。为了防止可信任的第三方服务挂掉,可以将运营商作为兜底方案。
七十二、URL 和 URI 的区别
- URI = Universal Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。
- URL = Universal Resource Locator 统一资源定位符,一种定位资源的主要访问制的字符串,一个标准的URL必须包括:protocol、host、port、path、parameter、anchor。
- URN = Universal Resource Name 统一资源名称,通过特定命名空间中的唯一名称或ID来标识资源。
URL 和 URL 区别
URI 包含 URL,URL是一个完整的URI,可在全网唯一标识。如:http://localhost:8080/hellow/login.html 和 /hellow/login.html 都是 URI,但只有前者可在全网唯一标识,后者只能在特定环境唯一标识,所以前者可以被称为URL或URI,而后者只能称为URI。
URL 组成
- 1.协议部分:该URL的协议部分为“http:”,这代表网页使用的是HTTP协议。在Internet中可以使用多种协议,如HTTP,FTP等等本例中使用的是HTTP协议。在"HTTP"后面的“//”为分隔符
- 2.域名部分:该URL的域名部分为“www.aspxfans.com”。一个URL中,也可以使用IP地址作为域名使用
- 3.端口部分:跟在域名后面的是端口,域名和端口之间使用“:”作为分隔符。端口不是URL必须的部分,如果省略端口部分,将采用默认端口
- 4.虚拟目录部分:从域名后的第一个“/”开始到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是一个URL必须的部分。本例中的虚拟目录是“/news/”
- 5.文件名部分:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。本例中的文件名是“index.asp”。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名
URI 属于 URL 更高层次的抽象,一种字符串文本标准。
就是说,URI 属于父类,而 URL 属于 URI 的子类。URL 是 URI 的一个子集。二者的区别在于,URI 表示请求服务器的路径,定义这么一个资源。而 URL 同时说明要如何访问这个资源(http://)。
.七十三、a 和 frameWork 区别
- .a是一个纯二进制文件,.framework中除了有二进制文件之外还有资源文件。
- .a文件不能直接使用,至少要有.h文件配合,.framework文件可以直接使用。
- .a + .h + sourceFile = .framework。
提示:组件化过程中可以将各组件库,导出为 .a 文件,减少项目编译时间。
七十四、数据库知识
关系数据库一对多和多对多
参考
七十五、IM SDK
传输协议 UDP 和 TCP 的选择
第三方服务商IM底层协议基本上都是TCP。CocoaAsyncSocket:
这个框架实现了两种传输协议TCP和UDP,分别对应GCDAsyncSocket类和GCDAsyncUdpSocket。
对于小公司或者技术不那么成熟的公司,IM一定要用TCP来实现,因为如果你要用UDP的话,需要做的事太多。当然QQ就是用的UDP协议,当然不仅仅是UDP,腾讯还用了自己的私有协议,来保证了传输的可靠性,杜绝了UDP下各种数据丢包,乱序等等一系列问题。
总之一句话,如果你觉得团队技术很成熟,那么你用UDP也行,否则还是用TCP为好。
关于传输格式的选择
推荐使用 Protobuf ,相比 JOSN、XML 具有如下优点:
- 省流量: 流量消耗极少,省流量。一条消息数据用Protobuf序列化后的大小是 JSON 的1/10、XML格式的1/20、是二进制序列化的1/10。
- 高效存储: 同 XML 相比, Protobuf 性能优势明显。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。
- 高效:提高网络请求成功率,消息体越大,失败几率随之增加。
- 可靠: 微信和手机 QQ 这样的主流 IM 应用也早已在使用它(采用的是改造过的Protobuf协议)
- 易于使用:开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的模型类,可以支持java、c++、python、Objective-C等语言环境。
心跳检测
心跳就是用来检测TCP连接的双方是否可用。那又会有人要问了,TCP不是本身就自带一个KeepAlive机制吗?这里我们需要说明的是TCP的 KeepAlive 机制只能保证连接的存在,但是并不能保证客户端以及服务端的可用性。
- 客户端发起心跳 Ping(一般都是客户端),假如设置在10秒后如果没有收到回调,那么说明服务器或者客户端某一方出现问题,这时候我们需要主动断开连接。
- 服务端也是一样,会维护一个socket的心跳间隔,当约定时间内,没有收到客户端发来的心跳,我们会知道该连接已经失效,然后主动断开连接。
断线重连
理论上,我们自己主动去断开的Scoket连接(例如退出账号,APP退出到后台等等),不需要重连。其他的连接断开,我们都需要进行断线重连。一般解决方案是尝试重连几次,如果仍旧无法重连成功,那么不再进行重连。可参考这种方案:采用2的指数级别增长,第一次断线立刻重连,第二次2秒,第三次4秒,第四次8秒...直到大于64秒就不再重连。而任意的一次成功的连接,都会重置这个重连时间。避免快速或无限制的重连。
TCP和 UDP 粘包和断包问题
粘包概念及原因
由于数据传输过程中为数据流,经过TCP传输后,三条数据被合并成了一条,这就是数据粘包问题。TCP中会将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这么做的目的是为了减少广域网的小分组数目,从而减小网络拥塞的出现。UDP不会使用块的合并优化算法。但是 UDP 也会出现粘包问题。
除了数据块合并会导致粘包问题,下面两种情况都会造成TCP和UDP粘包:
- 发送端需要等缓冲区满才发送出去,造成粘包
- 接收方不及时接收缓冲区的包,造成多个包接收。
断包解决方案
- 1、封包的时候给每个包的数据最前面加一个标记,来标明数据的长度和类型(类型显然是需要的,我们需要知道它是文本、图片、还是录音等等,来用正确的方式处理这个数据)。
- 2、拆包的时候,先获取到我们给每个包的标记,然后根据标记的数据长度,去获取数据。最后再根据标记的类型去处理数据。
七十六、去除实际业务中数组中重复元素
七十七、ABTest SDK
七十八、YYCache
https://blog.ibireme.com/2015/10/26/yycache/
https://www.jianshu.com/p/b592ee20f09a
七十九、 YYModel
1、整体结构
2、Type Encodings
3、有很多逻辑是调用底层 runtime API 实现
八十、ASyncDisplayKit
https://zhuanlan.zhihu.com/p/25371361
八十一、AFNetworking 2.0 和 3.0
- 1、 2.0 使用 NSURLConnection 的基础API ,3.0 使用 NSURLSession 的API。
- 2、2.0 需要开辟常驻线程,3.0 不需要。
AFN 2.0 的做法是把网络请求的发起和解析都放在同一个子线程(假设为A)中进行,由于子线程默认不开启 runloop,它会向一个 C 语言程序那样在运行完所有代码后退出线程。AFN 中为避免阻塞线程,网络请求是异步发送的,这会导致获取到请求数据时,A 线程已经退出,代理方法没有机会执行。从另外一方面考虑,如果每个网络请求都开辟一个子线程去同步发送请求处理,此时的开销会很大。因此,综合上述两点,AFN 的做法是使用 runloop 保证线程不死。
AFN 3.0 中, 由于NSURLSession不需要再当前线程等待网络请求得回调结果,所以说在AFN中就不需要常驻线程等待回调,而是交给NSOperationQueue来处理,让系统自己来决定是否需要开启新的线程。并且设置maxConcurrentOperationCount为1,让回调串行执行。 - 3、3.0 中 NSURLSession 提升了网络连接速度
事实上在HTTP/0.9 ,HTTP/1.0协议的时代,每次HTTP的请求,都需要先经过TCP三次握手连接,而后才能开始HTTP的请求。是在 HTTP/1.1 中共享的 Session 会复用 TCP 的连接,这样就避免了每次操作都开启一个 TCP 三次握手的时间浪费,即提升了网络连接速度。
https://www.jianshu.com/p/5572238f2039
八十二、OC 和 C 对比
- 数据类型:OC 和 C 都有基本数据类型、结构体、指针、空类型 void。但是OC 比 C 多的类型有 Block、@Selector 等。
- 面向对象(封装、继承、多态)和面向过程:OC 中有类和对象的概念
- OC 具有动态性 (运行时)
- OC中属性、协议
- OC 比 C 多有 try...catch...finally
https://www.cnblogs.com/xufengyuan/p/6476831.html
八十三、 对OC 的理解
1、OC作为一门面向对象的语言,自然具有面向对象的语言特性,如:封装、多态、继承。
2、它具有静态语言的特性,又有动态语言的效率。
3、动态性主要体现在三个方面:动态类型、动态绑定、动态加载。之所以叫做动态,是因为必须到运行时才会做一些事情。
- 1、 动态类型:即运行时再决定对象的类型。
- 2、动态绑定 :基于动态类型,在某个实例对象被确定后,其类型就被确定了。该对象的属性和响应的消息也被完全确定,这就是动态绑定。
- 3、动态加载:根据需求加载所需要的资源和代码。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。
https://blog.csdn.net/gogosmilex/article/details/48624181
八十四、数组和链表区别
- 连续和不连续
- 查找的时间复杂度对比(链表可以改造为跳表,查找效率也比较高,数组最主要是支持下标访问)
- 删除和插入时间复杂度对比
- 链表因为需要维护指针,占用的内存空间可能更大
八十五、线程最大并发数实现方法(GCD)
NSOperationQueue 中一个属性可以很方便的设置线程最大并发数。GCD 中可以借助型号量来设置最大线程并发数。
- 信号量是一个整型值,有初始计数值(大于0);可以接收通知信号和等待信号。
- 当信号量收到通知信号时,计数+1;
- 当信号量收到等待信号时,计数-1;
- 如果信号量为0,线程会被阻塞(此时不会消耗 CPU),直到信号量大于0,才会继续下去。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_async(workConcurrentQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"1");
sleep(5);
dispatch_semaphore_signal(semaphore);
});
dispatch_async(workConcurrentQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"2");
sleep(3);
dispatch_semaphore_signal(semaphore);
});
dispatch_async(workConcurrentQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"3");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(workConcurrentQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"4");
dispatch_semaphore_signal(semaphore);
});
理解线程和 CPU 的关系,为什么要控制线程最大并发数?
- 在GCD中有两种队列,分别是串行队列和并发队列。在串行队列中,同一时间只有一个任务在执行,不能充分利用多核 CPU 的资源,效率较低。
- 并发队列可以分配多个线程,同时处理不同的任务;效率虽然提升了,但是多线程的并发是用时间片轮转方法实现的,线程创建、销毁、上下文切换等会消耗CPU 资源。
- 目前iPhone的处理器是多核(2个、4个),适当的并发可以提高效率,但是无节制地并发,如将大量任务不加思索就用并发队列来执行,这只会大量增加线程数,抢占CPU资源,甚至会挤占掉主线程的 CPU 资源(极端情况)。
https://blog.csdn.net/shihuboke/article/details/76736743
八十六、 多个网络请求完成后执行下一步
信号量实现
NSString *str = @"http://www.jianshu.com/p/6930f335adba";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%d---%d",i,i);
count++;
if (count==10) {
//10个请求都回来后再放开线程
dispatch_semaphore_signal(sem);
count = 0;
}
}];
[task resume];
}
//先卡主线程
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"end");
});
NSMutableArray *imageURLs= [NSMutableArray array];
dispatch_group_t group = dispatch_group_create(); // 1
for (UIImage *image in images) {
dispatch_group_enter(group); // 2
sendPhoto(image, success:^(NSString *url) {
[imageURLs addObject:url];
dispatch_group_leave(group); // 3
});
}
dispatch_group_notify(group, dispatch_get_global_queue(), ^{ // 4
postFeed(imageURLs, text);
});
上面这端代码可以理解为发朋友圈圈时上传多张图片的逻辑,即图片上传成功之后再去真正的发朋友圈。创建一个dispatch_group_t, 每次上传图片前先dispatch_group_enter,请求回调后再dispatch_group_leave,对于enter和leave必须配合使用,有几次enter就要有几次leave,否则group会一直存在。当所有enter的block都leave后,会执行dispatch_group_notify的block。
https://www.jianshu.com/p/e93fd15d93d3
https://www.jianshu.com/p/fb4fb80aefb8
https://www.cnblogs.com/bbqzsl/p/5287970.html
八十七、项目中分类同名怎么处理
1、避免措施
不同业务组分类命名以及分类方法命名添加在业务前缀。
2、出现此种情况如何检测
可以通过写一些检测工具去检测,参照下图和下文。关键点是使用 class_copyMethodList
方法
https://tech.meituan.com/2015/03/03/diveintocategory.html
八十八、按钮点击防抖处理
https://juejin.im/post/5b6be86d6fb9a04fbb114e02
八十九、通知实现原理
观察者模式是定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会收到通知并自动更新。NSNotificationCenter 是使用观察者模式来实现的用于跨层传递消息。
https://www.jianshu.com/p/364d39651c2a
如果添加通知的时候传入了 object 参数,那么发送通知时,就会匹配 通知名 和 object 两个条件。如果没传 object 参数,则只匹配 通知名。
九十、instance和id的区别
相同点:
- 都可以表示任何对象类型;
不同点: - id 可以用作返回值和参数;instance 只能用作返回值类型;
- ARC 下编译器会检测 instance 对象的真实类型,不符合类型会报错; MRC下 instancetype和 id 都不做具体类型检查。
九十一、指出如下代码不合理的地方
参照孙源面试题
九十二、流量监控
九十三、import 和 include
参考
启动优化 https://www.jianshu.com/p/f40fdd8799b8
九十四、为什么有类对象和元类对象
http://www.zyiz.net/tech/detail-111585.html
http://www.leewong.cn/2018/05/02/why-metaclass/
我的理解并不是先有MetaClass,向对象发送消息的基本解决方案是统一的,希望复用的。而实例和类之间用的这一套通过isa指针指向的Class单例中存储方法列表和查询方法的解决方案的流程,是应该在类上复用的,而MetaClass就顺理成章出现罢了。
九十五、垃圾回收和自动引用计数
垃圾回收
垃圾回收技术主要使用在.NET 和 Java 平台,在两个平台(Java Runtime 和 .NET Common Language Runtime)的上都有一套机制在后台检测不用的对象和对象图谱。
垃圾回收运用到间隔周期不是固定的,有可能是系统检测到内存占用较低,也可能是上次运行过了一段固定时间等,这样就造成了那些不再使用的对象被释放的时间难以确定。
优势
垃圾收集可以清理所有不被使用的对象图谱,包括循环引用。
垃圾回收运行在后台,可以作为定期应用程序流程运行。劣势
对象释放的时间范围是难以确定的。
当垃圾回收运行时,可能导致系统资源紧张,需暂停一些优先级较低的线程。
自动引用计数
自动引用计数主要体现在Cocoa平台,不像垃圾回收是在后台检测并回收不用的对象,编译器会自动注入可执行代码来跟踪引用计数,并在需要的时候释放对象。如果你尝试反编译支持ARC的可执行文件,看起来就像开发者花很多时间写代码处理对象生命周期一样, 其实这些繁重的工作都是编辑器完成的。
优势
实时地,精确地释放那些变成没用的对象。
没有后台处理,在低功耗的系统上更有效率,如手机设备。劣势
不能处理循环引用。
九十六、串行队列
串行队列内任务依次执行,上一个任务执行完毕,线程才能取下一个任务执行。
- (void)viewDidLoad {
//打印结果 1111, async,sync,2222
//此处即使改为串行队列,1111,sync,2222的顺序也是不变的,因为sync不开启新线程
dispatch_queue_t q = dispatch_queue_create("CC_queue",NULL);
dispatch_async(q, ^{
NSLog(@"async-%@",[NSThread currentThread]);//开启新线程,子线程
//此处代码会导致下面 dispatch_sync 和 2222卡顿,1111不会卡顿
//dispatch_sync 卡顿是因为串行队列内任务是依次执行的,async内任务一直没执行完,sync内任务一直无法取出执行。(串行队列可以做线程安全处理)
//2222 卡顿是因为 sync不开新线程,仍然在主线程执行,sync一直不执行导致后续2222被阻塞
// 1111 之所以不会卡顿是因为async在子线程执行
sleep(10);
});
NSLog(@"1111");
dispatch_sync(q,^{
NSLog(@"sync-%@",[NSThread currentThread]);//不开新线程,主线程执行
});
NSLog(@"2222");
}
-(void)gcdDemo2
{
//1.队列-串行
/*
参数1:队列名称
参数2:队列属性 DISPATCH_QUEUE_SERIAL 串行,等价于NULL
*/
dispatch_queue_t q = dispatch_queue_create("CC_queue",NULL);
//2.异步执行任务 async
for(int i = 0;i < 10;i++)
{
dispatch_async(q,^{
NSLog(@"%@",[NSThread currentThread]);
});
}
//它在主线程
NSLog(@"come here");
}
1.开启几条线程吗?
开启1条线程
2.顺序执行?
顺序执行,只有1个线程(打印线程始终为同一对象,并非上一线程销毁,然后重新创建一条线程),任务是按照队列顺序来的,所以是顺序执行。
只要是异步就可以获取多个线程,但是串行队列,任务没有完成,不能拿任务,所以只会获取1个线程。
3.come here什么时候执行?
一上来就执行!有可能有交替的,插在中间。子线程和主线程谁先执行任务,是不能确定的。因为这是CPU调度的。come Here在主线程
九十七、convertPoint: 与 convertRect:
1.convertRect的使用
a.
[A convertRect:B.frame toView:C];
计算A上的B视图在C中的位置CGRect
b.
[A convertRect:B.frame fromView:C];
计算C上的B视图在A中的位置CGRect
2.convertPoint的使用
a.
[A convertPoint:B.center toView:C];
计算A上的B视图在C中的位置CGPoint
b.
[A convertPoint:B.center fromView:C];
计算C上的B视图在A中的位置CGPoint
九十八、Lottie实现原理
iOS里的动画根本都是基于CoreAnimation里的API实现的,Lottie也是如此。在AE上实现动画成果,通过插件导出对应的json文件,Lottie的库解析该json,转成对应的零碎API办法。图片的援用能够应用Base64编到json里,也能够通过我的项目集成,通过门路援用。
参考
九十九、category和extension
Category
- category只能给某个已有的类扩充方法,不能扩充成员变量
category中也可以添加属性,只不过@property只会生成setter和getter的声明,不会生成setter和getter的实现以及成员变量。 - 如果category中的方法和类中原有方法同名,运行时会优先调用category中的方法。也就是,category中的方法会覆盖掉类中原有的方法。所以开发中尽量保证不要让分类中的方法和原有类中的方法名相同。避免出现这种情况的解决方案是给分类的方法名统一添加前缀。比如category_
Category加载过程
- 通过Runtime加载某个类的所有Category数据
把所有Category的方法、属性、协议数据,合并到一个大数组中
后面参与编译的Category数据,会在数组的前面
将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
Extension
- extension被开发者称之为扩展、延展、匿名分类。extension看起来很像一个匿名的category,但是extension和category几乎完全是两个东西。和category不同的是extension不但可以声明方法,还可以声明属性、成员变量。extension一般用于声明私有方法,私有属性,私有成员变量。
- extension在编译期加载到类,它就是类的一部分,但是category则完全不一样,它是在运行期加载到类。extension在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它、extension伴随类的产生而产生,亦随之一起消亡。
Category的实现原理?
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
Category和Class Extension的区别是什么?
Class Extension在编译的时候,它的数据就已经包含在类信息中,Category是在运行时,才会将数据合并到类信息中
Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现
添加关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
获取关联对象
id objc_getAssociatedObject(id object, const void * key)
移除所有关联对象
void objc_removeAssociatedObjects(id object)
关联对象并不是存储在被关联对象本身内存中
关联对象存储在全局的统一的一个AssociationsManager中