iOS面试了20几家总结出来的面试题(二)
收录:原文地址
原作者:执笔续春秋
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<NSCacheDelegate> 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 <NSObject>
@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中
- 另外整理了一份 2020年《大厂最新常问iOS面试题+答案》,有需要,直接加iOS技术交流群:789143298,免费获取;群内更有内推机会!
- 【点击快速加入】:与更多iOS开发者一起交流
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内部会删除当前的引用计数器,并且释放当前对象。
- 详情请查看www.jianshu.com/p/ef6d9bf8f…
杂项
-
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
黑暗模式 详情请查阅 www.jianshu.com/p/0da3b107f…
二、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)具体请查看原理版本:www.jianshu.com/p/bacedd8a3…
或者详细使用版本:www.cocoachina.com/articles/12…
关于 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之后
- 如图
- 加载流程如下:
图丢了!!!!!百度去吧!
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、定位
- 尽量不要实时更新
- 定位精度尽量不要太高
三、算法
定义相关
- 链表和数组的区别是什么? 链表和字典的区别是什么?
数组在内存中是逐个存放的,链表每隔节点没有相对固定的位置关系 数组被声明后,大小就固定了,不能进行动态扩充。 链表可以动态生成节点,并且添加到已有的链表后面 数组存在越界问题,链表则不存在 数组的插入删除的时间复杂度是O(n),链表O(1) 数组的查询下标时间复杂度为O(1), 链表为O(n) 根据值查询的时间复杂度,链表和数组都是O(n)
- 如何检测链表中是否有环?
思路 假设有两个学生A和B在跑道上跑步,两人从相同起点出发,假设A的速度为2m/s,B的速度为1m/s,结果会发生什么? 答案很简单,A绕了跑道一圈之后会追上B! 将这个问题延伸到链表中,跑道就是链表,我们可以设置两个指针,a跑的快,b跑的慢,如果链表有环,那么当程序执行到某一状态时,a==b。如果链表没有环,程序会执行到a==NULL,结束。
1、 调换A和B
// int a = 10;
// int b = 20;
//
// a = a + b;
// b = a - b;
// a = a - b;
//
// NSLog(@"a = %d , b = %d", a, b);
//
//
// a = a*b;
// b = a/b;
// a = a/b;
//
// NSLog(@"1 ===== : a = %d , b = %d", a, b);
2、最大公约数
// int n = 20,v = 30,temp = 0,max,min;
//
// if (n>v) {
// max = n;
// min = v;
// } else {
// max = v;
// min = n;
// }
//
//
// while (min != 0) {
// temp = max - min;
// max = min;
// min = temp;
// }
//
// NSLog(@"%d", max);
3、打印2 - 100 的素数(质数) 除了1和自身被整除的.
// NSMutableArray *primeNumberArray =[NSMutableArray array];
// for(int i=2; i<=100; i++) {
//
// NSInteger n = 0;
//
// for(int j = 1; j <= i; j++) {
//
// if(i % j == 0) {
// n = n + 1;
// }
// }
//
// if(n == 2) {
// [primeNumberArray addObject:@(i)];
// }
// }
//
// NSLog(@"primeNumber = %@",primeNumberArray);
//
4、 字符串倒叙
// NSString *string = @"hei Son 我是你father";
// NSMutableString *string1 = [NSMutableString string];
// for (NSInteger i = string.length; i>0; i--) {
// [string1 appendString:[string substringWithRange:NSMakeRange(i -1,1)]];
// }
//
// NSLog(@"%@", string1);
//
5、 寻找出字符串中有那些中文
// for (int i = 0; i < string.length; i++) {
// NSString *str1 = [string substringWithRange:NSMakeRange(i, 1)];
// const char *cStr = [str1 UTF8String];
// if (strlen(cStr) == 3 ) { // oc中 中文三个字节
// NSLog(@"%@", str1);
// }
// }
6. 排序
- 冒泡排序
比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
for (int i = 0; i<result.count-1; i++) {
for (int j = 0; j<result.count-1-i; j++) {
NSInteger left = [result[j] integerValue];
NSInteger right = [result[j+1] integerValue];
if (left>right) {
[result exchangeObjectAtIndex:j withObjectAtIndex:j+1];
}
}
}
NSLog(@"%@",result);
时间复杂度O(n^2)
- 选择排序
选择排序就是通过遍历数组找出每次遍历数组的最小元素的下标,然后将其按顺序从第一位依次排列
//self.array = @[@2,@4,@3,@1];
NSMutableArray *mutableArray = [self.array mutableCopy];//oc数组中不能存储基本数据类型,所以快速赋值完成后,系统默认数组元素为NSNumber类型
if (mutableArray == nil || [mutableArray count] == 0)
{
return;
}
for (int i = 0; i < [mutableArray count]; i++)
{
NSInteger minIdx = i;//默认最小值的索引为i
for (int j = i+1; j < [mutableArray count]; j++)//通过循环寻找当前数组中最小值的索引值
{
if (NSOrderedAscending == [mutableArray[j] compare:mutableArray[minIdx]])//NSNumber类判断大小方法,这句话的意思是当mutableArray[j] <mutableArray[minIdx]时
{
minIdx = j;//更新数组中最小值的索引值
}
}
[mutableArray exchangeObjectAtIndex:i withObjectAtIndex:minIdx];//将每次循环结束后找到的最小值交换到数组的第i位
NSLog(@"%@",mutableArray);
}
}
时间复杂度为O(n)。最坏情况下仍为O(n^2)
- 升序/降序
NSMutableArray *priceArray = [NSMutableArray arrayWithObjects:@"0.2",@"5",@"44",@"67",@"98.5",@"1.55", nil];
[priceArray sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
if ([obj1 integerValue] < [obj2 integerValue]){
return NSOrderedAscending;
} else {
return NSOrderedDescending;
}
}];
这里再次得到的priceArray即为升序排列的数组;
若果想要得到降序的调换一下return的位置即可
7 寻找最近的公共View
// 所有View的父View
+ (NSArray *)superViews:(UIView *)view{
if (view==nil) {
return @[];
}
NSMutableArray *result = [NSMutableArray array];
while (view!=nil) {
[result addObject:view];
view = view.superview;
}
return [result copy];
}
// 相互对比两个节点中的view,出现一样就返回
+ (UIView *)commonView_1:(UIView *)viewA andView:(UIView *)viewB{
NSArray *arr1 = [self superViews:viewA];
NSArray *arr2 = [self superViews:viewB];
for (NSUInteger i =0; i<arr1.count; ++i) {
UIView *targetView = arr1[i];
for (NSUInteger j=0; j<arr2.count; ++j) {
if (targetView == arr2[j]) {
return targetView;
}
}
}
return nil;
}
// 利用NSSet中的hash表,可以将上面代码进行进一步优化
+ (UIView *)commomView_2:(UIView *)viewA andView:(UIView *)viewB{
NSArray *arr1 = [self superViews:viewA];
NSArray *arr2 = [self superViews:viewB];
NSSet *set = [NSSet setWithArray:arr2];
for (NSUInteger i =0; i<arr1.count; ++i) {
UIView *targetView = arr1[i];
if ([set containsObject:targetView]) {
return targetView;
}
}
return nil;
}
8.数组题:如何在有序数组中找出和等于给定值的两个元素?
NSArray *arr = @[@"1", @"12", @"13", @"23", @"31", @"43", @"52", @"66", @"88", @"111", @"127", @"199"];
[self confirmNumbers:arr total:199];
----
- (void)confirmNumbers:(NSArray *)array total:(NSInteger)totalNmuber {
if (array.count <= 1) { return; }
NSInteger tempAddCount = 0;
NSInteger tempDeleltCount = 0;
for (int i = 0; i <= array.count; i++) {
NSInteger tNamber = [array[tempAddCount] integerValue] + [array[array.count-1-tempDeleltCount] integerValue];
if (tNamber == totalNmuber) {
NSLog(@"%ld, %ld, 第一个元素%@ - 后面的元素%@", (long)tempAddCount, (long)tempDeleltCount, array[tempAddCount] ,array[array.count-tempDeleltCount-1]);
break;
} else if (tNamber < totalNmuber) {
tempAddCount ++;
} else {
tempDeleltCount ++;
}
if (i == array.count - 1) {
NSLog(@"啥都没匹配着");
break;
}
}
}
9 用递归写一个算法,计算从1到100的和。
NSLog(@"%ld", [self getSumResult:100]);
- (NSInteger)getSumResult:(NSInteger)number {
if (number <=0 ) {
return number;
}
return number + [self getSumResult:number - 1];
}
// 递归效率差的原因是 每一次调用函数(自己)都是要有内存开销的,影响CUP的效率
10.打乱一个数组
NSArray* arr = @[@"1",@"2",@"3"];
arr = [arr sortedArrayUsingComparator:^NSComparisonResult(NSString *str1, NSString *str2) {
int seed = arc4random_uniform(2);
if (seed) {
return [str1 compare:str2];
} else {
return [str2 compare:str1];
}
}];
三、软技术篇 ###1.开发过程中, 你碰到那些技术难点?是怎么解决的?
- 其实这个题主要还是面试官看想了解你的真实项目经验, 如果你回答的东西,根本就算不上是什么技术问题,而是基础问题那估计也没什么聊下去的必要了, 举个例子,你回答的是“我在开发过程中总是发现UITableView这个控件写起来比较麻烦, 而且还总是报数组越界的问题” 那么对不起, 你最多能找一个实习的工作。因为你能说出这个问题,我估计你上面的硬核面试题,一半儿都不知道咋回事!
- 其实这题也没那么高大上,说一下你真实开发过程中遇到的难点,说的越高大上越好, 最好是把面试官说懵逼了,当然如果你有自信的话,最好别夸大,万一你遇上一个恰好在这个领域人家更牛逼呢?
- 举个例子“我在开发我们项目的时候, 涉及到图像处理的问题,就比如说现在网络上比较火的用SDWebImage下载超清大图的时候出现的崩溃问题,因为decodeImageWithImage 这个方法用于对图片进行压缩并且缓存起来,以保证tableview/collectionview交互更加流畅,但是如果用此方法加载超清大图的时候, 会适得其反,有可能导致上G的内存消耗, 解决办法是对于高清图片,应该放在图片解压之后,禁止缓存解压后的数据。 代码如下”
- SD4.X的解决办法
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
- SD5.0 及以上的解决办法
SDWebImageAvoidDecodeImage添加了这个枚举,意思是在子线程程解压缩图片
[self.imageView sd_setImageWithURL:self.url placeholderImage:[UIImage imageNamed:@"logo"] options:SDWebImageAvoidDecodeImage];
-
更多详情请查看 也必须看!
-
在举个例子“由于在开发过程中,用到的所有图片必须是原图, 问题就出现了, 如果几张图片是超清的超大图片,把这些图片全部渲染到一个画布中并且进行随机形状的超高清拼图, 这个过程会出现一个奇怪的问题,就是绘制的结果是大概率会变成 一张纯白色的没有任何图案的图片,出现的原因是因为在App内部如果正在运行的内存达到一定的值得时候绘制图像的上下文就会获取一个空白的图片,解决办法因为是概率事件所以内部做了一个循环渲染的机制,在特定次数范围内,如果出现绘制成功的话返回正常的图片,如果没有正确绘制,则做一个内部的提示语App内部没有任何反应, 所以如果制作跟图片相关的项目,特别涉及到自定义系统相册的功能,最好优化好内存问题,因为内存优化不好,导致的问题有很多系统层级的BUG。而且很难找到问题的原因。
-
** 这就把面试官引向了一个优化内存的事情。比如优化内存的工具, 检测内存泄漏,循环引用的工具等等。 下面会一一介绍。**
2.过程中, 你用过什么调试工具?
- instrument
Leaks(泄漏):一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录;
- locations(内存分配):跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史;
结语
这是陆陆续续面试中总结出来的, 总之,有的问题回答的比较捡漏, 有的问题模棱两可,有些面试官的反馈比较扎实,有些面试官说回答的泰国笼统不够细致,反正这东西见仁见智吧。
此文章基本上不会添加新的问题了,如果有错误我会及时改正,避免耽误有需要帮助的小伙伴! 最后一次更新时间:2020年07月21日
推荐👇:
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:789143298 ,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!