iOS已经到了小公司用不起,大公司不招的地步了。当然,也没有实习生要来学这个。整个移动端都太难了,今年大家都太难了。面试了一些公司,就自我总结一下吧。有空也能背一下。
基础
一个NSObject对象占用多少内存?
系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得) 在源代码中有对齐逻辑,如果字节小于8,会自动补齐到
对象的isa指针指向哪里?
在OC对象中可分为实例对象、类对象、元类对象。实例对象保存成员变量信息,类对象保存属性、对象方法、协议信息、成员变量描述信息,元类对象保存的是类方法等信息。
- instance(实例)对象的isa指向class(类)对象
- class(类)对象的isa指向meta-class对象
- meta-class(元类)对象的isa指向基类的meta-class对象
OC的类信息存放在哪里?
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法,存放在meta-class对象中
- 成员变量的具体值,存放在instance对象
KVO的原理是什么?(KVO的本质是什么?)
KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
原理: 利用RuntimeAPI动态生成一个子类,并且让instance实例对象的isa指向这个全新的子类(如:NSKVONotifying_YSPerson),当修改instance实例对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,该函数的内部实现如下
- willChangeValueForKey:
- 父类原来的setter
- didChangeValueForKey:内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
可以通过写【- (void)willChangeValueForKey:】方法来验证,会调用。当然手动添加上面的流程,也会触发KVO的方法调用。
直接修改成员变量会触发KVO吗?不会
通过KVC修改属性会触发KVO么? 会触发 手动触发KVO
[self.person willChangeValueForKey:@"age"]; [self.person didChangeValueForKey:@"age"];
KVC的赋值和取值过程是怎样的?原理是什么?
KVC(Key-value coding)键值编码。简单来说指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。是iOS开发中的黑魔法之一,很多高级的iOS开发技巧都是基于KVC实现的。 运用场景:
- 动态地设值和取值
- 用KVC来访问和修改私有变量
- model和字典互转
- 修改一些系统控件的内部属性,使用runtime来获取Apple不想开放的成员变量,利用KVC进行修改,比如自定义tabbar,textfield等,这个的应用也是比较常见
设值流程
取值流程
Category的实现原理
- 通过Runtime加载某个类的所有Category数据,Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中) 定义在objc-runtime-new.h中 图片1
Category如何添加成员变量?
添加关联对象
// 添加关联对象 void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy) // 获得关联对象 id objc_getAssociatedObject(id object, const void * key) // 移除所有的关联对象 id objc_getAssociatedObject(id object, const void * key)
常用用法:
static void *MyKey = &MyKey; objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, MyKey) // 使用get方法的@selecor作为key objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, @selector(getter))
关联对象原理
关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,里面是由HashMap来管理。
实现关联对象技术的核心对象有(objc4源码解读:objc-references.mm)
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
+initialize和+load的的区别
+load方法
会在runtime加载类、分类时调用,是根据方法地址直接调用,并不是经过objc_msgSend函数调用
每个类、分类的+load,在程序运行过程中只调用一次
调用顺序
- 先调用类的+load,按照编译先后顺序调用(先编译,先调用),调用子类的+load之前会先调用父类的+load
- 再调用分类的+load,按照编译先后顺序调用(先编译,先调用)
+initialize方法
会在类第一次接收到消息时调用,是通过objc_msgSend进行调用
调用顺序 先调用父类的+initialize,再调用子类的+initialize (先初始化父类,再初始化子类,每个类只会初始化1次)
block为什么要用copy修饰
block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区. 在 ARC 中写不写都行: 在 ARC 环境下,编译器会根据情況自动将栈上的 block 复制到堆上,比如以下情况:
-
block 作为函数返回值时
-
将 block 赋值给 __strong 指针时(property 的 copy 属性对应的是这一条)
-
block 作为 Cocoa API 中方法名含有 using Block 的方法参数时
-
block 作为 GCD API 的方法参数时
-
其中, block 的 property 设置为 copy, 对应的是这一条:将 block 赋值给 __strong 指针时。
换句话说:
对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。
weak属性的实现原理
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组
weak编译解析
首先需要看一下weak编译之后具体出现什么样的变化,通过Clang的方法把weak编译成C++
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")); id __attribute__((objc_ownership(weak))) obj1 = obj;
编译之后的weak,通过objc_ownership(weak)实现weak方法,objc_ownership字面意思是:获得对象的所有权,是对对象weak的初始化的一个操作。
weak是有Runtime维护的weak表
在runtime源码中,可以找到'objc-weak.h'和‘objc-weak.mm’文件,并且在objc-weak.h文件中关于定义weak表的结构体以及相关的方法
weak_table_t是一个全局weak 引用的表,使用不定类型对象的地址作为 key,用 weak_entry_t 类型结构体对象作为 value 。其中的 weak_entries 成员
/** * The global weak references table. Stores object ids as keys, * and weak_entry_t structs as their values. */ struct weak_table_t { weak_entry_t *weak_entries; //保存了所有指向指定对象的weak指针 weak_entries的对象 size_t num_entries; // weak对象的存储空间 uintptr_t mask; //参与判断引用计数辅助量 uintptr_t max_hash_displacement; //hash key 最大偏移值 };
weak全局表中的存储weak定义的对象的表结构weak_entry_t,weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。
那么 runtime 如何实现 weak 变量的自动置nil?
runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
这需要对对象整个释放过程了解,如下是对象释放的整体流程:
- 调用objc_release
- 因为对象的引用计数为0,所以执行dealloc
- 在dealloc中,调用了_objc_rootDealloc函数
- 在_objc_rootDealloc中,调用了object_dispose函数
- 调用objc_destructInstance
- 最后调用objc_clear_deallocating。
什么是runtime,平时项目中有用过么?
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行,OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数。平时编写的OC代码,底层都是转换成了Runtime API进行调用。
具体应用
- 发送消息
- 交换方法实现(交换系统的方法)
- 利用关联对象(AssociatedObject)给分类添加属性,给alertView添加传值
- 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
- 动态添加方法
- 字典转模型KVC实现
讲一下 OC 的消息机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend底层有3大阶段
消息发送(当前类、父类中查找)、动态方法解析、消息转发
RunLoop内部实现逻辑
RunLoop 顾名思义是运行循环,在程序运行过程中循环做一些事情。
RunLoop的基本作用
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
RunLoop与线程
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
RunLoop的运行逻辑
- 通知Observers:进入Loop
- 通知Observers:即将处理Timers
- 通知Observers:即将处理Sources
- 处理Blocks
- 处理Source0(可能会再次处理Blocks)
- 如果存在Source1,就跳转到第8步
- 通知Observers:开始休眠(等待消息唤醒)
- 通知Observers:结束休眠(被某个消息唤醒)
- 处理Timer
- 处理GCD Async To Main Queue
- 处理Source1
- 处理Blocks
- 根据前面的执行结果,决定如何操作
- 回到第02步
- 退出Loop
- 通知Observers:退出Loop
RunLoop休眠的实现原理
有个用户态和内核态,mach_msg(),等待消息
- 没有消息就让线程休眠
- 有消息就唤醒线程
CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
- RunLoop启动时只能选择其中一个Mode,作为currentMode。如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
- 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
- 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
model主要是用来指定事件在运行循环中的优先级的,分为:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode:启动时
- NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的 Mode 有两个(常见的2种Mode):
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)App的默认Mode,通常主线程是在这个Mode下运行
- NSRunLoopCommonModes(kCFRunLoopCommonModes)不是一个真正的Mode,比如定时器,在滑动的时候还能继续倒计时,解决运行模式的缺陷
RunLoop在实际开中的应用
- 控制线程生命周期(线程保活)
- 解决NSTimer在滑动时停止工作的问题
- 监控应用卡顿
- 性能优化
多线程锁有多少种,分别怎么使用
NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中
NSLock *lock = [[NSLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveMethod)(int); RecursiveMethod = ^(int value) { [lock lock]; if (value > 0) { NSLog(@"value = %d", value); sleep(2); RecursiveMethod(value - 1); } [lock unlock]; }; RecursiveMethod(5); });
这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。
在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。
所以,对上面的代码进行一下改造,
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
这样,程序就能正常运行了。
OC对象的内存管理
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
APP启动流程
APP的启动可以分为2种
- 冷启动(Cold Launch):从零开始启动APP
- 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP
APP启动时间的优化,主要是针对冷启动进行优化
通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
DYLD_PRINT_STATISTICS设置为1
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1
APP的冷启动可以概括为3大阶段
- dyld:dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)
- 装载APP的可执行文件,同时会递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理
- runtime。
由runtime负责加载成objc定义的结构,runtime所做的事情有
- 调用map_images进行可执行文件内容的解析和处理
- 在load_images中调用call_load_methods,调用所有Class和Category的+load方法
- 进行各种objc结构的初始化(注册Objc类、初始化类对象等等)
- 调用C++静态初始化器和__attribute__((constructor))修饰的函数
到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理
- main
所有初始化工作结束后,dyld就会调用main函数 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法
APP的启动优化
按照不同的阶段进行优化
- dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
- 减少C++虚函数数量
- Swift尽量使用struct
- runtime
- 用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、ObjC的+load
- main
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 按需加载
APP安装包瘦身
安装包(IPA)主要由可执行文件、资源组成
- 资源(图片、音频、视频等)
- 采取无损压缩
- 去除没有用到的资源: https://github.com/tinymind/LSUnusedResources
- 可执行文件瘦身
- 编译器优化,Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES
- 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions
- 利用AppCode(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code -编写LLVM插件检测出重复代码、未被调用的代码
离屏渲染怎么产生,怎么避免
在OpenGL中,GPU有2种渲染方式
- On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
- Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
离屏渲染消耗性能的原因
- 需要创建新的缓冲区
- 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
哪些操作会触发离屏渲染?
- 光栅化:layer.shouldRasterize = YES
- 遮罩:layer.mask
- 圆角:同时设置layer.masksToBounds = YES、layer.cornerRadius大于0。考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
- 阴影:layer.shadowXXX,如果设置了layer.shadowPath就不会产生离屏渲染
项目经验
1、卡顿监控怎么使用到项目中,都做了那些优化
第三方库
1、AFNetworking原理
2、SDWebimage原理
简单来说:
- 调用setImageWithURL,会先显示默认的图片(placeholderImage),然后根据URL开始处理图片
- 交给 SDImageCache 从缓存查找图片是否已经下载,如果存在,则直接显示出来
- 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。根据 URLKey 在硬盘缓存目录下尝试读取图片文件,如果读取到了图片,将图片添加到内存缓存中,再显示出来
- 如果内存和硬盘都没有该图片,则需要下载图片,共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片
- 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI
- 回调给 SDWebImageManager 告知图片下载完成,通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片
- 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程
参考:
https://www.jianshu.com/p/94f8bccc027d
https://www.jianshu.com/p/e5d583e81ac0
标签:总结,面试题,调用,对象,iOS,weak,objc,block,RunLoop From: https://www.cnblogs.com/jys509/p/16903585.html