| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> iOS 面试题 -> 正文阅读 |
|
[移动开发]iOS 面试题 |
RunLoop1、什么是
|
线程类型 | 对比 | 备注 |
---|---|---|
NSThread | 跨平台C语言标准库中的多线程框架 | 过于底层使用很麻烦,需要封装使用. |
NSOperation / NSOperationQueue | 更加面向对象 可以设置并发数量 | GCD 的封装 |
GCD(Grand Central Dispatch) | iOS5后苹果推出的双核CPU优化的多线程框架,对A5以后的CPU有很多底层优化,C函数的形式调用 有点面向过程,不能直接设置并发数,需要写一些代码曲线方式实现并发 | 推荐使用 |
3中队列:主线程队列、并发队列、串行队列
在GCD中有两种队列:串行队列
和并发队列
。两者都符合 FIFO
的原则,二者的主要区别是:执行的顺序不同和开启的线程数不同。
主线程队列: main queue
可以调用dispatch_get_main_queue()
来获得。因为main queue
是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
串行队列(Serial Dispatch Queue):
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只能开启一个线程,一个线程执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为程序创建的。
并行队列(Concurrent Dispatch Queue):
同时允许多个任务同时执行。(可以开启多个线程,并且同时执行)。并发队列的并发功能只有在异步(dispatch_async) 函数下才有效。
GCD
有哪些方法 api
?Dispatch Queue :
开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。
dispatch_async { queue, ^{
//想执行的任务
});
通过 dispatch_async 函数“追加”赋值在变量 queue 的“Dispatch Queue中”。
Dispatch Queue 的种类:
有两种Dispatch Queue,一种是等待现在执行中处理的 Serial Dispatch Queue
,另一种是不等待现在执行中处理的 Concurrent Dispatch Queue
。
dispatch_queue_create :
创建队列
Main Dispatch Queue 和 Global Dispatch Queue :
系统提供的两种队列
dispatch_set_target_queue :
变更队列执行的优先级
dispatch_after :
延时执行。
注意
的是dispatch_after
函数并不是在指定时间后执行处理,而只是在指定时间追加处理到 Dispatch Queue
。
dispatch_group :
调度任务组。
dispatch_group_notify
:最后任务执行完的通知,比如:
- (void)dispatch_group {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT , 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"thread1:%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"thread2:%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"thread3:%@", [NSThread currentThread]);
});
// 三个异步执行结束后,dispatch_group_notify 得到通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 4
NSLog(@"completed:%@", [NSThread currentThread]);
});
}
dispatch_group_wait
:
dispatch_group_wait
实际上会使当前的线程处于等待的状态,也就是说如果是在主线程执行dispatch_group_wait
,在上面的block
执行完之前,主线程会处于卡死的状态。可以注意到dispatch_group_wait
的第二个参数是指定超时的时间,如果指定为DISPATCH_TIME_FOREVER
(如上面这个例子)则表示会永久等待,直到上面的Block
全部执行完。除此之外,还可以指定为具体的等待时间,根据dispatch_group_wait
的返回值来判断是上面block
执行完了还是等待超时了。
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let group = dispatch_group_create()
dispatch_group_async(group, globalQueue) { () -> Void in
println("1")
}
dispatch_group_async(group, globalQueue) { () -> Void in
println("2")
}
dispatch_group_async(group, globalQueue) { () -> Void in
println("3")
}
//使用dispatch_group_wait函数
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
println("completed")
}
dispatch_barrier_async
:
dispatch_barrier_async
就如同它的名字一样,在队列执行的任务中增加“栅栏”,在增加“栅栏”之前已经开始执行的block
将会继续执行,当dispatch_barrier_async
开始执行的时候其他的block
处于等待状态,dispatch_barrier_async
的任务执行完后,其后的block
才会执行。
dispatch_sync 和 dispatch_async
dispatch_sync : 把任务Block
同步追加到指定的Dispatch Queue
中
dispatch_async :把任务Block
异步追加到指定的Dispatch Queue
中
dispatch_apply
dispatch_apply
会将一个指定的block
执行指定的次数。如果要对某个数组中的所有元素执行同样的block
的时候,这个函数就显得很有用了,用法很简单,指定执行的次数以及Dispatch Queue
,在block
回调中会带一个索引,然后就可以根据这个索引来判断当前是对哪个元素进行操作:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_apply(10, globalQueue) { (index) -> Void in
print(index)
}
print("completed")
}
由于是Concurrent Dispatch Queue
,不能保证哪个索引的元素是先执行的,但是“completed
”一定是在最后打印,因为dispatch_apply
函数是同步的,执行过程中会使线程在此处等待,所以一般的,我们应该在一个异步线程
里使用dispatch_apply
函数:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(globalQueue, { () -> Void in
dispatch_apply(10, globalQueue) { (index) -> Void in
print(index)
}
print("completed")
})
print("在dispatch_apply之前")
}
dispatch_suspend / dispatch_resume
某些情况下,我们可能会想让Dispatch Queue
暂时停止一下,然后在某个时刻恢复处理,这时就可以使用dispatch_suspend
以及dispatch_resume
函数:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
//暂停
dispatch_suspend(globalQueue)
//恢复
dispatch_resume(globalQueue)
}
注意:
暂停时,如果已经有block
正在执行,那么不会对该block
的执行产生影响。dispatch_suspend
只会对还未开始执行的block
产生影响。
Dispatch Semaphore
信号量在多线程开发中被广泛使用,当一个线程在进入一段关键代码之前,线程必须获取一个信号量,一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待前面的线程释放信号量。
信号量的具体做法是:当信号计数大于0时,每条进来的线程使计数减1,直到变为0,变为0后其他的线程将进不来,处于等待状态;执行完任务的线程释放信号,使计数加1,如此循环下去。
下面这个例子中使用了10条线程,但是同时只执行一条,其他的线程处于等待状态:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let semaphore = dispatch_semaphore_create(1)
for i in 0 ... 9 {
dispatch_async(globalQueue, { () -> Void in
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
let time = dispatch_time(DISPATCH_TIME_NOW, (Int64)(2 * NSEC_PER_SEC))
dispatch_after(time, globalQueue) { () -> Void in
print("2秒后执行")
dispatch_semaphore_signal(semaphore)
}
})
}
}
取得信号量的线程在2秒后释放了信息量,相当于是每2秒执行一次。
通过上面的例子可以看到,在GCD
中,用dispatch_semaphore_create
函数能初始化一个信号量,同时需要指定信号量的初始值;使用dispatch_semaphore_wait
函数分配信号量并使计数减1,为0时处于等待状态;使用dispatch_semaphore_signal
函数释放信号量,并使计数加1。
另外dispatch_semaphore_wait
同样也支持超时,只需要给其第二个参数指定超时的时候即可,同Dispatch Group
的dispatch_group_wait
函数类似,可以通过返回值来判断。
注意:
如果是在OS X 10.8或iOS 6以及之后版本中使用,Dispatch Semaphore
将会由ARC
自动管理,如果是在此之前的版本,需要自己手动释放。
dispatch_once
函数通常用在单例模式上,它可以保证在程序运行期间某段代码只执行一次。如果我们要通过dispatch_once
创建一个单例类,在Swift可以这样:
class SingletonObject {
class var sharedInstance : SingletonObject {
struct Static {
static var onceToken : dispatch_once_t = 0
static var instance : SingletonObject? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = SingletonObject()
}
return Static.instance!
}
}
这样就能通过GCD的安全机制保证这段代码只执行一次。
GCD
主线程 & 主队列的关系?提交到主队列的任务在主线程执行。
主队列是主线中的一个串行队列。
所有的和UI相关的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI。
每一个应用程序只有唯一的一个主队列用来update UI
补充一点:如果在主线程中创建自定义队列(串行或者并行均可),在这个队列中执行同步任务,同样可以更新UI操作,主队列中可以更新UI,自定义队列也可以更新UI,但自定义队列的更新UI的前提是在主线程中执行同步任务。
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block)
在某队列开启同步线程
dispatch_barrier_sync()
障碍锁的方式同步
dispatch_group_create()
+ dispatch_group_wait()
dispatch_apply()
插队追加 操作同步
dispatch_semaphore_create()
+ dispatch_semaphore_wait()
信号量锁
串行NSOperationQueue队列并发数为1的时候 [NSOpertaion start] 启动任务即使同步操作 (NSOperationQueue.maxConcurrentOperationCount = 1)
pthread_mutex
底层锁函数
上层应用层封装的NSLock
NSRecursiveLock
递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中
NSConditionLock
& NSCondition
条件锁
@synchronized
同步操作 单位时间内只允许一个线程进入临界区
dispatch_once()
单位时间内只允许一个线程进入临界区
dispatch_once
实现原理 ?这个问题问的很傻吊也很高超.因为要解释清楚所有步骤需要记住里面所有代码
我认为这个问题应该从操作系统层面回答, 这个问题的核心是操作系统返回状态决定的,单位时间内操作系统只允许一个线程进入临界区,进入临界区的线程会被标记
回归到代码就是
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
|_____dispatch_once_f(val, block, _dispatch_Block_invoke(block))
|_______&l->dgo_once // &l->dgo_once 地址中存储的值。显然若该值为DLOCK_ONCE_DONE,即为once已经执行过
dgo_once
是dispatch_once_gate_s
的成员变量
typedef struct dispatch_once_gate_s {
union {
dispatch_gate_s dgo_gate;
uintptr_t dgo_once;
};
} dispatch_once_gate_s, *dispatch_once_gate_t;
有个内联函数static inline bool _dispatch_once_gate_tryenter(dispatch_once_gate_t l)
这个内联函数返回一个 原子性操作的结果
return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,(uintptr_t)_dispatch_lock_value_for_self(), relaxed)
比较+交换 的原子操作。比较 &l->dgo_once
的值是否等于 DLOCK_ONCE_UNLOCKED
这样就实现了我们的执行1次的GCD API.
互斥条件(Mutual exclusion) :
资源不能被共享,只能由一个进程使用。
请求与保持条件(Hold and wait):
进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
不可抢占条件(No pre-emption) :
有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。
循环等待条件(Circular wait) :
若干个进程形成环形链,每个都占用对方申请的下一个资源。
死锁预防:
破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。
死锁避免:
避免是指进程在每次申请资源时判断这些操作是否安全。例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。
死锁检测:
死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
死锁解除:
这是与死锁检测结合使用的,它使用的方式就是剥夺。即:将某进程所拥有的资源强行收回,分配给其他的进程。
死锁的避免:
死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁。
避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:
如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;
如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;
显然要避免死锁,必须事先知道系统拥有的资源数量及其属性。
锁类型 | 使用场景 | 备注 |
---|---|---|
pthread_mutex | 互斥锁 | PTHREAD_MUTEX_NORMAL ,#import <pthread.h> |
OSSpinLock | 自旋锁 | 不安全,iOS 10 已启用 |
os_unfair_lock | 互斥锁 | 替代 OSSpinLock |
pthread_mutex (recursive) | 递归锁 | PTHREAD_MUTEX_RECURSIVE ,#import <pthread.h> |
pthread_cond_t | 条件锁 | #import <pthread.h> |
pthread_rwlock | 读写锁 | 读操作重入,写操作互斥 |
@synchronized | 互斥锁 | 性能差,且无法锁住内存地址更改的对象 |
NSLock | 互斥锁 | 封装 pthread_mutex |
NSRecursiveLock | 递归锁 | 封装pthread_mutex (recursive) |
NSCondition | 条件锁 | 封装 pthread_cond_t |
NSConditionLock | 条件锁 | 可以指定具体条件值 封装 pthread_cond_t |
NSOperationQueue
中的 maxConcurrentOperationCount
默认值默认值 -1。 这个值操作系统会根据资源使用的综合开销情况设置。
NSTimer
、CADisplayLink
、dispatch_source_t
的优劣?定时器类型 | 优势 | 劣势 |
---|---|---|
NSTimer | 使用简单 | 依赖 RunLoop ,具体表现在无 RunLoop 无法使用、NSRunLoopCommonModes 、不精确 |
CADisplayLink | 依赖屏幕刷新频率出发事件,最精.最合适做UI刷新 | 若屏幕刷新被影响,事件也被影响、事件触发的时间间隔只能是屏幕刷新 duration 的倍数、若事件所需时间大于触发事件,跳过数次、不能被继承 |
dispatch_source_t | 不依赖 RunLoop | 依赖线程队列,使用麻烦 使用不当容易Crash |
Tableview
懒加载、Cell 复用高度缓存(因为 heightForRowAtIndexPath: 是调用最频繁的方法)
当 cell 的行高固定时,使用固定行高 self.tableView.rowHeight = xxx;
当 cell 的行高是不固定时,根据内容进行计算后缓存起来使用。第一次肯定会计算,后续使用缓存时就避免了多次计算;高度的计算方法通常写在自定义的cell中,调用时,既可以在设置 cell 高的代理方法中使用,也可以自定义的 model 中使用(且使用时,使用get方法处理)。
数据处理
使用正确的数据结构来存储数据;
数据尽量采用局部的 section,或 cellRow 的刷新,避免 reloadData;
大量数据操作时,使用异步子线程处理,避免主线程中直接操作;
缓存请求结果。
异步加载图片:SDWebImage 的使用
使用异步子线程处理,然后再返回主线程操作;
图片缓存处理,避免多次处理操作;
图片圆角处理时,设置 layer 的 shouldRasterize 属性为 YES,可以将负载转移给 CPU。
按需加载内容
滑动操作时,只显示目标范围内的 Cell 内容,显示过的超出目标范围内之后则进行清除;
滑动过程中,不加载显示图片,停止时才加载显示图片。
视图层面
(1)减少 subviews 的数量,自定义的子视图可以整合在形成一个整体的就整合成一个整体的子视图;
(2)使用 drawRect 进行绘制(即将 GPU 的部分渲染转接给 CPU ),或 CALayer 进行文本或图片的绘制。在实现 drawRect 方法的时候注意减少多余的绘制操作,它的参数 rect 就是我们需要绘制的区域,在 rect 范围之外的区域我们不需要进行绘制,否则会消耗相当大的资源;
(3)异步绘制,且设置属性 self.layer.drawsAsynchronously = YES;(遇到复杂界面,遇到性能瓶颈时,可能就是突破口);
(4)定义一种(尽量少)类型的 Cell 及善用 hidden 隐藏(显示) subviews;
(5)尽量使所有的 view 的 opaque 属性为 YES,包括 cell 自身,以提高视图渲染速度(避免无用的 alpha 通道合成,降低 GPU 负载);
(6)避免渐变,图片缩放的操作;
(7)使用 shadowPath 来画阴影;
(8)尽量不使用 cellForRowAtIndexPath: ,如果你需要用到它,只用一次然后缓存结果;
(9)cellForRowAtIndexPath 不要做耗时操作:如不读取文件 / 写入文件;尽量少用 addView 给 Cell 动态添加 View,可以初始化时就添加,然后通过 hide 来控制是否显示;
(10)我们在 Cell 上添加系统控件的时候,实际上系统都会调用底层的接口进行绘制,大量添加控件时,会消耗很大的资源并且也会影响渲染的性能。当使用默认的 UITableViewCell 并且在它的 ContentView 上面添加控件时会相当消耗性能。所以目前最佳的方法还是继承 UITableViewCell,并重写 drawRect 方法;
(11)当我们需要圆角效果时,可以使用一张中间透明图片蒙上去使用 ShadowPath 指定 layer 阴影效果路径使用异步进行 layer 渲染(Facebook 开源的异步绘制框架 AsyncDisplayKit )设置 layer 的 opaque 值为 YES ,减少复杂图层合成尽量使用不包含透明(alpha)通道的图片资源尽量设置 layer 的大小值为整形值直接让美工把图片切成圆角进行显示,这是效率最高的一种方案很多情况下用户上传图片进行显示,可以让服务端处理圆角使用代码手动生成圆角 Image 设置到要显示的 View 上,利用 UIBezierPath ( CoreGraphics 框架)画出来圆角图片。
卡顿原因: 在一个VSync
内GPU
和CPU
的协作,未能将渲染任务完成放入到帧缓冲区,视频控制器去缓冲区拿数据的时候是空的,所以卡帧。
卡顿优化:
图片等大文件IO缓存
耗时操作放入子线程
提高代码执行效率(JSON to Model的方案,锁的使用等,减少循环,UI布局frame子线程预计算)
UI减少全局刷新,尽量使用局部刷新
监控卡帧:
CADisplayLink
监控,结合子线程和信号量,两次事件触发时间间隔超过一个VSync
的时长,上报调用栈。
在RunLoop
中添加监听,如果kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
中间的耗时超过VSync
的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看。那个部分耗时长即可。
离屏渲染(Off-Screen Rendering):分为CPU离屏渲染 和 GPU离屏渲染两种形式。GPU离屏渲染指的是在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作。
一般情况下,OpenGL
会将应用提交到 Reader Server 的动画直接渲染显示,但对于一些复杂的图像动画显示并不能直接渲染叠加显示,而是需要根据 Command Buffer 分通道进行渲染之后在组合,这一组合过程中,就有些渲染通道是不会直接显示的;Masking 渲染需要更多的渲染通道和合并的步骤;而这些没有直接显示在屏幕上的通道就是 Off-Screen Readering Pass。
true
YES
和layer.opacity小于1.0drawRect :
方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现UIBezierPath
和 Core Graphics
代替 layer
设置圆角。即:UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100,100,100,100)];
imageView.image = [UIImage imageNamed:@"myImg"];
//开始对imageView进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.boundscornerRadius:imageView.frame.size.width]addClip];
[imageView drawRect:imageView.bounds];
imageView.image=UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
1.2、使用 CAShapeLayer
和 UIBezierPath
代替 layer
设置圆角。即:UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//设置大小
maskLayer.frame = imageView.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.view addSubview:imageView];
mageView.layer.shadowColor = [UIColorgrayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
我们还可以通过设置shouldRasterize
属性值为YES来强制开启离屏渲染。其实就是光栅化(Rasterization)。既然离屏渲染这么不好,为什么我们还要强制开启呢?当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于UITableViewCell中,cell的复用反而降低了性能。最好用于图层较多的静态内容的图形。而且产生的位图缓存的大小是有限制的,一般是2.5个屏幕尺寸。在100ms之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。ShadowPath
指定layer
阴影效果路径AsyncDisplayKit (Texttrue)
)UIBezierPath
(CoreGraphics框架)画出来圆角图片webp
。-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring -Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab -Wl,-rename_section,__TEXT,__const,__RODATA,__const -Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname -Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname -Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
xcasset
管理图片BitCode
Product
选择 Analyze
(快捷键: Command + Shift + B)Instruments
了。具体操作是通过 Xcode 打开项目,然后点击 Product
--> Profile
。Instruments
工具。选择 Leaks
选项,点击右下角的【choose】按钮,这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作。Leaks
是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。 橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。Leaks Checks
,在 Details
所在栏中选择 CallTree
,并且在右下角勾选 Invert Call Tree
和 Hide System Libraries
,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。timer
的 invalidate
,并 timer 置为 nil;__weak
、__strong
__weak
、__strong
APP 启动分为热启动和冷启动。
pre-main
和 main()
。启动时间也是针对这两个阶段进行优化,下面我们也将从这两方面进行优化:
Total pre-main time: 866.86 milliseconds (100.0%)
dylib loading time: 328.28 milliseconds (37.8%)
rebase/binding time: 49.19 milliseconds (5.6%)
ObjC setup time: 62.85 milliseconds (7.2%)
initializer time: 426.38 milliseconds (49.1%)
slowest intializers :
libSystem.B.dylib : 7.52 milliseconds (0.8%)
libMainThreadChecker.dylib : 37.19 milliseconds (4.2%)
libglInterpose.dylib : 61.17 milliseconds (7.0%)
libMTLInterpose.dylib : 22.23 milliseconds (2.5%)
MyMoney : 392.50 milliseconds (45.2%)
pre-main 阶段主要由4部分组成:
rebase/binding
阶段优化很好,本阶段耗时也会很少+ load()
方法,调用 C/C++ 中的构造器函数。 initializer
阶段执行结束后, dylib 开始调用 main() 函数。在这一步,检查 + load()
方法,尽量把事情推迟到 + initialize()
方法里执行;并且控制 category 数量,去掉不必要的 category。didFinishLaunchingWithOptions
方法里执行了多项项业务,有一大部分业务并不是一定要在这里执行的,如支付配置、客服配置、分享配置等。整理该方法里的业务,能延迟加载的就往后推迟,防止其影响启动时间。didFinishLaunchingWithOptions
,将业务分级,对于非必须的业务移到首页显示后加载。同时,为了防止以后新加的业务继续往 didFinishLaunchingWithOptions
里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。编写软件过程中,程序员面临着来自耦合性、内聚性以及可维护性、可扩展性、重用性、灵活性等多方面的挑战,设计模式是为了让程序具有更好的:
设计模式有 7 大原则:
单例模式
意图
:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决
:一个全局使用的类频繁地创建与销毁。
工厂模式
简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。比如,一台咖啡机就可以理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体实现,只要告诉它你的需求即可。
抽象工厂模式
抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让子类去做决定。
比如:以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。
代理模式
代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
优点:
缺点:
举一个生活中的例子:比如买飞机票,由于离飞机场太远,直接去飞机场买票不太现实,这个时候我们就可以上携程 App 上购买飞机票,这个时候携程 App 就相当于是飞机票的代理商。
观察者模式
观察者模式是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
优点:
缺点:
策略模式
策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。
优点:遵循了开闭原则,扩展性良好。
缺点:随着策略的增加,对外暴露越来越多。
单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在,整个系统只能使用一个对象实例。
优点:
1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
2. 避免对资源的多重占用
缺点:
1. 没有接口,不能继承,与单一职责原则冲突
2. 一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
MVC:
MVC即 Model-VIew-Controller。他是1970年代被引入到软件设计大众的。MVC模式致力于关注点的切分,这意味着 model 和 controller 的逻辑是不与用户界面(View)挂钩的。因此,维护和测试程序变得更加简单容易。
MVC设计模式将应用程序分离为3个主要的方面:Model,View和Controller
MVP
MVP 模式把应用分成了 3 个主要方面: Model 、View 、 Presenter。
MVP模式关键点:
MVVM
MVVM 即 Model-View-View Mode l。这个模式提供对 View 和 View Model 的双向数据绑定。这使得 View Model 的状态改变可以自动传递给View 。典型的情况是,View Model 通过使用 obsever 模式(观察者模式)来将 View Model 的变化通知给 Model。
Model :Model 层代表了描述业务逻辑和数据的一系列类的集合。它也定义了数据修改和操作的业务规则。
View: View 代表了UI组件,像CSS,JQuery,html等。他只负责展示从 ViewModel 接收到的数据。也就是把模型转化成UI。
View Model :View Model 负责暴漏方法,命令,其他属性来操作 VIew 的状态,组装 model 作为 View 动作的结果,并且触发 view 自己的事件。
MVVM模式关键点:
业内常见的路由方案有3种:
Url-scheme注册(MGJRouter
)
iOS系统中默认是支持 Url Scheme方式的,例如可以在浏览器中输入: weixin:// 就可以打开微信应用。自然在APP内部也可以通过这种方法来实现组件之间的路由设计。
这种方式实现的原理是:在APP启动的时候,或者是向以下实例中的每个模块自己的 load
方法里面注册自己的断链(Url),以及对外提供服务(Block),通过url-scheme标记好,然后维护在url-router里面。 url-router中保存了各个组件对应的url-scheme,只要其它组件调用了 open url 的方法,url-router就会去根据url去查找对应的服务并执行。
URI
( web service
模式的资源通用表示方式)的格式。例如 appscheme://path
: ctd://home/scan
map
,key
是url
,value
是对应存放的block
数组,url
和block
都会常驻在内存中,当打开一个url
时,JLRoutes
就可以遍历这个全局的map
,通过url
来执行对应的block
。蘑菇街
的技术团队开源的一个router
,特点是使用简单方便。JLRoutes
的问题主要在于查找url的实现不够高效,通过遍历而不是匹配,还有就是功能偏多。HHRouter
的url
查找是基于匹配,所以会更高效,MGJRouter
也是采用的这种方法,但HHRouter
和 ViewController
绑定地过于紧密,一定程度上降低了灵活性。于是就有了 MGJRouter
, 从数据结构上看它和 HHRouter
是一样的。URL注册
对于实施组件化是完全没有必要的,拓展性和可维护性都降低;Open-url
的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度;但是可以通过传递parms
来解决,但是这个区分了远程调用和本地调用的接口;URL
去完成调度?是没有必要的,为啥要复杂化?URL
和服务的对应表,并且需要开发人员对这样一个表进行维护;URL
及服务,因此内存中需要保存这样一份表,当组件多起来以后就会出现一些内存的问题;本地调用
和远程调用
,它们的处理逻辑是不同的。正确的做法
应该是把远程调用通过一个中间层转化成本地调用,如果把两者混为一谈,后期可能会出现无法区分业务的情况。比如对于组件无法响应的问题,远程调用可能直接显示一个404页面
,但是本地调用可能需要做其它处理。如果不加以区分,那么就无法完成这种业务要求。 远程调用只能传递被序列化JSON
的数据,像UIImage
这样非常规的对象是不行的,所以如果组件接口要考虑远程调用,这里的参数与就不能是这类非常规对象。优缺点:
优点:
Url-Scheme
是借鉴前端Router
和 系统App 内跳转方法
得出来的解决方案。所以不管是H5、RN、Android、iOS 都通用。缺点:
URL
的map
规则是需要注册的,它们会在load
方法里面写。写在load
方法里面是会影响App启动速度的。URL
链接里面关于组件
和页面的名字
都是硬编码,参数
也都是硬编码。而且每个URL
参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担。而且URL短连接散落在整个App四处,维护起来实在有点麻烦。NSObject
的参数,URL
是不够友好的,它最多是传递一个字典。利用Runtime
实现的target-action
方式(CTMediator
)- 个人推荐
相较于 url-scheme
的方式进行组件间的路由, runtime
的方式利用了 OC运行时
的特征,实现了组件间服务的自动发现,无需注册即可实现组建间的调用。因此,不管从维护性
、可读性
、扩展性
来说,都是一个比较完美的方案。
target-action
的原理:
传统的中介者模式
。这个中间件 Mediator
会依赖其他组件,其他组件也会依赖 Mediator
。
但是能不能让 Mediator
不在依赖组件,各个组件之间不再依赖,组件间调用只依赖中间件 Mediator
呢 ?
官方 casa 大神
的优化建议是这样的:
利用 target-action
的方式,创建一个 target
的类,类中定义了一些 action
方法,这些方法的结果是返回一个 Controller
或其他 Object
。再给中间件 CTMediator
添加一个分类方法(category
),定义组件外部可调用的方法接口,内部实现 perform: target: action
的方法。该方法主要通过 runtime
中的 NSClassFromString
获取 target
类和 NSSelectorFromString
获取方法名,这样就可以执行先去创建的 target
类中的方法得到返回值,在通过分类中的方法传值。
优缺点:
优点:
Runtime
的特性,无需注册这一步。Target-Action
方案只有存在组件依赖Mediator
这一层依赖关系。在Mediator
中维护针对Mediator
的Category
,每个category
对应一个Target
,Category
中的方法对应Action
场景。Target-Action
方案也统一了所有组件间调用入口。url
中进行Native
前缀进行验证。缺点:
Target_Action
在Category
中将常规参数打包成字典,在Target
处再把字典拆包成常规参数,这就造成了一部分的硬编码。protcol-class
注册
通过协议
和类
绑定,核心思想和代理传值是一样的,遵循协议,实现协议中的方法。
主要思路:
CommonProtocol.h
,里面存放各个模块提供的协议。在各个模块依赖这个头文件,实现协议的方法。ProtocolMediator
, 提供模块的注册和获取模块的功能(其实就是将类和协议名进行绑定,放在一个字典里,key
是协议名字符串,value
是类)。协议
,核心代码如下:Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
[B_VC action_B:@"param1" para2:222 para3:333 para4:444];
[self presentViewController:B_VC animated:YES completion:nil];
优缺点:
优点:
缺点:
Protocol
都要向ModuleManager
进行注册。保证项目的稳定性从4个方面来说:
CADisplayLink
Instruments
Instruments
来查看leaks
、代码方面:Delegate、Block、 Block、 NSNotification埋点:主要是为了收集数据和信息,用来跟踪应用使用的状况,后续用来进一步优化产品或是提供运营的数据支撑,包括访问数(Visits),访客数(Visitor),停留时长(Time On Site),页面浏览数(Page Views)和跳出率(Bounce Rate)等。
以大致分为两种:页面统计(track this virtual page view)、 统计操作行为(track this button by an event)。
手动埋点(代码埋点):
国内的主要第三方数据分析服务商,如百度统计、友盟、TalkingData、GrowingIO 等。
优点:
缺点:
自动化埋点(无埋点):
无埋点是指开发人员集成采集 SDK
后,SDK
便直接开始捕捉和监测用户在应用里的所有行为,并全部上报,不需要开发人员添加额外代码;或者是说用户展现界面元素时,通过控件绑定触发事件,事件被触发的时候系统会有相应的接口让开发者处理这些行为。现在市面上主流无埋点做法有两种
:一种是预先跟踪所有的渲染信息,一种是滞后跟踪的渲染信息。
数据分析师/数据产品通过管理后台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后就可以结合时间属性、用户属性、事件进行分析了。所以无埋点并不是真的不用埋点了。
优点:
可视化埋点:
SDK
外,不需要额外去写埋点代码,而是由业务人员通过访问分析平台的 圈选
功能来圈
出需要对用户行为进行捕捉的控件,并给出事件命名。圈选完毕后,这些配置会同步到各个用户的终端上,由采集 SDK
按照圈选的配置自动进行用户行为数据的采集和发送。git diff
?MVC
架构。
优点:
MVC
的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。MVC
模式允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。缺点:
依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。
MVC
是苹果官方推荐的项目架构,相对于 MVP
、 MVVM
架构来说入门相对的低一些;而且公司的项目不是很大,在综合人力成本等方面选择了 MVC
架构。
针对 Controller
臃肿问题作出优化,将数据相关进行抽离管理,向 MVVM
模式靠拢。
SDWebImage
SDWebImage
组织架构:
SDWebImageDownloader
:负责维持图片的下载队列;
SDWebImageDownloaderOperation
:负责真正的图片下载请求;
SDImageCache
:负责图片的缓存;
SDWebImageManager
:是总的管理类,维护了一个SDWebImageDownloader
实例和一个 SDImageCache
实例,是下载与缓存的桥梁;
SDWebImageDecoder
:负责图片的解压缩;
SDWebImagePrefetcher
:负责图片的预取;
UIImageView+WebCache
:和其他的扩展都是与用户直接打交道的。
SDWebImage
图片加载流程:
AFNetWorking
AFNetWorking
组织架构:主要有5
个模块
AFHTTPSessionManager
:是对 NSURLSession
的封装,负责发送网络请求,是 AFNetWotking
中使用最多一个模块AFNetworkingReachabilityManager
:实时监测网络状态的工具类AFSecurityPolicy
:网络安全策略的工具类,主要是针对于 Https 服务Serializstion
:请求序列化工具类
AFURLRequestSerialization
:请求入参序列化工具基类AFURLResponseSerialization
:请求回参序列化工具基类
AFJSONResponseSerializer
: Json
解析器,AFNetWorking
的默认解析器AFXMLParserResponseSerializer
:XML
解析器AFHTTPResponseSerializer
: 万能解析器,直接返回二进制数据(NSData
),服务器不会对数据进行处理UIKit
: 对iOS UIKit
的扩展AFNetworking
的可能面试考点 :
AFNetworking
2.x怎么开启常驻子线程?为何需要常驻子线程?2.x
版本中 AFNetWorking
通过 RunLoop
开启了一个常驻子线程,具体代码是这样的:+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *RunLoop = [NSRunLoop currentRunLoop];
[RunLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[RunLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
为何要开启常驻子线程?NSURLConnection
的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到 NSURLConnection
的回调,就必须让子线程至少存活到回调的时机。而AF让线程常驻的原因是,当发起多个http
请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来。RunLoop
来开启常驻线程。AFURLSessionManager
与 NSURLSession
的关系,每次都需要新建 manager
吗?AFNetWorking
中 manager
与 session
是1对1的关系, AFNetWorking
会在 manager
初始化的时候创建对应的 NSURLSession
。同样, AFNetWorking
也在注释中写明了可以提供一个配置好的 manager
单例来全局复用。session
其实就是在利用 http2.0
的多路复用特点,减少访问同一个服务器时,重新建立 tcp
连接的耗时和资源。AFSecurityPolicy
如何避免中间人攻击?ATS的策略
,基本都切到 HTTPS
了,HTTPS
的基本原理还是需要了解一下的,这里不做介绍。Charles/Fiddler
等工具实际上就可以看成中间人攻击。SSL Pinning
。 AFSecurityPolicy
的 AFSSLPinningMode
就是相关设置项。SSL Pinning
的原理就是需要将服务器的公钥打包到客户端中, tls
验证时,会将服务器的证书和本地的证书做一个对比,一致的话才允许验证通过。typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,
AFSSLPinningModePublicKey, // 只验证证书中的公钥
AFSSLPinningModeCertificate, // 验证证书所有字段,包括有效期之内
};
由于数字证书存在有效期,内置到客户端后就存在失效后导致验证失败的问题,所以可以考虑设置为 AFSSLPinningModePublicKey
的模式,这样的话,只要保证证书续期后,证书中的公钥不变,就能够通过验证了。AFNetWorking 3.x
为什么不再需要常驻线程?AFNetWorking 2.x
使用 NSURLConnection
,痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。所以 AFNetWorking2.x
在权衡之后选择了常驻线程。AFNetWorking 3.x
之后使用了 NSURLSession
:self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
AFNetWorking 3.x
使用 NSURLSession
解决了 NSURLConnection
的痛点,从上面的代码可以看出, NSURLSession
发起的请求,不再需要在当前线程进行代理方法的回调。可以指定回调的 delegateQueue
,这样我们就不用为了等待代理回调方法而苦苦保活线程了。同时还要注意一下:
指定的用于接收回调的 Queue
的 maxConcurrentOperationCount
设为了 1
,这里目的是想要让并发的请求串行的进行回调。
为什么 3.0 中需要设置为 1 ?
self.operationQueue.maxConcurrentOperationCount = 1;
解答:功能不一样:3.0的operationQueue是用来接收NSURLSessionDelegate回调的,
鉴于一些多线程数据访问的安全性考虑,
设置了maxConcurrentOperationCount = 1 来达到串行回调的效果。
而2.0的operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。
- (AFHTTPRequestOperation *)POST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
为什么要串行回调?
- (AFURLSessionManagerTaskDelegate *)delegateForTask:(NSURLSessionTask *)task {
NSParameterAssert(task);
AFURLSessionManagerTaskDelegate *delegate = nil;
[self.lock lock];
//给所要访问的资源加锁,防止造成数据混乱
delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)];
[self.lock unlock];
return delegate;
}
这边对 self.mutableTaskDelegatesKeyedByTaskIdentifier
的访问进行了加锁,目的
是保证多线程环境下的数据安全。既然加了锁,就算 maxConcurrentOperationCount
不设为 1
,当某个请求正在回调时,下一个请求还是得等待一直到上个请求获取完所要的资源后解锁,所以这边并发回调也是没有意义的。相反多 task
回调导致的多线程并发,还会导致性能的浪费。所以 maxConcurrentOperationCount = 1
。重构技巧:
适合节点:
Bug
的时候)重构是一个不断的过程。
MVC
或者 MVVM
模式进行总的架构设计。策略模式
:针对实现目标/功能的复杂度,判断情况选用 策略模式
。观察者模式
和 代理模式
针对实时情况而定。工厂模式
和 抽象工厂模式
:根据过程父子关系复杂程度和子类种类数量多少程度,判断是否使用 工厂模式
和 抽象工厂模式
。适配器模式
: 高度自定义问题,前端/移动端 根据数据格式做适配。(比如说 电商SKU
Cell
适配等)单例模式
:根据模块在项目的 唯一性
,重要性
等作出判断。(比如:应用的配置信息,用户的个人信息,本地数据库进行操作,数据上传云端,通信管理类等)展现层
、业务层
、数据层
性能统计
、Networking
、Patch
、网络诊断
、数据存储
模块。对于基础模块来说,其本身应该是自洽的,即可以单独编译或者几个模块合在一起可以单独编译。所有的依赖关系都应该是业务模块指向基础模块的。Runtime
实现的 target-action
方式(CTMediator
)顺序存储结构
:顺序存放
,每个结点只有一个元素。存储位置反映数据元素间的逻辑关系。
存储密度大
,但是插入、删除操作效率较差。(比如:数组
:1-2-3-4-5-6-7-8-9-10,存储是按顺序的。再比如栈
和队列
等)。链式存储结构
:一组指针
,指针
反映数据元素间的逻辑关系。
哈希(散列)存储结构
:哈希函数
解决冲突的方法,将关键字散列
在连续的
有限的
地址空间内,并将哈希函数的值作为该数据元素的存储地址。
索引存储结构
:地址连续的内存空间
外,尚需建立一个索引表
。索引表中的索引指示结点的存储位置,兼有动态和静态的特性。链表:
是一种物理存储单元上非连续
、非顺序
的存储结构,数据元素的逻辑顺序
是通过链表中的指针
链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
.
单向链表:
A->B->C->D->E->F->G->H。 这就是单向链表 ,H 是头 A 是尾,像一个只有一个头的火车一样。只能一个头拉着跑。
双向链表:
H<- A->B->C->D->E->F->G->H。 这就是双向链表。有头没尾,两边都可以跑 ,跟地铁一样 到头了,可以倒着开回来。
循环链表:
A->B->C->D->E->F->G->H,绕成一个圈。就像蛇吃自己的这就是循环。
数组是可以再内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。
优点:
缺点:
适用场景:
频繁查询,对存储空间要求不大,很少增加和删除的情况。
链表:
是一种物理存储单元上非连续
、非顺序
的存储结构,数据元素的逻辑顺序
是通过链表中的指针
链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
栈:一种常用的先进后出(FILO—First-In/Last-Out)的数据结构。常用:
队列:是一种先进先出(FIFO—first in first out)的数据结构。
堆:是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:
public static int treeDepth(BinaryTreeNode root) {
if (root == null) {
return 0;
}
int left = treeDepth(root.left);
int right = treeDepth(root.right);
return left > right ? (left + 1) : (right + 1);
}
在计算机科学中,时间复杂性,又称时间复杂度。
算法的时间复杂度是一个函数
,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述
,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的
,亦即考察输入值大小趋近无穷时的情况。
空间复杂度(Space Complexity)是对一个算法在运行过程中
临时占用存储空间大小的量度
,记做S(n)=O(f(n))。比如:
直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。
一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量:时间复杂度
& 空间复杂度
。
冒泡排序:
原理:就是重复地走访过要排序的元素列,依次比较两个相邻的元素,顺序不对就交换,直至没有相邻元素需要交换,也就是排序完成。
这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
冒泡排序是一种稳定排序算法。
时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n2)
void buddleSort(int num[],int count)
{
for (int i = 0; i < count - 1; i++) {
for (int j = 0; j < count - i - 1; j++) { // 注意 j < count - i - 1
if (num[j] > num[j + 1]) EXCHANGE(num[j], num[j + 1]) // 如果 前面的数后面的大则交换
}
}
}
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
选择排序是不稳定的排序方法。
时间复杂度:最好和平均情况下都是O(n2)
void selectSort(int num[],int count)
{
for (int i = 0; i < count; i++) {
int min = i;
for (int j = i; j < count; j++) { // 注意 j = i
if (num[j] < num[min]) min = j;
}
if (i != min) EXCHANGE(num[i], num[min]);//可以看出,最多交换count - 1次
}
}
直接插入排序
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,
插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止
直接插入排序是稳定的排序算法。
时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n2)
/**
*
* num[] 是已经排序好的,在插入一个数直接进行排序
*
*/
void insertSort2(int num[],int count)
{
int i,j;
for (i = 1; i < count; i++) {
if (num[i] < num[i - 1]) { // 当前数比前一位的数小
int temp = num[i]; // 记住 当前数
for (j = i; j > 0; j--) { // 从当前数起 逆序
if (num[j - 1] > temp) num[j] = num[j - 1]; // 如果 当前数比前一位小,前一位后移
else break;
}
num[j] = temp;
}
}
}
二分插入排序
由于在插入排序过程中,待插入数据左边的序列总是有序的,针对有序序列,就可以用二分法去插入数据了,也就是二分插入排序法。适用于数据量比较大的情况。
二分插入排序的算法思想:
算法的基本过程:
(1)**计算 0 ~ i-1 的中间点,用 i 索引处的元素与中间值进行比较,如果 i 索引处的元素大,说明要插入的这个元素应该在中间值和刚加入i索引之间,反之,就是在刚开始的位置到中间值的位置,这样很简单的完成了折半**;
(2)在相应的半个范围里面找插入的位置时,不断的用(1)步骤**缩小范围,不停的折半,范围依次缩小为 1/2 1/4 1/8 …快速的确定出第 i 个元素要插在什么地方;
**(3)**确定位置之后,将整个序列后移,并将元素插入到相应位置。
二分插入排序是稳定的排序算法。
时间复杂度:最好情况(刚好插入位置为二分位置)下是O(log?n),平均情况和最坏情况是o(n2)
void insertSortBinary(int num[],int count)
{
int i,j;
for (i = 1; i < count; i++) {
if (num[i] < num[i - 1]) {
int temp = num[i];
int left = 0,right = i - 1;
while (left <= right) {
int mid = (left + right)/2;
if (num[mid] < temp) left = mid + 1;
else right = mid - 1;
}
//只是比较次数变少了,交换次数还是一样的
for (j = i; j > left; j--) {
num[j] = num[j - 1];
}
num[left] = temp;
}
}
}
希尔排序
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,排序完成。
希尔排序是非稳定排序算法。
时间复杂度:O(n^(1.3—2))
void shellSort(int num[],int count)
{
int shellNum = 2;
int gap = round(count/shellNum);
while (gap > 0) {
for (int i = gap; i < count; i++) {
int temp = num[i];
int j = i;
while (j >= gap && num[j - gap] > temp) {
num[j] = num[j - gap];
j = j - gap;
}
num[j] = temp;
}
gap = round(gap/shellNum);
}
}
快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。
它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序是非稳定的排序算法
时间复杂度:最差为O(n^2),平均为O(nlogn),最好为O(nlogn)
void quickSort(int num[],int count,int left,int right)
{
if (left >= right){
return ;
}
int key = num[left];
int lp = left; //左指针
int rp = right; //右指针
while (lp < rp) {
if (num[rp] < key) {
int temp = num[rp];
for (int i = rp - 1; i >= lp; i--) {
num[i + 1] = num[i];
}
num[lp] = temp;
lp ++;
rp ++;
}
rp --;
}
quickSort(num, count, left, lp - 1);
quickSort(num, count, rp + 1, right);
}
堆排序
是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
**最大堆调整(Max Heapify):**将堆的末端子节点作调整,使得子节点永远小于父节点
**创建最大堆(Build Max Heap):**将堆中的所有数据重新排序
**堆排序(HeapSort):**移除位在第一个数据的根节点,并做最大堆调整的递归运算
堆排序是一个非稳定的排序算法。
时间复杂度:O(nlogn)
void maxHeapify(int num[], int start, int end) {
//建立父节点指标和子节点指标
int dad = start;
int son = dad * 2 + 1;
while (son <= end) { //若子节点指标在范围内才做比较
if (son + 1 <= end && num[son] < num[son + 1]) //先比较两个子节点大小,选择最大的
son++;
if (num[dad] > num[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
return;
else { //否则交换父子内容再继续子节点和孙节点比较
EXCHANGE(num[dad], num[son])
dad = son;
son = dad * 2 + 1;
}
}
}
void heapSort(int num[], int count) {
int i;
//初始化,i从最後一个父节点开始调整
for (i = count / 2 - 1; i >= 0; i--)
maxHeapify(num, i, count - 1);
//先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
for (i = count - 1; i > 0; i--) {
EXCHANGE(num[0], num[i])
maxHeapify(num, 0, i - 1);
}
}
- (NSString *)reversalString:(NSString *)originString{
NSString *resultStr = @"";
for (NSInteger i = originString.length -1; i >= 0; i--) {
NSString *indexStr = [originString substringWithRange:NSMakeRange(i, 1)];
resultStr = [resultStr stringByAppendingString:indexStr];
}
return resultStr;
}
头插法
:struct ListNode* reverseList(struct ListNode* head){
//新链表的头指针
struct ListNode* newhead = NULL;
//需要头插的结点
struct ListNode* cur = head;
while(cur)
{
//保存需要头插结点的下一个节点
struct ListNode* next = cur->next;
//将cur头插到新链表
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
迭代法
:struct ListNode* reverseList(struct ListNode* head){
struct ListNode* pre = NULL;
//需要反转指向的结点
struct ListNode* cur = head;
while(cur)
{
//保存需要头插结点的下一个节点
struct ListNode* next = cur->next;
//将cur头插到新链表
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
- (void)merge {
/*
有序数组A:1、4、5、8、10...1000000,有序数组B:2、3、6、7、9...999998,A、B两个数组不相互重复,请合并成一个有序数组C,写出代码和时间复杂度。
*/
//(1).
NSMutableArray *A = [NSMutableArray arrayWithObjects:@4,@5,@8,@10,@15, nil];
// NSMutableArray *B = [NSMutableArray arrayWithObjects:@2,@6,@7,@9,@11,@17,@18, nil];
NSMutableArray *B = [NSMutableArray arrayWithObjects:@2,@6,@7,@9,@11,@12,@13, nil];
NSMutableArray *C = [NSMutableArray array];
int count = (int)A.count+(int)B.count;
int index = 0;
for (int i = 0; i < count; i++) {
if (A[0]<B[0]) {
[C addObject:A[0]];
[A removeObject:A[0]];
}
else if (B[0]<A[0]) {
[C addObject:B[0]];
[B removeObject:B[0]];
}
if (A.count==0) {
[C addObjectsFromArray:B];
NSLog(@"C = %@",C);
index = i+1;
NSLog(@"index = %d",index);
return;
}
else if (B.count==0) {
[C addObjectsFromArray:A];
NSLog(@"C = %@",C);
index = i+1;
NSLog(@"index = %d",index);
return;
}
}
//(2).
//时间复杂度
//T(n) = O(f(n)):用"T(n)"表示,"O"为数学符号,f(n)为同数量级,一般是算法中频度最大的语句频度。
//时间复杂度:T(n) = O(index);
}
两个思路:
# define SIZE 256
char GetChar(char str[])
{
if(!str)
return 0;
char* p = NULL;
unsigned count[SIZE] = {0};
char buffer[SIZE];
char* q = buffer;
for(p=str; *p!=0; p++)
{
if(++count[(unsigned char)*p] == 1)
*q++ = *p;
}
for (p=buffer; p<q; p++)
{
if(count[(unsigned char)*p] == 1)
return *p;
}
return 0;
}
这个问的其实是数据结构中的二叉树,查找一个普通二叉树中两个节点最近的公共祖先问题。
假设两个视图为UIViewA、UIViewC,其中 UIViewA继承于UIViewB,UIViewB继承于UIViewD,UIViewC也继承于UIViewD;即 A->B->D,C->D
- (void)viewDidLoad {
[super viewDidLoad];
Class commonClass1 = [self commonClass1:[ViewA class] andClass:[ViewC class]];
NSLog(@"%@",commonClass1);
// 输出:2018-03-22 17:36:01.868966+0800 两个UIView的最近公共父类[84288:2458900] ViewD
}
// 获取所有父类
- (NSArray *)superClasses:(Class)class {
if (class == nil) {
return @[];
}
NSMutableArray *result = [NSMutableArray array];
while (class != nil) {
[result addObject:class];
class = [class superclass];
}
return [result copy];
}
- (Class)commonClass1:(Class)classA andClass:(Class)classB {
NSArray *arr1 = [self superClasses:classA];
NSArray *arr2 = [self superClasses:classB];
for (NSUInteger i = 0; i < arr1.count; ++i) {
Class targetClass = arr1[i];
for (NSUInteger j = 0; j < arr2.count; ++j) {
if (targetClass == arr2[j]) {
return targetClass;
}
}
}
return nil;
}
- (Class)commonClass2:(Class)classA andClass:(Class)classB{
NSArray *arr1 = [self superClasses:classA];
NSArray *arr2 = [self superClasses:classB];
NSSet *set = [NSSet setWithArray:arr2];
for (NSUInteger i =0; i<arr1.count; ++i) {
Class targetClass = arr1[i];
if ([set containsObject:targetClass]) {
return targetClass;
}
}
return nil;
}
假设每个输入只对应一种答案,且同样的元素不能被重复利用。 示例:给定nums = [2, 7, 11, 15], target = 9 — 返回 [0, 1] 思路:
class Solution {
public int[] twoSum(int[] nums, int target) {
int len = nums.length;
int[] result = new int[2];
for(int i = 0; i < len; i++){
for(int j = i+1; j < len; j++){
if(nums[i] + nums[j] == target){
result[0] = i;
result[1] = j;
return result;
}
}
}
return result;
}
}
HTTP协议
:超文本传输协议,他是基于TCP应用层协议
是无状态协议,需要通过cookies 或者 session 来保持会话
它包含了3个部分:
请求报文:
响应报文
状态行:包含HTTP 版本,状态码,状态码原因短语
响应首部字段
响应内容实体
URL 构成:
协议构成:
请求行、请求头、请求体
常用的请求方式?
答: GET、POST、PUT、DELETE、HEAD、OPTIONS?
GET 和 POST 的区别?
HTTPS 协议
:
HTTPS是一种通过计算机网络进行安全通信的传输协议,经由HTTP进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS使用的主要目的
是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性。
PS:
TLS
是传输层加密协议,前身是SSL协议
,由网景公司1995年发布,有时候两者不区分。
通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:
TCP:(Transmission Control Protocol )传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议
UDP:(User Datagram Protocol)用户数据报协议,是一种高速传输和实时性有较高的、无连接的、不可靠的 传输层协议。
TCP 和 UDP 的区别?
1、连接性:TCP 面向连接,UDP 无连接
2、可靠性:TCP 可靠的、保证消息顺序,UDP 不可靠(易丢包)、不能保证顺序
3、模式:TCP 流模式,UDP 数据报格式
4、资源损耗:TCP 更损耗数据
Socket:socket 是**“open—write/read—close”模式的一种实现,那么socket 就提供了这些操作对应的函数接口**。使用socket 需要注意:
? 多态表现为了三个方面:动态类型、动态绑定、动态加载。之所以叫做多态,是因为必须到运行时(run time)才会做一些事情。
动态类型:
编译器编译的时候是不能被识别的(如 id 类型),要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。
动态绑定 :
动态绑定(dynamic binding)貌似比较难记忆,但事实上很简单,只需记住关键词**@selector/SEL**即可。
而在OC中,其实是没有函数的概念的,我们叫**“消息机制”,所谓的函数调用就是给对象发送一条消息**。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法,最终形成的这个SEL变量就代表一个方法的引用。动态绑定的特定不仅方便,而且效率更高。
动态加载 :
让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。
? OC 不支持多继承,但是可以用 **代理(Delegate)**来实现多继承。runtime
消息转发等实现伪多继承
?
? 代理是一种设计模式,以**@protocol形式体现,一般是一对一传递**。
? 一般以weak关键词以规避循环引用。
? 使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。
如果实现通知机制?
键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法,直接或通过实例变量访问的机制。非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。
KVC 底层实现原理:
当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作:
1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值
2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(_key).如果有,直接给该成员属性进行赋值
3.如果没有成员属性_key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值
4.如果都没有,就会调用 valueforUndefinedKey 和setValue:forUndefinedKey:方法
KVC 使用场景:
OC 中,基本数据类型的默认关键字是atomic, readwrite, assign;普通属性的默认关键字是atomic, readwrite, strong。
读写权限:readonly,readwrite(默认)
原子性: atomic(默认),nonatomic。atomic读写线程安全,但效率低,而且不是绝对的安全,比如如果修饰的是数组,那么对数组的读写是安全的,但如果是操作数组进行添加移除其中对象的还,就不保证安全了。nonatomic禁止多线程,变量保护,提高性能。
引用计数:
retain/strong:表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。
assign:修饰基本数据类型,修饰对象类型时,不改变其引用计数,会产生悬垂指针,修饰的对象在被释放后,assign指针仍然指向原对象内存地址,如果使用assign指针继续访问原对象的话,就可能会导致内存泄漏或程序异常。这些数值主要存在于栈上。
weak:不改变被修饰对象的引用计数,所指对象在被释放后,weak指针会自动置为nil,不会造成野指针
copy:分为深拷贝和浅拷贝
可变对象的copy和mutableCopy都是深拷贝
不可变对象的copy是浅拷贝,mutableCopy是深拷贝
copy方法返回的都是不可变对象
库的本质
是可执行的二进制文件
,是资源文件
和代码编译
的一个集合
。根据链接方式不同,可以分为动态库和静态库,其中系统提供的库都属于动态库
。
静态库:
静态库形式:.a
和 .framework
,作用
是在进行链接生成可执行文件时
,从静态库文件中拷贝需要的内容
到最终的可执行文件中。
被多次使用就有多份冗余拷贝。
//在使用gcc编译时采用 -static选项来进行静态文件的链接:
gcc -c main.c
gcc -static -o main main.o
动态库:
静态库形式: .dylib
和 .framework
,并不在链接时将需要的二进制代码都拷贝到可执行文件中
,而是拷贝一些重定位和符号表信息
,当程序运行时需要的时候再通过符号表从动态库中
获取(动态加载)。 系统只加载一次
,多个程序共用,节省内存。
动静态库区别:
库名称 | 优点 | 缺点 |
---|---|---|
静态库 | 1. 目标程序没有外部依赖,直接就可以运行。2. 效率教动态库高。 | 1. 会使用目标程序的体积增大。因为它将需要用到的代码从二进制文件中拷贝了一份 |
动态库 | 1. 不需要拷贝到目标程序中,不会影响目标程序的体积 。2. 同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库 )。3. 编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。实现动态更新 。 | 1. 动态载入会带来一部分性能损失(可以忽略不计)2. 动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux lib not found 错误)。 |
推流端
(采集、美颜处理、编码、推流)、服务端处理
(转码、录制、截图、鉴黄)、播放器
(拉流、解码、渲染)、互动系统
(聊天室、礼物系统、赞)
推流端:实现推流
AVFoundation.Framework
,从 captureSession
代理回调中获取音视频。GPUImage
实现VideoToolBox
框架,音频使用 AudioToolBox
框架MPEG
,H.264
。x264
把视频元数据 YUV
/RGB
编码 H.264
什么是推流?
流媒体协议
发送到流媒体服务器
。muxing
(封装)FLV
或 TS
流媒体协议:
RTMP
,RTSP
, FLV
, HLS
。视频格式:
FLV
, TS
。音频格式:
mp3
, ACC
。librtmp
把数据推送到流媒体服务器(基于 RTMP 协议
)。HLS
:直接使用 HTTP 协议上传。服务端:实现 CDN 分发
播放端:实现播放
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 | -2025/1/31 9:24:51- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |