27. SDWebImage是怎么做缓存的?
- 首先说,缓存采用了二级 缓存策略。 图片缓存的时候, 在内存有缓存, 在磁盘中也有缓存, 其中内存缓存是用NSCache做的 (下面会有NSCache的说明)。
一、如何做缓存的步骤:
0、下载图片
1、将图片缓存在内存中
2、判断图片的格式png或jpeg,将图片转成NSData数据
3、获取图片的存储路径, 其中图片的文件名是通过传入Key经过MD5加密后获得的
4、将图片存在进磁盘中。
二、如何获取图片的?
1、在内存缓存中找
2、如果内存中找不到, 会去默认磁盘目录中寻找, 如果找不到,在去自定义磁盘目录中寻找
3、如果磁盘也找不到就会下载图片
4、获取图片数据之后, 将图片数据从NSData转化UIImage。其中转化根据图片的类型进行转化
5、默认对图片进行解压缩,生成位图图片
6、将位图图片返回
三、图片是如何被解压缩的?
1、判断图片是否是动态图片,如果是,不能解压缩
2、判断图片是否透明,如果是,不能解压缩
3、判断图片的颜色空间是不是RGB如果不是、不能解压缩
4、根据图片的大小创建一个上下文
5、将图片绘制在上下文中
6、从上下文中读取一个不透明的位图图像,该图像就是解压缩后的图像
7、将位图图像返回
接上说 NSCache
- 这个NSCache说白了就是做缓存专用的一个系统类
- 类似可变字典一样,但是NSCache是线程安全的, 系统类自动做好了加锁和释放锁等一系列的操作, 还有一个重要的是如果内存不足的时候NSCache会自动释放掉存储的对象,不需要开发者手动干预。
- 来看一眼NSCache提供的属性和相关方法
//名称
@property (copy) NSString *name;
//NSCacheDelegate代理
@property (nullable, assign) id delegate;
//通过key获取value,类似于字典中通过key取value的操作
- (nullable ObjectType)objectForKey:(KeyType)key;
//设置key、value
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
/*
设置key、value
cost表示obj这个value对象的占用的消耗?可以自行设置每个需要添加进缓存的对象的cost值
这个值与后面的totalCostLimit对应,如果添加进缓存的cost总值大于totalCostLimit就会自动进行删除
感觉在实际开发中直接使用setObject:forKey:方法就可以解决问题了
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
//根据key删除value对象
- (void)removeObjectForKey:(KeyType)key;
//删除保存的所有的key-value
- (void)removeAllObjects;
/*
当NSCache缓存的对象的总cost值大于这个值则会自动释放一部分对象直到占用小于该值
非严格限制意味着如果保存的对象超出这个大小也不一定会被删除
这个值就是与前面setObject:forKey:cost:方法对应
*/
@property NSUInteger totalCostLimit; // limits are imprecise/not strict
/*
缓存能够保存的key-value个数的最大数量
当保存的数量大于该值就会被自动释放
非严格限制意味着如果超出了这个数量也不一定会被删除
*/
@property NSUInteger countLimit; // limits are imprecise/not strict
/*
这个值与NSDiscardableContent协议有关,默认为YES
当一个类实现了该协议,并且这个类的对象不再被使用时意味着可以被释放
*/
@property BOOL evictsObjectsWithDiscardedContent;
@end
//NSCacheDelegate协议
@protocol NSCacheDelegate
@optional
//上述协议只有这一个方法,缓存中的一个对象即将被删除时被回调
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end**
countLimit注意一下这个属性, 这个属性就是设置最大缓存数量,啥意思呢? 这玩意就和栈差不多, 先进先出(叫什么FIFO?)原则。比如你countLimit设置为5 那么当你缓存第6个对象的时候, 原本第一个就被移除了。 所以这便就有有一个风险,也可能会是面试点,为什么,通过key去取值的时候,一定要判断一个获取的对象是否为nil?答:就因为很有可能某些对象被释放(顶)掉了。
又又又可能出现的面试题!NSCache里面缓存的对象,在什么场景下会被释放?
- 回答之前,先说一情况,在某C中创建了NSCache对象,点击手机的Home或者任何方式进入后台,会发现NSCache中的代理方法被执行了,于是NSCache对象会释放掉所有对象,还有的是,如果发生内存警告也会释放掉所有对象。所以, 这道题应该如下这么回答!
- NSCache自身释放了,其中存储的对象也就释放了。
- 手动调用释放方法removeObjectForKey、removeAllObjects
- 缓存对象个数大于countLimit
- 缓存总消耗大于totalCostLimit
- 程序进入后台
- 收到内存警告
28.SDWebImage实现原理是什么? 它是如何解决tableView的复用时出现图片错乱问题的呢
- 原理如上,
- 错乱是在UIImageView+WebCache文件中这个方法每次都会调用 [self sd_cancelCurrentImageLoad];
29. 为什么刷新UI要在主线程操作
UIKit并不是一个线程安全的类,所以涉及多个线程同时对UI进行操作会造成影响。
为什么不把UIKit框架设置为线程安全呢?
因为线程安全需要加锁,我们都知道加锁就会消耗性能,影响处理速度,影响渲染速度,我们通常自己在写@property时都会写nonatomic来追求高性能高效率。
假设能够异步设置view的属性,那我们究竟是希望这些改动能够同时生效,还是按照各自runloop的进度去改变这个view的属性呢?
假设UITableView在其他线程去移除了一个cell,而在另一个线程却对这个cell所在的index进行一些操作,这时候可能就会引发crash。
如果在后台线程移除了一个view,这个时候runloop周期还没有完结,用户在主线程点击了这个“将要”消失的view,那么究竟该不该响应事件?在哪条线程进行响应?
在Cocoa Touch框架中,UIApplication初始化工作是在主线程进行的。而界面上所有的视图都是在UIApplication 实例的叶子节点(内存管理角度),所以所有的手势交互操作都是在主线程上才能响应
30. RunTime
类的结构体:
//Class也表示一个结构体指针的类型
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
分类结构体
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 对象方法
struct method_list_t *classMethods; // 类方法
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties; // 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
引申1. class_copyIvarList与class_copyPropertyList的区别?
- 1.class_copyIvarList:能够获取.h和.m中的所有属性以及大括号中声明的变量,获取的属性名称有下划线(大括号中的除外)。
- 2.class_copyPropertyList:只能获取由property声明的属性,包括.m中的,获取的属性名称不带下划线。
引申2. class_ro_t和class_rw_t的区别?
- class_rw_t提供了运行时对类拓展的能力,
- class_ro_t存储的大多是类在编译时就已经确定的信息。
- 二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。简单的说class_rw_t存储列表使用的二维数组,class_ro_t使用的一维数组。
- class_ro_t存储于class_rw_t结构体中,是不可改变的。保存着类的在编译时就已经确定的信息。
- 运行时修改类的方法,属性,协议等都存储于class_rw_t中
31. NSNotification
- NSNotificationCent 子线程中发出通知,也要在主线程中刷新UI
// 比如
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新UI
});
- NSNotificationCenter用完之后不移除, 会崩溃么?
- 有时候会导致crash。比如在通知事件中处理数据或者UI事件,但是由于通知的不确定性造成事件的不确定,有异步操作在通知事件中处理等都可能造成崩溃。
- 而且通知的崩溃很难检测。
32. 什么情况使用 weak 关键字,相比 assign 有什么不同?(轮回系列)
- weak 这个词儿解决了一件事情,就是内存的事情
- 在ARC中weak的出现解决了一些循环引用的问题, 比如delegate, xib连线出来的控件一般也是weak(也可以用strong )
- weak表明了一种“非拥有的关系”,不保留新值,也不释放旧值。
- assign也是如此,但常用的assign一般用于基本数据类型(CGFloat 或 NSlnteger等)
- assign可以用于非OC对象,也可以用于OC对象(MRC时代使用), 但是weak必须用在OC对象。
引申 1.关键字copy 的用法?
- block用Copy是MRC时代留下来的传统。在MRC中方法内部的block是在栈区的, 使用copy可以把它放到堆区。 在ARC中写不写都行,用Strong也是可以的。
- NSString、NSArray、NSDictionary也经常使用copy, 因为里面有对应的可变的子类型,为了确保安全性, 建议使用copy修饰
引申 2.@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的。
- @property = ivar(实例变量) + getter + setter
- 自动合成
33. 说说内存管理?
- 其实遇到这道题,挺纠结的,有些TMD面试官就是习惯搞人,从这个玩意里面 能往死给你嗑! 你要看过相关内存管理的详细原理,你会发现这里面的C++操作很多,没学过C++的人能看个八九不离十,可是也只是能说个大其概,但是内部细节还是得用C++来说,废话不多说, 直接上说所谓得面试答案。
- 粗糙版本这么回答,
- 版本一: 内存中每一个对象都有一个属于自己的引用计数器。当某个对象A被另一个对象引用时,A的引用计数器就+1,如果再有一个对象引用到A,那么A的引用计数器就再+1。当其中某个对象不再引用A了,A的引用计数器会-1。直到A的引用计数减到了0,那么就没有人再需要它了,就是时候把它释放掉了
- 版本二:对象通过 alloc copy new 生成得得对象在MRC年代需要手动管理内存, 利用得技术是returnCount引用计数器,来管理对象得释放时机,alloc创建对象引用计数器 + 1, retain持有关系 引用计数器 +1,release 引用计数器 - 1。 如果当前对象得returnCount = 0 对象就会被在dealloc方法里面适当时机进行释放(啥时候释放?)如果当前returnCount大于0得时候,就会一直被持有。
- 稍微详细版本的,首先当 alloc copy new 生成得对象里面 在内部底层源码也同时和当前对象相关联得SideTable, 其内部有三个属性, 一个是一把自旋锁,一个是引用计数器相关,一个是维护weak生命得属性得表, 其中retain、release 对利用键值对会对当前对象得引用计数器进行加减操作(位移),如果当前引用计数器为0得时候,其dealloc内部会删除当前的引用计数器,并且释放当前对象。
- 详情请查看https://www.jianshu.com/p/ef6d9bf8fe59
杂项
1、imageName与imageWithContentsOfFile区别?
imageWithContentsOfFile: 加载本地目录图片,并不会缓存,占用内存小, 不能加载image.xcassets里面的图片资源。 相同的图片会被重复加载到内存中
imageName:加载到内存中, 会缓存起来, 占用内存较大,相同的图片不会被重复加载到内存当中,会读取image.xcassets的图片图片资源。
如果不断重复读取同一个图片,则使用imageName
如果不需要重复读取同一个图片,并且需要低内存,则使用imageWithContentsOfFile
2.IBOutlet连出来的视图属性为什么可以被设置成weak?
因为链接之Xcode 内部把链接的控件 放进了一个_topLevelObjectsToKeepAliveFromStoryboard的私有数组中,这个数组强引用这所有top level的对象 所以用weak也无伤大雅。
-
- id 为什么不能用点语法?
点语法就是setter和getter方法, 然而id类 无法确定所指的类是什么类型, 寻不到setter个getter方法,id类型的对象 只能用【】方法调用方法
-
4.id和NSObject的区别?
- id是struct objc_object结构体指针,可以指向任何OC对象,当然不包括NSInteger等类型,因为这些数据类型不是OC对象。
另外OC的基类不止有NSObject一个,还有个NSProxy虚类。所以说id类型和NSObject并不是等价的。
-
5 . OC中 Null 与 nil的区别
- NULL是指指针是空值,用来判断C 指针;
- nil是指一个OC对象(指针)为空;
- Nil是指一个OC类为空;
- NSNull则用于填充集合元素;这个类只有一个方法null,并且是单例的;
- 6 . 自旋锁和互斥锁
- 相同点:都能保证同一时间只有一个线程访问共享资源,都能保证系统安全
- 不同点:
互斥锁:如果共享数据已经有了其他线程加锁了,线程会进行休眠状态等待锁,一旦被访问的资源被解锁,则等待资源的线程会被唤醒。信号量dispatch_semaphore 为互斥锁 @synchronized是NSLock的封装 属于互斥锁 互斥锁一般用于等待时间较长的情况
**适用于**:线程等待锁的时间较长
自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。OSSpinLock 属于自旋锁 自旋锁一般用于时间较短的情况,OSSpinLock
**适用于**:线程等待锁的时间较端
7 . 进程和线程的区别
进程是指在系统中正在运行的一个应用程序
线程是进程中的一个实体,一个进程想要执行任务, 必须至少有一条线程,应程序启动的时候会默认开启一条线程,也就是主线程
一个进程拥有多个线程
-
8 LayoutSubviews和drawRect调用时机
LayoutSubviews调用时机- init初始化不会调用LayoutSubviews方法
- addsubView 时候会调用
- 改变一个View的frame的时候调用
- 滚动UIScrollView导致UIView重新布局的时候会调用
- 手动调用setNeedsLayout或者layoutIfNeeded
drawRect调用时机
- drawRect 掉用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.、
- 9 cocoaPods里面pod install和update的区别?
**pod install **
- 一般是第一次想要为项目添加pod的时候使用的,当然也可以在添加和移除库使用
- 每次pod install的时候,pod install 回为每一个安装的pod库在Podfile.lock文件中写入其版本号,并且锁定当前版本号。
- 如果pod install的时候,不会更新其版本库,而是去下载新的或者移除当前版本
pod update- 当执行了pod update的时候,cocoaPods不会考虑Podfile.lock中的版本。直接去更新当前所有的库到最新,然后Podfile.lock会更新这一次的版本号。
- 10 frame和masonry哪个性能好?为什么
- 有的相对布局最终都会转换成Frame绝对布局 中间多了一层转换的操作
- 11 . iOS从iOS9 - 13的特性
iOS9
从HTTP升级到HTTPS
App瘦身 下面有讲 这里不赘述( App瘦身 )
新增UIStackView
iOS10
新增通知推送相关的操作。自定义通知弹窗,自定义通知类型(地理位置,时间间隔,日历等)
iOS11
无线 调试
齐刘海儿,导航条,安全距离等
iOS12
启动速度优化
应用启动速度提升40%
键盘响应速度提升50%
相机启动速度提升70%
iOS13
黑暗模式 详情请查阅 https://www.jianshu.com/p/0da3b107f06c
二、App包以及启动过程
App瘦身
1、App如何瘦身?
- 删除陈旧代码、删除陈旧xib/sb,删除无用的图片资源(检测未使用图片的工具LSUnusedResources )
- 无损压缩图片,本地音视频压缩。以直接减少图片大小
- 使用webP格式的图片(加载速度比较慢,但可以达到瘦身的效果)
- 减小类名称的长度(高性能的话可以试一试)
- 减少使用静态库
- 一些主题类的东西提供下载功能,不直接打包在应用包里面,按需加载资源
- iOS9 之后的新特性 应用程序切片(App Slicing)、中间代码(Bitcode)和按需加载资源(On Demand Resources)
Slicing: 这个过程是iOS9出来之后 不需要程序员干预的一个瘦身的过程,简单来说就是我们再上传IPA包到iTunes Connect,然后AppStore会对app进行切片,切成特定的机型想要的数据,比如@3x给max用,@2x就自动剔除了。 是一个自动的过程、
Bitcode:是一种中间码,如果配置了Bitcode(Xcode7以后默认开启)的程序会在App Store Connect上被重新编译等一系列操作,进而苹果内部会对可执行文件进行优化,也就是说不需要我们干预什么东西,也操作不了, 如果后面苹果有更牛逼的优化操作,也是苹果的事情, 跟我们个人开发者一毛钱关系没有。
On Demand Resources 按需加载, 是程序员自己手动操作,说白了就是在用的时候去下载某些资源, 但是我们自己在配置的时候都需要配置,要额外写一些代码啥的,等我们提交到市场的时候, 苹果内部会把我们按需加载的资源从包里面做了一些抽离操作啥的, 让我们的包在下载的时候更小,举个例子,就是吃鸡里面沙漠地图如果玩家不自己下载, 就玩不了沙漠。
on-demond resource(ODR)具体请查看原理版本:https://www.jianshu.com/p/bacedd8a3ad8
或者详细使用版本:http://www.cocoachina.com/articles/12155
关于 slicing, bitcode, on-demond resource(ODR)的参考资源https://blog.csdn.net/zhuod/article/details/70051514?utm_source=blogxgwz6
2、app启动时候都经历了什么?
启动分为两种。 一种是之前启动过,按了一下home键,然后再点启动,这个启动叫热启动,另外就是第一次启动app,或者启动杀死之后的app 叫做冷启动
根据info.plist里面的设置加载,建立沙箱,权限检查等
加载可执行文件
加载动态库
objc运行时的初始化处理(类的注册,category注册,selector唯一性检查等等)
初始化,包括+load方法
执行main函数
Application 初始化,到 applicationDidFinishLaunchingWithOptions 执行完
渲染屏幕,到viewDidAppear 执行完毕,展现给用户
- mian之前
根据info.plist里面的设置加载,建立沙箱,权限检查等
加载可执行文件
加载动态库
objc运行时的初始化处理(类的注册,category注册,selector唯一性检查等等)
初始化,包括+load方法
- mian之后
- 如图
- 加载流程如下:
[图片上传失败...(image-8500a-1597126379928)]
3、优化启动时间
- 启动时间是用户点击App图标,到第一个界面展示的时间。
注意:启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。
- 以main函数作为分水岭,启动时间其实包括了两部分:
- main函数之前(分析并加载动态库,注册需要的类(包括系统的类),Category中的方法也会注册到对用的类中,执行必要的初始化方法( +load方法)等等
- main函数到第一个界面的viewDidAppear:。
- 所以,优化也是从两个方面进行的,个人建议优先优化后者,因为绝大多数App的瓶颈在自己的代码里。
mian函数之前的启动优化
- 减少动态库的数量(这是目前为止最耗时的了, 基本上占了95%以上的时间)
- 合并动态库,比如自己写的UI控件合并成自己的UIKit
- 确认动态库是optional还是required。如果该Framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
- 合并Category(UIView+Frame,UIView+AutoLayout合并成一个)
- 将不必需在+load方法中做的事情,延时放到+initialize。
mian函数之后的启动优化
首先分析一下从main函数开始执行,到第一个页面显示, 这段时间做了哪些事情
- 执行didFinishLaunchingWithOptions方法
- 初始化Window,初始化基础ViewContreoller(一般是UINavigationController+UITabViewController)
- 获取数据(本地和远程)
- 最后展示给用户
- 减少创建线程(高性能iOS开发一书中提到,线程不仅仅有创建时的时间开销,还会消耗内核的内存,即应用的内存空间。 每个线程大约消耗 1KB 的内核内存空间。线程创建的耗时(不包含启动时间),其区间范围在 4000~5000 微秒,即 4~5 毫秒。创建线程后启动线程的耗时区间为 5~100 毫秒,平均大约在 29 毫秒。这是很大的时间开销,若在应用启动时开启多个线程,则尤为明显。线程的启动时间之所以如此之长,是因为多次的上下文切换所带来的开销。所以线程在开发过程中也避免滥用)
- 合并或者删减不必要的类(或者分类)和函数objc的类越多,函数越多启动越慢
- 在设计师可接受的范文尽量使用小的图片
- AppDelegate
通常优化的一般来说,还是从AppDelegate先入手优化
didFinishLaunchingWithOptions
applicationDidBecomeActive
优化的核心思想就是,能延时的延时, 不能延时的尽量放到后台去优化。
- 日志、统计等必须在 APP 一启动就最先配置的事件。仍然把它留在 didFinishLaunchingWithOptions 里启动。
- 项目配置、环境配置、用户信息的初始化 、推送、IM等事件,这些功能在用户进入 APP 主体的之前是必须要加载完的,把他放到广告页面的viewDidAppear启动。
- 其他 SDK 和配置事件,由于启动时间不是必须的,所以我们可以放在第一个界面的 viewDidAppear 方法里,这里完全不会影响到启动时间。
- 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
- 尽量不要在didFinishLaunchingWithOptions 里面创建和开启多线程
参考文献https://www.jianshu.com/p/f40fdd8799b8
其文章内部作者谈到了美团关于启动优化的相关分析,看似似曾相似,没记错的画《高性能iOS应用开发》这本书就是美团这几个哥们儿翻译的吧,实现方式和书中颇为相似。
3、App电量消耗
- 1.定位
- 2.网络请求
- 3.CPU处理
- 4.GPU处理
- 5.Bluetooth
定位优化
1.尽量不要实时更新
2.定位精度尽量不要太高
网络优化
1.减少、压缩网络数据
2.能使用缓存就使用缓存,减少网络请求
3.断点续传
4.批量传输
5.设置适合的超时时间,用户可以取消耗时的网络请求
6.网络不可用时就不要再执行网络请求了
CPU/GPU优化
相关离屏渲染操作尽量避免
内存管理处理好
使用懒加载
使用绘制
图片与imageView相同大小避免多余运算
Timer的时间间隔不宜太短,满足需求即可
线程适量,不宜过多,不要阻塞主线程
适当使用多线程
减少视图刷新:确保必要的时候才刷新,能刷新1行cell最好只刷新一行;
为了优化耗电我们还可以做:
1.尽量不要使用定时器
2.优化I/O操作(文件的读写操作)
2.1最好不要频繁读写小数据,最好批量读写
2.2数据量比较大的时候可以考虑使用数据库
2.3读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
高性能iOS应用开发中提到一下几点
- 1、CPU优化
- 数据处理(例如文本格式优化)
- 待处理的数据大小----更大的显示屏允许软件在单个视图中展示更多的信息,但这也意味着要处理更多的数据
- 处理数据的算法和数据结构
- 执行更新的次数,尤其是在数据更新之后,触发应用的状态或者UI进行更新(比如刷新单行cell)
- 服务器中的数据尽量不要在客户端上处理(例如服务器字符串,在客户端进行拆分操作)
- 按需加载(例如tableViewcell 不需要一下子全部渲染,快速滑动的时候 过程中的留白处理。)
- 2、网络
- 在进行网络请求之前,先检查是否有网络连接。(没网络的时候,不要请求网络)
- 避免没有连接WiFi的情况下进行高带宽的消耗操作(因为3G、4G等手机网络耗电量远大于WIFi信号),例如视频流在4G或者非Wifi情况下应该给出响应的提示。
- 3、定位
- 尽量不要实时更新
- 定位精度尽量不要太高