1.iOS Runtime面试题(什么是method swizzling(俗称黑魔法))
什么是method swizzling(俗称黑魔法)
简单说就是进行方法交换
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。
换方法的几种实现方式
* 利用 method_exchangeImplementations 交换两个方法的实现
* 利用 class_replaceMethod替换方法的实现
* 利用 method_setImplementation 来直接设置某个方法的IMP
[图片上传失败...(image-46db5a-1581791102546)]
2.iOS Runtime面试题(什么时候会报unrecognized selector的异常?)
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
(1)runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行
(2)开始消息转发
1.先征询接收者能否动态添加方法(动态方法解析)
2.有没有其他对象能够处理
- (id)forwardingTargetForSelector:(SEL)aSelector{
// 当然这里可以通过判断方法名来决定转发的对象
//NSString *seletorName = NSStringFromSelector(aSelector);
//if([seletorName isEqualToString : @"xxx"])
ClassB *b = [[ClassB alloc] init];
if([b respondsToSelector:aSelector]){
return b;
}
return nil; }
3.运行期系统把与消息有关的全部细节封装到NSInvocation(完整消息转发)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector]; // 当然这里可以通过判断方法名来决定转发的对象
//NSString *seletorName = NSStringFromSelector(aSelector);
//if([seletorName isEqualToString : @"xxx"])
//ClassB *b = [[ClassB alloc] init];
if(signature == nil){
signature = [_b methodSignatureForSelector:aSelector];
}
NSUInteger argCount = [signature numberOfArguments];
for (NSInteger i=0; i<argCount; i++) {
NSLog(@"%s" , [signature getArgumentTypeAtIndex:i]);
}
NSLog(@"returnType:%s ,returnLen:%ld" , [signature methodReturnType] , [signature methodReturnLength]); NSLog(@"signature:%@" , signature); return signature; }
- (void)forwardInvocation:(NSInvocation *)anInvocation{
// 当然这里可以通过判断方法名来决定转发的对象
//NSString *seletorName = NSStringFromSelector(aSelector);
//if([seletorName isEqualToString : @"xxx"]) SEL selector = [anInvocation selector];
//ClassB *b = [[ClassB alloc] init];
if([_b respondsToSelector:selector]){
//这里如果没有响应,系统则会报错崩溃 [anInvocation invokeWithTarget:_b];
}
}
4.如果完整消息转发失败则crash报unrecognized selector的异常
3.iOS Runtime面试题(如何给 Category 添加属性?关联对象以什么形式进行存储?)
如何给 Category 添加属性?关联对象以什么形式进行存储?
查看的是 关联对象 的知识点。
详细的说一下 关联对象。
关联对象 以哈希表的格式,存储在一个全局的单例中。
找一下52个方法里的关联key等知识!!!
@interface NSObject (Extension) @property (nonatomic,copy ) NSString *name; @end @implementation NSObject (Extension) - (void)setName:(NSString *)name { objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)name { return objc_getAssociatedObject(self,@selector(name)); } @end
4.iOS Runtime面试题(能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?)
不能向编译后得到的类中增加实例变量;
能向运行时创建的类中添加实例变量;
1.因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.所以不能向存在的类中添加实例变量。
2.运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
5.iOS Runtime面试题(类对象的数据结构?)
类对象就是 objc_class。
struct objc_class : objc_object { // Class ISA; Class superclass; //父类指针 cache_t cache; // formerly cache pointer and vtable 方法缓存 class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 用于获取地址 class_rw_t *data() { return bits.data(); // &FAST_DATA_MASK 获取地址值 }
它的结构相对丰富一些。继承自objc_object结构体,所以包含isa指针
* isa:指向元类
* superClass: 指向父类
* Cache: 方法的缓存列表
* data: 顾名思义,就是数据。是一个被封装好的 class_rw_t。
补充一下实例对象!!!
6.iOS Runtime面试题(runtime如何通过selector找到对应的IMP地址?)
每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
补充消息转发时候的获取方法列表等操作!!!
7.iOS Runtime面试题(runtime如何实现weak变量的自动置nil?知道SideTable吗?)
runtime 对注册的类会进行布局,对于weak修饰的对象会放到一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0时会dealloc,加入weak指向的对象的内存地址为a,那么就会以a为键,那么就以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil
(1)初始化时runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
(2)添加引用时:objc_initWeak函数会调用objc_storeWeak函数objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
(3)释放时,调用clearDeallocating函数,clearDeallocating函数首先根据对象的地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设置为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
全部原理太多了在:[https://www.jianshu.com/p/5de63ac9dab7](https://www.jianshu.com/p/5de63ac9dab7)
总结:
其实Weak表是一个hash(哈希)表,Key是weak所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
8.iOS Runtime面试题(objc中向一个nil对象发送消息将会发生什么?)
如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误,也不会崩溃。
详解:
(1)如果一个方法的返回值是一个对象,那么发送给nil的消息将会返回0(nil);
(2)如果方法的返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double, long double或者long long的整型标量,发送给nil的消息将返回0;
(3)如果方法返回值为结构体,发送给nil的消息将返回0.结构体中各个字段的值都将是0;
(4)如果方法的返回值不是上述的几种情况,那么发送给nil的消息的返回值将是未定义的。
9.iOS Runtime面试题(objc在向一个对象发送消息时,发生了什么?)
objc在向一个对象发送消息时,runtime会根据对象的isa指针找到该对象实际所属的类
然后在该类中的方法列表及其父类方法列表中寻找发发运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到,就去执行他的IMP
10.iOS Runtime面试题(isKindOfClass 与 isMemberOfClass)
待补充!!![https://www.jianshu.com/p/87ce88d0bb99](https://www.jianshu.com/p/87ce88d0bb99)
11.iOS Runtime面试题(Category 在编译过后,是在什么时机与原有的类合并到一起的?)
这里应该去查一下object init的原理!!!
11.的原理也该查一下,下面写的太繁琐了
程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init。
然后会 map_images。
接下来调用 map_images_nolock。
再然后就是 read_images,这个方法会读取所有的类的相关信息。
最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。
在 reMethodizeClass:方法内部会调用 attachCategories:,这个方法会传入 Class 和 Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t结构体中。
12.iOS Runtime面试题(Category 有哪些用途?)
搜一下分类和扩展区别实现!!!
还有分类为什么不能添加属性(就是分类的结构体里面关于属性的问题)!!!
13.iOS Runtime面试题(Category 的实现原理?)
被添加在了 class_rw_t 的对应结构里。
Category实际上是Category_t的结构体,在运行时,新添加的方法,都被以倒叙插入到原有的方法列表的最前面,所以不同的Category,添加了同一个方法,执行的实际上是最后一个。
下面代码要更新下!!!
static struct _catrgory_t _OBJC_$_CATEGORY_NSObject_$_Tools __attribute__ ((used,section),("__DATA,__objc__const")) { // name // class // instance method list // class method list // protocol list // properties }
Category在刚刚编译完的时候,和原来的类是分开的,只有程序在运行起来后,通过Runtime,Category和原来的类才会合并到一起
mememove,memcpy,这两个方法是位移,复制,简单的理解就是原有的方法移动到最后,根据新开辟的控件,把前面的位置留给分类,然后分类中的方法按照倒叙依次插入,可以得出的结论就是,越晚参与编译的分类,里面的方法才是生效的那个
14.iOS Runtime面试题(_objc_msgForward函数是做什么的)
_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
详解:_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:
List itemresolveInstanceMethod:方法 (或resolveClassMethod:)。
List itemforwardingTargetForSelector:方法
List itemmethodSignatureForSelector:方法
List itemforwardInvocation:方法
List itemdoesNotRecognizeSelector: 方法
15.iOS Runtime面试题([self class] 与 [super class])
下面的代码输出什么?
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
详解:这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。
self是类的隐藏参数,指向当前调用方法的这个类的实例
super本质是一个编译器的标识符,和self是指向一个消息接收者。不同点在于:super会告诉编译器,当调用方法是,去调用父类的方法,而不是本类中的方法。
当使用self调用方法时,则从父类的方法列表中开始找。然后调用父类的这个方法。
在调用[super class]的时候,runtime会去调用 objc_msgSenderSuper方法,而不是objc_msgSend;
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是当前父类的super_class
objc_msgSenderSuper的工作原理应该是这样的
从objc_super结构体指向的superClass父类的方法列表开始查找Selector,找到后以objc->receiver去调用父类的这个selector。注意,最后调用者是objc_receiver,而不是super_class
那么objc_msgSendSuper最后就转变成:
// 注意这里是从父类开始msgSend,而不是从本类开始 objc_msgSend(objc_super->receiver, @selector(class)) /// Specifies an instance of a class. 这是类的一个实例 __unsafe_unretained id receiver; // 由于是实例调用,所以是减号方法 - (Class)class { return object_getClass(self); }
由于找到了父类NSObject里面的class方法的IMP,又因为传入的参数objc_super->receiver = self. self就是son,调用class,所以父类的方法class执行IMP之后,输出的还是son最后输出的两个都一样,都是输出son
扩展题:[https://www.jianshu.com/p/a8e32de4858f](https://www.jianshu.com/p/a8e32de4858f)
16.iOS Runloop面试题(为什么 NSTimer 有时候不好使?)
找一下!!!
17.iOS Runloop面试题(PerformSelector 的实现原理?)
在写多线程时候因为直接在主函数里写的多线程,但是多线程没有开启runloop(正确性待验证)
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
18.iOS Runloop面试题(PerformSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?)
不起作用,子线程默认没有 Runloop,也就没有 Timer。
解决的办法是可以使用 GCD 来实现:Dispatch_after
19.iOS Runloop面试题(RunLoop的Mode)
关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。
当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的
总共是有五种CFRunLoopMode:
* kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
* UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
* UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
* GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
* kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案
20.iOS Runloop面试题(RunLoop的实现机制)
[https://www.jianshu.com/p/cc1bb6cba76d](https://www.jianshu.com/p/cc1bb6cba76d)
21.iOS Runloop面试题(RunLoop和线程)
* 线程和runloop是一一对应的,其映射关系是保存在一个全局的Dictionary里
* 自己创建的线程默认是没有开启RunLoop的
(1)怎么创建一个常驻线程?
1.为当前线程开启了一个RunLoop(第一次调用[NSRunLoop currentRunLoop])方法时实际是会先创建一个 RunLoop
2.向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中的一个item都没有,RunLoop会退出)
3.启动该RunLoop
@autoreleasepool { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }
(2)输出下边代码的执行顺序
NSLog(@"1"); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); [self performSelector:@selector(test) withObject:nil afterDelay:10]; NSLog(@"3"); }); NSLog(@"4"); - (void)test { NSLog(@"5"); }
答案是1423,test方法并不会执行。
原因是如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的RunLoop中。也就是如果当前线程没有开启RunLoop,该方法会失效。
那么我们改成:
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); [[NSRunLoop currentRunLoop] run]; [self performSelector:@selector(test) withObject:nil afterDelay:10]; NSLog(@"3"); });
然而test方法依然不执行。
原因是如果RunLoop的mode中一个item都没有,RunLoop会退出。即在调用RunLoop的run方法后,由于其mode中没有添加任何item去维持RunLoop的时间循环,RunLoop随即还是会退出。
所以我们自己启动RunLoop,一定要在添加item后
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); [self performSelector:@selector(test) withObject:nil afterDelay:10]; [[NSRunLoop currentRunLoop] run];//一定是添加在item后,item维持RunLoop的时间循环 NSLog(@"3"); });
(3)怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?
当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。
我们就可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
22.iOS Runloop面试题(RunLoop的数据结构)
[https://www.jianshu.com/p/64c478fe2cf1](https://www.jianshu.com/p/64c478fe2cf1)
23.iOS Runloop面试题(RunLoop概念)
RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。
1、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
2、有消息需要处理时,立刻被唤醒,由内核态切换到用户态
为什么main函数不会退出?
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环)
//无限循环代码模式(伪代码) int main(int argc, char * argv[]) { BOOL running = YES; do { // 执行各种任务,处理各种事件 // ...... } while (running); return 0; }
UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。
24.iOS Runloop面试题(讲一下 Observer ?)
[https://www.jianshu.com/p/bac5c2462e69](https://www.jianshu.com/p/bac5c2462e69)
25.iOS Runloop面试题(解释一下 NSTimer。)
NSTimer其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged的。一个 NSTimer注册到 RunLoop后,RunLoop会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer有个属性叫做 Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer相似),造成界面卡顿的感觉。在快速滑动 TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook开源的 AsyncDisplayLink就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。
26.iOS Runloop面试题(解释一下 `事件响应` 的过程?)
[https://www.jianshu.com/p/9c3d136302be](https://www.jianshu.com/p/9c3d136302be)
27.iOS Runloop面试题(解释一下 手势识别 的过程?)
[https://www.jianshu.com/p/32d97298d669](https://www.jianshu.com/p/32d97298d669)
顺便补充下UIGestureRecognizer和touch事件!!!
28.iOS Runloop面试题(什么是异步绘制?)
异步绘制,就是可以在子线程把需要绘制的图形,提前在子线程处理好。将准备好的图像数据直接返给主线程使用,这样可以降低主线程的压力。
异步绘制的过程
要通过系统的 [view.delegate displayLayer:]这个入口来实现异步绘制。
* 代理负责生成对应的 Bitmap
* 设置该 Bitmap 为 layer.contents 属性的值。
!!![https://www.jianshu.com/p/6634dbdf2964](https://www.jianshu.com/p/6634dbdf2964)
!!![https://www.jianshu.com/p/ee67b17dbf19](https://www.jianshu.com/p/ee67b17dbf19)
29.iOS Runloop面试题(利用 runloop 解释一下页面的渲染的过程?)
[https://www.jianshu.com/p/8900516f1644](https://www.jianshu.com/p/8900516f1644)
30.iOS OC底层面试题(KVC(Key-value coding))
-(id)valueForKey:(NSString *)key; -(void)setValue:(id)value forKey:(NSString *)key;
KVC就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的
当调用setValue:属性值forKey:@”name“的代码时,,底层的执行机制如下:
* 程序优先调用set<Key>:属性值方法,代码通过setter方法完成设置。注意,这里的<key>是指成员变量名,首字母大小写要符合KVC的命名规则,下同
* 如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为_<key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以<key>命名的变量,KVC都可以对该成员变量赋值。
* 如果该类即没有set<key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。
* 和上面一样,如果该类即没有set<Key>:方法,也没有_<key>和_is<Key>成员变量,KVC机制再会继续搜索<key>和is<Key>的成员变量。再给它们赋值。
* 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
即如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。
如果开发者想让这个类禁用KVC,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会直接用setValue:forUndefinedKey:方法。
当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:
* 首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
* 如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
* 如果上面的方法没有找到,那么会同时查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>,enumeratorOf<Key>,memberOf<Key>组合的形式调用。
* 如果还没有找到,再检查类方法+
(BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:方法,默认是抛出异常。
31.iOS OC底层面试题(KVO (Key-value observing))
待添加!!!
* 那么通过直接赋值成员变量会触发KVO吗?
不会,因为不会调用setter方法,需要加上
willChangeValueForKey和didChangeValueForKey方法来手动触发才行
32.iOS OC底层面试题(分类、扩展、代理(Delegate))
一、分类
* 1.分类的作用?
声明私有方法,分解体积大的类文件,把framework的私有方法公开
* 2.分类的特点
运行时决议,可以为系统类添加分类 。
说得详细些,在运行时时期,将 Category 中的实例方法列表、协议列表、属性列表添加到主类中后(所以Category中的方法在方法列表中的位置是在主类的同名方法之前的),然后会递归调用所有类的 load 方法,这一切都是在main函数之前执行的。
* 3.分类可以添加哪些内容?
实例方法,类方法,协议,属性(添加getter和setter方法,并没有实例变量,添加实例变量需要用关联对象)
* 4.如果工程里有两个分类A和B,两个分类中有一个同名的方法,哪个方法最终生效?
取决于分类的编译顺序,最后编译的那个分类的同名方法最终生效,而之前的都会被覆盖掉(这里并不是真正的覆盖,因为其余方法仍然存在,只是访问不到,因为在动态添加类的方法的时候是倒序遍历方法列表的,而最后编译的分类的方法会放在方法列表前面,访问的时候就会先被访问到,同理如果声明了一个和原类方法同名的方法,也会覆盖掉原类的方法)。
* 5.如果声明了两个同名的分类会怎样?
会报错,所以第三方的分类,一般都带有命名前缀
* 6.分类能添加成员变量吗?!!!
不能。只能通过关联对象(objc_setAssociatedObject)来模拟实现成员变量,但其实质是关联内容,所有对象的关联内容都放在同一个全局容器哈希表中:AssociationsHashMap,由AssociationsManager统一管理。
二、扩展
* 1.一般用扩展做什么?
声明私有属性,声明方法(没什么意义),声明私有成员变量
* 2.扩展的特点
编译时决议,只能以声明的形式存在,多数情况下寄生在宿主类的.m中,不能为系统类添加扩展。
三、代理(Delegate)
代理是一种设计模式,以@protocol形式体现,一般是一对一传递。
一般以weak关键词以规避循环引用。
待补充!!!
33.iOS OC底层面试题(属性关键字copy)
可变对象的copy和mutableCopy都是深拷贝
不可变对象的copy是浅拷贝,mutableCopy是深拷贝!!!
copy方法返回的都是不可变对象
* @property (nonatomic, copy) NSMutableArray * array;这样写有什么影响?
因为copy方法返回的都是不可变对象,所以array对象实际上是不可变的,如果对其进行可变操作如添加移除对象,则会造成程序crash!!!
assign:修饰基本数据类型,修饰对象类型时,不改变其引用计数,会产生悬垂指针,修饰的对象在被释放后,assign指针仍然指向原对象内存地址,如果使用assign指针继续访问原对象的话,就可能会导致内存泄漏或程序异常(实际上会报错???)
34.iOS Animation面试题(请说一下对 CALayer 的认识。)
layer层是涂层绘制、渲染、以及动画的完成者,它无法直接的处理触摸事件(也可以捕捉事件)
layer包含的方面非常多,常见的属性有 Frame、Bounds、Position、AnchorPoint、Contents等等。
35.iOS Animation面试题(`CALayer` 的 `Contents` 有几下几个主要的属性:)
待补充!!!
36.iOS Block面试题(Block的几种形式)
比较基础:[https://www.jianshu.com/p/ab5cd4153bf8](https://www.jianshu.com/p/ab5cd4153bf8)
37.iOS Block面试题(Block变量截获)
待补充!!![https://www.jianshu.com/p/dd5fea4dcea8](https://www.jianshu.com/p/dd5fea4dcea8)
38.iOS Block面试题(什么是Block?)
待补充!!![https://www.jianshu.com/p/cc91ff650d6c](https://www.jianshu.com/p/cc91ff650d6c)
39.iOS UI相关面试题
一、UIView与CALayer
<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents
二、事件传递与视图响应链 :
待补充!!!
三、图像显示原理
1.CPU:输出位图
2.GPU :图层渲染,纹理合成
3.把结果放到帧缓冲区(frame buffer)中
4.再由视频控制器根据vsync信号在指定时间之前去提取帧缓冲区的屏幕显示内容
5.显示到屏幕上
CPU工作
1.Layout: UI布局,文本计算
2.Display: 绘制
3.Prepare: 图片解码
4.Commit:提交位图
GPU渲染管线(OpenGL)
顶点着色,图元装配,光栅化,片段着色,片段处理
四、UI卡顿掉帧原因
iOS设备的硬件时钟会发出Vsync(垂直同步信号),然后App的CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染。随后,GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。也就是说,一帧的显示是由CPU和GPU共同决定的。
一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿。
五、滑动优化方案
CPU:
把以下操作放在子线程中
1.对象创建、调整、销毁
2.预排版(布局计算、文本计算、缓存高度等等)
3.预渲染(文本等异步绘制,图片解码等)
GPU:
纹理渲染,视图混合
一般遇到性能问题时,考虑以下问题:
是否受到CPU或者GPU的限制?
是否有不必要的CPU渲染?
是否有太多的离屏渲染操作?
是否有太多的图层混合操作?
是否有奇怪的图片格式或者尺寸?
是否涉及到昂贵的view或者效果?
view的层次结构是否合理?
六、UI绘制原理
异步绘制:
[self.layer.delegate displayLayer: ]
代理负责生成对应的bitmap
设置该bitmap作为该layer.contents属性的值
七、离屏渲染
On-Screen Rendering:当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
Off-Screen Rendering:离屏渲染,分为CPU离屏渲染和GPU离屏渲染两种形式。GPU离屏渲染指的是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作
应当尽量避免的则是GPU离屏渲染
GPU离屏渲染何时会触发呢?
圆角(当和maskToBounds一起使用时)、图层蒙版、阴影,设置
layer.shouldRasterize = YES
为什么要避免GPU离屏渲染?
GPU需要做额外的渲染操作。通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响。另外由于离屏渲染会增加GPU的工作量,可能会导致CPU+GPU的处理时间超出16.7ms,导致掉帧卡顿。所以可以的话应尽量减少offscreen-render的图层
40.iOS 多线程面试题(进程、线程)
一、 进程:
* 1.进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的最小单元.
* 2.进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.
* 3.每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源
补充进程通信!!!???
二、 线程
* 1.程序执行流的最小单元,线程是进程中的一个实体.
* 2.一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程
三、 进程和线程的关系
* 1.线程是进程的执行单元,进程的所有任务都在线程中执行
* 2.线程是 CPU 分配资源和调度的最小单位
* 3.一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
* 4.同一个进程内的线程共享进程资源
41.iOS 多线程面试题(死锁)
1、一个比较常见的死锁例子:主队列同步
- (void)viewDidLoad { [super viewDidLoad]; dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"deallock"); }); // Do any additional setup after loading the view, typically from a nib. }
在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。
同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。!!!
而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。!!!
想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。!!!
2、同样,下边的代码也会造成死锁:
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ dispatch_sync(serialQueue, ^{ NSLog(@"deadlock"); }); });
外面的函数无论是同步还是异步都会造成死锁。!!!
这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁!!!
解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ dispatch_sync(serialQueue2, ^{ NSLog(@"deadlock"); }); });
这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。
队列的知识也需要再看看!!!
42\. iOS 多线程面试题(dispatch_barrier_async)
1、问:怎么用GCD实现多读单写?
多读单写的意思就是:可以多个读者同时读取数据,而在读的时候,不能取写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的。
这里的写处理就是通过栅栏的形式去写。
就可以用dispatch_barrier_sync(栅栏函数)去实现
- (id)readDataForKey:(NSString *)key { __block id result; dispatch_sync(_concurrentQueue, ^{ result = [self valueForKey:key]; }); return result; } - (void)writeData:(id)data forKey:(NSString *)key { dispatch_barrier_async(_concurrentQueue, ^{ [self setValue:data forKey:key]; }); }
dispatch_barrier_async全部知识:[https://www.jianshu.com/p/540c2b22ba38](https://www.jianshu.com/p/540c2b22ba38)
_concurrentQueue目的是栅栏函数和同步公用一个并发队列!!!
43.iOS 多线程面试题(dispatch_group_async)
场景:在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面。
dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); for (NSInteger i = 0; i < 10; i++) { dispatch_group_async(group, concurrentQueue, ^{ sleep(1); NSLog(@"%zd:网络请求",i); }); } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"刷新页面"); });
注意最后要写:dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新页面");
});!!!!!!!!!
44.iOS 多线程面试题(Dispatch Semaphore 信号量)
[https://www.jianshu.com/p/8549a35b7bf2](https://www.jianshu.com/p/8549a35b7bf2)
45.iOS 多线程面试题(延时函数(dispatch_after))
dispatch_after能让我们添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue
//第一个参数是time,第二个参数是dispatch_queue,第三个参数是要执行的block dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"dispatch_after"); });
由于其内部使用的是dispatch_time_t管理时间,而不是NSTimer。
所以如果在子线程中调用,相比performSelector:afterDelay,不用关心runloop是否开启
46.iOS 多线程面试题(使用dispatch_once实现单例)
多写几遍背下来!!!
+ (instancetype)shareInstance { static dispatch_once_t onceToken; static id instance = nil; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; }
47.iOS 多线程面试题(NSThread+runloop实现常驻线程)
[https://www.jianshu.com/p/f0bcc10abad0](https://www.jianshu.com/p/f0bcc10abad0)
48.iOS 多线程面试题(自旋锁与互斥锁)
[https://www.jianshu.com/p/80043c824d2d](https://www.jianshu.com/p/80043c824d2d)
49.iOS 内存管理面试题(在 MRC 下如何重写属性的 Setter 、 Getter、strong、retain、copy)
strong、retain、copy待补充!!!
-(void)setBrand:(NSString *)brand{ //如果实例变量指向的地址和参数指向的地址不同 if (_brand != brand) { //将实例变量的引用计数减一 [_brand release]; //将参数变量的引用计数加一,并赋值给实例变量 _brand = [brand retain]; } }
-(NSString *)brand{ //将实例变量的引用计数加1后,添加自动减1 //作用,保证调用getter方法取值时可以取到值的同时在完全不需要使用后释放 return [[_brand retain] autorelease]; }
//MRC下 手动释放内存 可重写dealloc但不要调用dealloc 会崩溃 -(void)dealloc{ [_string release]; //必须最后调用super dealloc [super dealloc]; }
50.iOS 内存管理面试题(循环引用)
[https://www.jianshu.com/p/33b7b326dcc4](https://www.jianshu.com/p/33b7b326dcc4)
2、NSTimer循环引用属于相互循环使用
在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,那么该对象和定时器就相互循环引用了。
如何解决呢?
这里我们可以使用手动断开循环引用:
如果是不重复定时器,在回调方法里将定时器invalidate并置为nil即可。
如果是重复定时器,在合适的位置将其invalidate并置为nil即可
51.iOS 内存管理面试题(说一下什么是 `悬垂指针`?什么是 `野指针`?)
指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 迷途指针
没有进行初始化的指针,其实都是 野指针
52.iOS 内存管理面试题(是否了解 深拷贝 和 浅拷贝 的概念,集合类深拷贝如何实现)
简而言之:
1、对不可变的非集合对象,copy是指针拷贝,mutablecopy是内容拷贝
2、对于可变的非集合对象,copy,mutablecopy都是内容拷贝
3、对不可变的数组、字典、集合等集合类对象,copy是指针拷贝,mutablecopy是内容拷贝
4、对于可变的数组、字典、集合等集合类对象,copy,mutablecopy都是内容拷贝
但是,对于集合对象的内容复制仅仅是对对象本身,但是对象的里面的元素还是指针复制。要想复制整个集合对象,就要用集合深复制的方法,有两种:!!!!!!!!
(1)使用initWithArray:copyItems:方法,将第二个参数设置为YES即可
NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];
(2)将集合对象进行归档(archive)然后解归档(unarchive):
NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];
53.iOS 内存相关面试题(能不能简述一下 Dealloc 的实现机制.md)
[https://www.jianshu.com/p/c00e2fdb5afb](https://www.jianshu.com/p/c00e2fdb5afb)
54.iOS 内存相关面试题(内存中的5大区分别是什么?)
背下来!!!
栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放。
文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放。
程序代码区:存放函数体的二进制代码。
55.iOS 内存管理面试题(内存管理默认的关键字是什么?)
atomic,readWrite,strong如果改为基本数据类型,那就是atomic,readWrite, assign。
56.iOS 内存管理面试题(内存管理方案)
内存管理方案
* taggedPointer :存储小对象如NSNumber。深入理解Tagged Pointer
* NONPOINTER_ISA(非指针型的isa):在64位架构下,isa指针是占64比特位的,实际上只有30多位就已经够用了,为了提高利用率,剩余的比特位存储了内存管理的相关数据内容。
* 散列表:复杂的数据结构,包括了引用计数表和弱引用表
通过SideTables()结构来实现的,SideTables()结构下,有很多SideTable的数据结构。
而sideTable当中包含了自旋锁,引用计数表,弱引用表。
SideTables()实际上是一个哈希表,通过对象的地址来计算该对象的引用计数在哪个sideTable中。
自旋锁:
* 自旋锁是“忙等”的锁。
* 适用于轻量访问。
引用计数表和弱引用表实际是一个哈希表,来提高查找效率。
搜索一下hash表!!!
57.iOS 内存管理面试题(内存布局)
栈(stack):方法调用,局部变量等,是连续的,高地址往低地址扩展
堆(heap):通过alloc等分配的对象,是离散的,低地址往高地址扩展,需要我们手动控制
未初始化数据(bss):未初始化的全局变量等
已初始化数据(data):已初始化的全局变量等
代码段(text):程序代码
3、static、const和sizeof关键字
static关键字
答:Static的用途主要有两个,一是用于修饰存储类型使之成为静态存储类型,二是用于修饰链接属性使之成为内部链接属性。
* 1、静态存储类型:
在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值不会被销毁,函数下次运行时能仍用到这个值。
在函数外定义的静态变量——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。
* 2、内部链接属性
静态函数只能在声明它的源文件中使用。
const关键字
* 1、声明常变量,使得指定的变量不能被修改。
const int a = 5;/*a的值一直为5,不能被改变*/ const int b; b = 10;/*b的值被赋值为10后,不能被改变*/ const int *ptr; /*ptr为指向整型常量的指针,ptr的值可以修改,但不能修改其所指向的值*/ int *const ptr;/*ptr为指向整型的常量指针,ptr的值不能修改,但可以修改其所指向的值*/ const int *const ptr;/*ptr为指向整型常量的常量指针,ptr及其指向的值都不能修改*/const int a = 5;/*a的值一直为5,不能被改变*/ const int b; b = 10;/*b的值被赋值为10后,不能被改变*/ const int *ptr; /*ptr为指向整型常量的指针,ptr的值可以修改,但不能修改其所指向的值*/ int *const ptr;/*ptr为指向整型的常量指针,ptr的值不能修改,但可以修改其所指向的值*/ const int *const ptr;/*ptr为指向整型常量的常量指针,ptr及其指向的值都不能修改*/
* 2、修饰函数形参,使得形参在函数内不能被修改,表示输入参数。
int fun(const int a);或int fun(const char *str);
* 3、修饰函数返回值,使得函数的返回值不能被修改。
const char *getstr(void);使用:const *str= getstr(); const int getint(void); 使用:const int a =getint();
sizeof关键字
sizeof是在编译阶段处理,且不能被编译为机器码。sizeof的结果等于对象或类型所占的内存字节数。sizeof的返回值类型为size_t。
* 变量:int a; sizeof(a)为4;
* 指针:int *p; sizeof(p)为4;
* 数组:int b[10]; sizeof(b)为数组的大小,4*10;int c[0]; sizeof(c)等于0
* 结构体:struct (int a; char ch;)s1; sizeof(s1)为8 与结构体字节对齐有关。
对结构体求sizeof时,有两个原则:
(1)展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。 (2)结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。
* 注意:不能对结构体中的位域成员使用sizeof
* sizeof(void)等于1
* sizeof(void *)等于4
58.iOS 内存管理面试题(讲一下 `iOS` 内存管理的理解)
实际上是三种方案的结合
* 1.TaggedPointer(针对类似于 NSNumber的小对象类型)
* 2.NONPOINTER_ISA(64位系统下)
* 第一位的 0或 1代表是纯地址型 isa指针,还是 NONPOINTER_ISA指针。
* 第二位,代表是否有关联对象
* 第三位代表是否有 C++代码。
* 接下来33位代表指向的内存地址
* 接下来有 弱引用的标记
* 接下来有是否 delloc的标记....等等
* 3.散列表(引用计数表、weak表)
* SideTables表在 非嵌入式的64位系统中,有 64张 SideTable表
* 每一张 SideTable主要是由三部分组成。自旋锁、引用计数表、弱引用表。
* 全局的 引用计数之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。
* 引用计数表中引入了 分离锁的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,提升执行效率
搜一下散列表!!!
59.iOS 内存管理面试题(讲一下 `@dynamic` 关键字?)
@dynamic 意味着编译器不会帮助我们自动合成 setter 和 getter 方法。我们需要手动实现、这里就涉及到 Runtime 的动态添加方法的知识点。
60.iOS 内存管理面试题(简要说一下 `@autoreleasePool` 的数据结构?)
简单说是双向链表,每张链表头尾相接,有 parent、child指针
每创建一个池子,会在首部创建一个 哨兵对象,作为标记
最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。
61.iOS 内存管理面试题(访问 `__weak` 修饰的变量,是否已经被注册在了 `@autoreleasePool` 中?为什么?)
答案是肯定的,__weak修饰的变量属于弱引用,如果没有被注册到 @autoreleasePool中,创建之后也就会随之销毁,为了延长它的生命周期,必须注册到 @autoreleasePool中,以延缓释放。
62.iOS 内存管理面试题(`retain`、`release` 的实现机制?)
1.Retain的实现机制。
SideTable& table = SideTables()[This];//第一层 hash算法,找到 指针变量所对应的 sideTable size_t& refcntStorage = table.refcnts[This];//然后再通过一层 hash算法,找到存储 引用计数的 size_t refcntStorage += SIZE_TABLE_RC_ONE;//IZE_TABLE_RC_ONE是一个宏定义,实际上是一个值为 4 的偏移量。
2.Release的实现机制。
SideTable& table = SideTables()[This];//第一层 hash算法,找到 指针变量所对应的 sideTable size_t& refcntStorage = table.refcnts[This];//然后再通过一层 hash算法,找到存储 引用计数的 size_t refcntStorage -= SIZE_TABLE_RC_ONE;//IZE_TABLE_RC_ONE是一个宏定义,实际上是一个值为 4 的偏移量。
二者的实现机制类似,概括讲就是通过第一层 hash算法,找到 指针变量所对应的 sideTable。然后再通过一层 hash算法,找到存储 引用计数的 size_t,然后对其进行增减操作。retainCount不是固定的 1,SIZE_TABLE_RC_ONE是一个宏定义,实际上是一个值为 4 的偏移量。
63.iOS 内存管理面试题(MRC(手动引用计数)和ARC(自动引用计数))
MRC(手动引用计数)和ARC(自动引用计数)
1、MRC:alloc,retain,release,retainCount,autorelease,dealloc
2、ARC:
* ARC是LLVM和Runtime协作的结果
* ARC禁止手动调用retain,release,retainCount,autorelease关键字
* ARC新增weak,strong关键字
3、引用计数管理:
* alloc: 经过一系列函数调用,最终调用了calloc函数,这里并没有设置引用计数为1
* retain: 经过两次哈希查找,找到其对应引用计数值,然后将引用计数加1(实际是加偏移量)
* release:和retain相反,经过两次哈希查找,找到其对应引用计数值,然后将引用计数减1
* dealloc:
4、弱引用管理:
* 添加weak变量:通过哈希算法位置查找添加。如果查找对应位置中已经有了当前对象所对应的弱引用数组,就把新的弱引用变量添加到数组当中;如果没有,就创建一个弱引用数组,并将该弱引用变量添加到该数组中。
* 当一个被weak修饰的对象被释放后,weak对象怎么处理的?
清除weak变量,同时设置指向为nil。当对象被dealloc释放后,在dealloc的内部实现中,会调用弱引用清除的相关函数,会根据当前对象指针查找弱引用表,找到当前对象所对应的弱引用数组,将数组中的所有弱引用指针都置为nil。
5、自动释放池:
在当次runloop将要结束的时候调用objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool
AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成,是和线程一一对应的。
内部属性有parent,child对应前后两个结点,thread对应线程 ,next指针指向栈中下一个可填充的位置。
* AutoreleasePool实现原理?
编译器会将 @autoreleasepool {} 改写为:
void * ctx = objc_autoreleasePoolPush; {} objc_autoreleasePoolPop(ctx);
objc_autoreleasePoolPush:
把当前next位置置为nil,即哨兵对象,然后next指针指向下一个可入栈位置,
AutoreleasePool的多层嵌套,即每次objc_autoreleasePoolPush,实际上是不断地向栈中插入哨兵对象。
objc_autoreleasePoolPop:
根据传入的哨兵对象找到对应位置。
给上次push操作之后添加的对象依次发送release消息。
回退next指针到正确的位置。
64.iOS 内存管理面试题(`BAD_ACCESS` 在什么情况下出现? )
访问了已经被销毁的内存空间,就会报出这个错误。
根本原因是有 悬垂指针 没有被释放。
65.iOS 内存管理面试题(`autoReleasePool` 什么时候释放?)
[https://www.jianshu.com/p/73150489071e](https://www.jianshu.com/p/73150489071e)
66.iOS 内存管理面试题( `ARC` 在运行时做了哪些工作?)
* 主要是指 weak关键字。weak修饰的变量能够在引用计数为0时被自动设置成 nil,显然是有运行时逻辑在工作的。
* 为了保证向后兼容性,ARC在运行时检测到类函数中的 autorelease后紧跟其后 retain,此时不直接调用对象的 autorelease方法,而是改为调用 objc_autoreleaseReturnValue。
objc_autoreleaseReturnValue会检视当前方法返回之后即将要执行的那段代码,若那段代码要在返回对象上执行 retain操作,则设置全局数据结构中的一个标志位,而不执行 autorelease操作,与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行 retain,而是改为执行 objc_retainAoutoreleasedReturnValue函数。此函数要检测刚才提到的标志位,若已经置位,则不执行 retain操作,设置并检测标志位,要比调用 autorelease和retain更快。
67.iOS 内存管理面试题(`ARC` 在编译时做了哪些工作)
根据代码执行的上下文语境,在适当的位置插入 retain,release
68.iOS 内存管理面试题(`ARC` 的 `retainCount` 怎么存储的?)
ARC的 retainCount怎么存储的?
存在64张哈希表中,根据哈希算法去查找所在的位置,无需遍历,十分快捷
散列表(引用计数表、weak表)
- SideTables表在 非嵌入式的64位系统中,有 64张 SideTable表
- 每一张 SideTable主要是由三部分组成。自旋锁、引用计数表、弱引用表。
- 全局的 引用计数之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。
- 引用计数表中引入了 分离锁的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,提升执行效率
引用计数表(哈希表)
通过指针的地址,查找到引用计数的地址,大大提升查找效率
通过 DisguisedPtr(objc_object)函数存储,同时也通过这个函数查找,这样就避免了循环遍历。
69.iOS 内存管理面试题(`__weak` 属性修饰的变量,如何实现在变量没有强引用后自动置为 `nil` ?)
用的弱引用 - weak表。也是一张 哈希表。
被 weak修饰的指针变量所指向的地址是 key,所有指向这块内存地址的指针会被添加在一个数组里,这个数组是 Value。当内存地址销毁,数组里的所有对象被置为 nil。
70.iOS 内存管理面试题( `__weak` 和 `_Unsafe_Unretain` 的区别?)
weak 修饰的指针变量,在指向的内存地址销毁后,会在 Runtime的机制下,自动置为 nil。
_Unsafe_Unretain不会置为 nil,容易出现 悬垂指针,发生崩溃。但是 _Unsafe_Unretain比 __weak效率高。
71.iOS 设计模式面试题(编程中的六大设计原则?)
背下来!!!
1.单一职责原则
通俗地讲就是一个类只做一件事
* CALayer:动画和视图的显示。
* UIView:只负责事件传递、事件响应。
2.开闭原则
对修改关闭,对扩展开放。
要考虑到后续的扩展性,而不是在原有的基础上来回修改
3.接口隔离原则
使用多个专门的协议、而不是一个庞大臃肿的协议
* UITableviewDelegate
* UITableViewDataSource
4.依赖倒置原则
抽象不应该依赖于具体实现、具体实现可以依赖于抽象。
调用接口感觉不到内部是如何操作的
5.里氏替换原则
父类可以被子类无缝替换,且原有的功能不受任何影响
例如 KVO
6.迪米特法则
一个对象应当对其他对象尽可能少的了解,实现高聚合、低耦合
72.RSA非对称加密和MD5加密
补充MD5加密及其应用!!!
RSA非对称加密
对称加密[算法]在加密和解密时使用的是同一个秘钥;而[非对称加密算法]需要两个[密钥]来进行加密和解密,这两个秘钥是[公开密钥](public key,简称公钥)和私有密钥(private key,简称私钥)。
RSA加密
与对称加密[算法]不同,[非对称加密算法]需要两个[密钥]:[公开密钥](publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的[密钥],所以这种算法叫作[非对称加密算法]。
RSA加密原理
RSA是常用的加密模式,其加密原理可用以下的例子进行简要的论述。
随机取两个质数
P = 61; q = 53; N = P * Q = 3233; // E是1-n之间的一个随机的质数 E = 17; // D是通过一系列数学运算得出的一个数字, // 运算方法后续会附上阮一峰老师的两篇文章链接 // (N,D)(N,E)要满足可以互相解值运算 // 假如(N,D)是公钥,(N,E)是私钥 // 满足私钥加密,公钥解密或者反过来公钥加密,私钥解密。 // 也要满足只知道(N,D)就想知道(N,E),那就要把N这个大的整数进行因数分解。 // 因数分解只能使用暴力穷举,N越大,相应的也就越安全 // 当 N 大到1024位或者2048位时,以目前的技术破解几乎不可能,所以很安全
73.简述 `SSL` 加密的过程用了哪些加密方法,为何这么作?
简述 SSL加密的过程用了哪些加密方法,为何这么作?
SSL加密的过程之前有些过,此处不再赘述。
SSL加密,在过程中实际使用了 对称加密和 非对称加密的结合。
主要的考虑是先使用 非对称加密进行连接,这样做是为了避免中间人攻击秘钥被劫持,但是 非对称加密的效率比较低。所以一旦建立了安全的连接之后,就可以使用轻量的 对称加密。
74.iOS 性能优化面试题
在性能优化中一个最具参考价值的属性是FPS:Frames Per Second,其实就是屏幕刷新率,苹果的iphone推荐的刷新率是60Hz,也就是说GPU每秒钟刷新屏幕60次,这每刷新一次就是一帧frame,FPS也就是每秒钟刷新多少帧画面。静止不变的页面FPS值是0,这个值是没有参考意义的,只有当页面在执行动画或者滑动的时候,FPS值才具有参考价值,FPS值的大小体现了页面的流畅程度高低,当低于45的时候卡顿会比较明显。
图层混合:
每一个layer是一个纹理,所有的纹理都以某种方式堆叠在彼此的顶部。对于屏幕上的每一个像素,GPU需要算出怎么混合这些纹理来得到像素RGB的值。
当Sa = 0.5时,RGB值为(0.5, 0, 0),可以看出,当两个不是完全不透明的CALayer覆盖在一起时,GPU大量做这种复合操作,随着这中操作的越多,GPU越忙碌,性能肯定会受到影响。
公式:
R = S + D * ( 1 – Sa )
结果的颜色是源色彩(顶端纹理)+目标颜色(低一层的纹理)*(1-源颜色的透明度)。
当Sa = 1时,R = S,GPU将不会做任何合成,而是简单从这个层拷贝,不需要考虑它下方的任何东西(因为都被它遮挡住了),这节省了GPU相当大的工作量。
一、入门级
1、用ARC管理内存
2、在正确的地方使用 reuseIdentifier
3、尽量把views设置为透明
4、避免过于庞大的XIB
5、不要阻塞主线程
6、在ImageViews中调整图片大小。如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background
thread,缩放一次,然后在UIImageView中使用缩放后的图片。
7、选择正确的Collection。
* Arrays: 有序的一组值。使用index来lookup很快,使用value lookup很慢, 插入/删除很慢。
* Dictionaries: 存储键值对。 用键来查找比较快。
* Sets: 无序的一组值。用值来查找很快,插入/删除很快。
8、打开gzip压缩。app可能大量依赖于服务器资源,问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。
二、中级
1、重用和延迟加载(lazy load) Views
* 更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
* 这里我们用到的技巧就是模仿UITableView和UICollectionView的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。
2、Cache, Cache, 还是Cache!
* 一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。
* 我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。
* NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。
3、权衡渲染方法.性能能还是要bundle保持合适的大小。
4、处理内存警告.移除对缓存,图片object和其他一些可以重创建的objects的strong references.
5、重用大开销对象
6、一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
7、避免反复处理数据.在服务器端和客户端使用相同的数据结构很重要。
8、选择正确的数据格式.解析JSON会比XML更快一些,JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization 就更加方便使用了。但是XML也有XML的好处,比如使用SAX 来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。
9、正确设定背景图片
* 全屏背景图,在view中添加一个UIImageView作为一个子View
* 只是某个小的view的背景图,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:
10、减少使用Web特性。想要更高的性能你就要调整下你的HTML了。第一件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。注意你使用的图片,保证图片的符合你使用的大小。
11、Shadow Path 。CoreAnimation不得不先在后台得出你的图形并加好阴影然后才渲染,这开销是很大的。使用shadowPath的话就避免了这个问题。使用shadow path的话iOS就不必每次都计算如何渲染,它使用一个预先计算好的路径。但问题是自己计算path的话可能在某些View中比较困难,且每当view的frame变化的时候你都需要去update shadow path.
12、优化Table View
* 正确使用reuseIdentifier来重用cells
* 尽量使所有的view opaque,包括cell自身
* 避免渐变,图片缩放,后台选人
* 缓存行高
* 如果cell内现实的内容来自web,使用异步加载,缓存请求结果
* 使用shadowPath来画阴影
* 减少subviews的数量
* 尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用-一次然后缓存结果
* 使用正确的数据结构来存储数据
* 使用rowHeight, sectionFooterHeight 和 sectionHeaderHeight来设定固定的高,不要请求delegate
13、选择正确的数据存储选项
* NSUserDefaults的问题是什么?虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了
* XML这种结构化档案呢?总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。
* NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。
* 在这种应用场景下,使用SQLite 或者 Core Data比较好。使用这些技术你用特定的查询语句就能只加载你需要的对象。
* 在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。
* Core Data代表一个对象的graph model,但SQLite就是一个DBMS。
* Apple在一般情况下建议使用Core Data,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。
* 如果你使用SQLite,你可以用FMDB这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。
三、高级
1、加速启动时间。快速打开app是很重要的,特别是用户第一次打开它时,对app来讲,第一印象太太太重要了。你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据,解析数据。避免过于庞大的XIB,因为他们是在主线程上加载的。所以尽量使用没有这个问题的Storyboards吧!一定要把设备从Xcode断开来测试启动速度
2、使用Autorelease Pool。NSAutoreleasePool`负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为。
3、选择是否缓存图片。常见的从bundle中加载图片的方式有两种,一个是用imageNamed,二是用imageWithContentsOfFile,第一种比较常见一点。
4、避免日期格式转换。如果你要用NSDateFormatter来处理很多日期格式,应该小心以待。就像先前提到的,任何时候重用NSDateFormatters都是一个好的实践。如果你可以控制你所处理的日期格式,尽量选择Unix时间戳。你可以方便地从时间戳转换到NSDate:
- (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp { return[NSDate dateWithTimeIntervalSince1970:timestamp]; }
这样会比用C来解析日期字符串还快!需要注意的是,许多web API会以微秒的形式返回时间戳,因为这种格式在javascript中更方便使用。记住用dateFromUnixTimestamp之前除以1000就好了。
平时你是如何对代码进行性能优化的?
* 利用性能分析工具检测,包括静态 Analyze 工具,以及运行时 Profile 工具,通过Xcode工具栏中Product->Profile可以启动,
* 比如测试程序启动运行时间,当点击Time Profiler应用程序开始运行后.就能获取到整个应用程序运行消耗时间分布和百分比.为了保证数据分析在统一使用场景真实需要注意一定要使用真机,因为此时模拟器是运行在Mac上,而Mac上的CPU往往比iOS设备要快。
* 为了防止一个应用占用过多的系统资源,开发iOS的苹果工程师门设计了一个“看门狗”的机制。在不同的场景下,“看门狗”会监测应用的性能。如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者们在crashlog里面,会看到诸如0x8badf00d这样的错误代码。
优化Table View
* 正确使用reuseIdentifier来重用cells
* 尽量使所有的view opaque,包括cell自身
* 如果cell内现实的内容来自web,使用异步加载,缓存请求结果
减少subviews的数量
* 尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
* 使用rowHeight, sectionFooterHeight和sectionHeaderHeight来设定固定的高,不要请求delegate
UIImage加载图片性能问题
* imagedNamed初始化
* imageWithContentsOfFile初始化
* imageNamed默认加载图片成功后会内存中缓存图片,这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象.如果缓存中没有找到相应的图片对象,则从指定地方加载图片然后缓存对象,并返回这个图片对象.
* imageWithContentsOfFile则仅只加载图片,不缓存.
* 加载一张大图并且使用一次,用imageWithContentsOfFile是最好,这样CPU不需要做缓存节约时间.
* 使用场景需要编程时,应该根据实际应用场景加以区分,UIimage虽小,但使用元素较多问题会有所凸显.
* 不要在viewWillAppear 中做费时的操作:viewWillAppear: 在view显示之前被调用,出于效率考虑,方法中不要处理复杂费时操作;在该方法设置 view 的显示属性之类的简单事情,比如背景色,字体等。否则,会明显感觉到 view 有卡顿或者延迟。
* 在正确的地方使用reuseIdentifier:table view用 tableView:cellForRowAtIndexPath:为rows分配cells的时候,它的数据应该重用自UITableViewCell。
* 尽量把views设置为透明:如果你有透明的Views你应该设置它们的opaque属性为YES。系统用一个最优的方式渲染这些views。这个简单的属性在IB或者代码里都可以设定。
* 避免过于庞大的XIB:尽量简单的为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去, 当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。
* 不要阻塞主线程:永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成,大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 选择一个子线程来执行耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
// 返回主线程更新UI
});
});
* 在Image Views中调整图片大小
如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的.
讲讲你用Instrument优化动画性能的经历吧(别问我什么是Instrument)
Apple的instrument为开发者提供了各种template去优化app性能和定位问题。很多公司都在赶feature,并没有充足的时间来做优化,导致不少开发者对instrument不怎么熟悉。但这里面其实涵盖了非常完整的计算机基础理论知识体系,memory,disk,network,thread,cpu,gpu等等,顺藤摸瓜去学习,是一笔巨大的知识财富。动画性能只是其中一个template,重点还是理解上面问题当中CPU GPU如何配合工作的知识。
facebook启动时间优化
1.瘦身请求依赖
2.UDP启动请求先行缓存
3.队列串行化处理启动响应
75.iOS 性能优化面试题(光栅化)
光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程,光栅化的本质是坐标变换、几何离散化
我们使用 UITableView 和 UICollectionView 时经常会遇到各个 Cell 的样式是一样的,这时候我们可以使用这个属性提高性能:
cell.layer.shouldRasterize=YES;
cell.layer.rasterizationScale=[[UIScreenmainScreen]scale];
76.iOS 性能优化面试题(如何高性能的画一个圆角?)
搜索一下直接切图画圆角,贝塞尔曲线圆角!!!
视图和圆角的大小对帧率并没有什么卵影响,数量才是伤害的核心输出
label.layer.cornerRadius = 5 label.layer.masksToBounds = true
如果能够只用 cornerRadius解决问题,就不用优化。
如果必须设置 masksToBounds,可以参考圆角视图的数量,如果数量较少(一页只有几个)也可以考虑不用优化。
UIImageView的圆角通过直接截取图片实现,其它视图的圆角可以通过 Core Graphics画出圆角矩形实现。
[图片上传失败...(image-56643b-1581791102540)]
77.iOS 性能优化面试题(如何提升 `tableview` 的流畅度?)
本质上是降低 CPU、GPU 的工作,从这两个大的方面去提升性能。
* CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制
* GPU:纹理的渲染
卡顿优化在 CPU 层面
* 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用 CALayer 取代 UIView
* 不要频繁地调用 UIView 的相关属性,比如 frame、bounds、transform 等属性,尽量减少不必要的修改
* 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
* Autolayout 会比直接设置 frame 消耗更多的 CPU 资源
* 图片的 size 最好刚好跟 UIImageView 的 size 保持一致
* 控制一下线程的最大并发数量
* 尽量把耗时的操作放到子线程
* 文本处理(尺寸计算、绘制)
* 图片处理(解码、绘制)
卡顿优化在 GPU层面
* 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
* GPU能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用 CPU 资源进行处理,所以纹理尽量不要超过这个尺寸
* 尽量减少视图数量和层次
* 减少透明的视图(alpha<1),不透明的就设置 opaque 为 YES
* 尽量避免出现离屏渲染
[iOS 保持界面流畅的技巧](https://links.jianshu.com/go?to=https%3A%2F%2Fblog.ibireme.com%2F2015%2F11%2F12%2Fsmooth_user_interfaces_for_ios%2F)
1.预排版,提前计算
在接收到服务端返回的数据后,尽量将 CoreText排版的结果、单个控件的高度、cell整体的高度提前计算好,将其存储在模型的属性中。需要使用时,直接从模型中往外取,避免了计算的过程。
尽量少用 UILabel,可以使用 CALayer。避免使用 AutoLayout的自动布局技术,采取纯代码的方式
2.预渲染,提前绘制
例如圆形的图标可以提前在,在接收到网络返回数据时,在后台线程进行处理,直接存储在模型数据里,回到主线程后直接调用就可以了
避免使用 CALayer 的 Border、corner、shadow、mask 等技术,这些都会触发离屏渲染。
3.异步绘制
4.全局并发线程
5.高效的图片异步加载
iOS 性能优化面试题(如何优化 `APP` 的电量?)
程序的耗电主要在以下四个方面:
* CPU 处理
* 定位
* 网络
* 图像
优化的途径主要体现在以下几个方面:
* 尽可能降低 CPU、GPU 的功耗。
* 尽量少用 定时器。
* 优化 I/O 操作。
* 不要频繁写入小数据,而是积攒到一定数量再写入
* 读写大量的数据可以使用 Dispatch_io ,GCD 内部已经做了优化。
* 数据量比较大时,建议使用数据库
* 网络方面的优化
* 减少压缩网络数据 (XML -> JSON -> ProtoBuf),如果可能建议使用 ProtoBuf。
* 如果请求的返回数据相同,可以使用 NSCache 进行缓存
* 使用断点续传,避免因网络失败后要重新下载。
* 网络不可用的时候,不尝试进行网络请求
* 长时间的网络请求,要提供可以取消的操作
* 采取批量传输。下载视频流的时候,尽量一大块一大块的进行下载,广告可以一次下载多个
* 定位层面的优化
* 如果只是需要快速确定用户位置,最好用 CLLocationManager 的 requestLocation 方法。定位完成后,会自动让定位硬件断电
* 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
* 尽量降低定位精度,比如尽量不要使用精度最高的 kCLLocationAccuracyBest
* 需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically 为 YES,如果用户不太可能移动的时候系统会自动暂停位置更新
* 尽量不要使用 startMonitoringSignificantLocationChanges,优先考虑 startMonitoringForRegion:
* 硬件检测优化
* 用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件
iOS 性能优化面试题(如何有效降低 APP 包的大小?)
降低包大小需要从两方面着手
可执行文件
* 编译器优化
* 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 检测未使用的代码:菜单栏 -> Code -> Inspect Code
* 编写LLVM插件检测出重复代码、未被调用的代码
资源包括 图片、音频、视频 等
* 优化的方式可以对资源进行无损的压缩
* 去除没有用到的资源: [https://github.com/tinymind/LSUnusedResources](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Ftinymind%2FLSUnusedResources)
iOS 性能优化面试题(什么是 离屏渲染?什么情况下会触发?该如何应对?)
离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作
离屏渲染出发的场景有以下:
* 圆角 (maskToBounds并用才会触发)
* 图层蒙版
* 阴影
* 光栅化
为什么要避免离屏渲染?
CPUGPU在绘制渲染视图时做了大量的工作。离屏渲染发生在 GPU层面上,会创建新的渲染缓冲区,会触发 OpenGL的多通道渲染管线,图形上下文的切换会造成额外的开销,增加 GPU工作量。如果 CPUGPU累计耗时 16.67毫秒还没有完成,就会造成卡顿掉帧。
圆角属性、蒙层遮罩都会触发离屏渲染。指定了以上属性,标记了它在新的图形上下文中,在未愈合之前,不可以用于显示的时候就出发了离屏渲染。
* 在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 就不会产生离屏渲染
iOS 性能优化面试题(如何检测离屏渲染?)
1、模拟器debug-选中color Offscreen - Renderd离屏渲染的图层高亮成黄 可能存在性能问题
2、真机Instrument-选中Core Animation-勾选Color Offscreen-Rendered Yellow
离屏渲染的触发方式
设置了以下属性时,都会触发离屏绘制:
1、layer.shouldRasterize(光栅化)
光栅化概念:将图转化为一个个栅格组成的图象。
光栅化特点:每个元素对应帧缓冲区中的一像素。
2、masks(遮罩)
3、shadows(阴影)
4、edge antialiasing(抗锯齿)
5、group opacity(不透明)
6、复杂形状设置圆角等
7、渐变
8、drawRect
例如我们日程经常打交道的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化。则会造成大量的离屏渲染,降低图形性能。
如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU渲染。如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。
现在摆在我们面前得有三个选择:当前屏幕渲染、离屏渲染、CPU渲染,该用哪个呢?这需要根据具体的使用场景来决定。
尽量使用当前屏幕渲染
鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。
离屏渲染 VS CPU渲染
由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染;但如果仅仅是实现一个简单的效果,直接使用CPU渲染的效率又可能比离屏渲染好,毕竟离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作
UIButton 的 masksToBounds = YES又设置setImage、setBackgroundImage、[button setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:@"btn_selected"]]];
下发生离屏渲染,但是[button setBackgroundColor:[UIColor redColor]];是不会出现离屏渲染的
关于 UIImageView,现在测试发现(现版本: iOS10),在性能的范围之内,给UIImageView设置圆角是不会触发离屏渲染的,但是同时给UIImageView设置背景色则肯定会触发.触发离屏渲染跟 png.jpg格式并无关联
日常我们使用layer的两个属性,实现圆角
imageView.layer.cornerRaidus = CGFloat(10);
imageView.layer.masksToBounds = YES;
这样处理的渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会给我们带来额外的性能损耗。如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,性能的代价会宏观地表现在用户体验上——掉帧
iOS 性能优化面试题(怎么检测图层混合?)
1、模拟器debug- 选中 color blended layers红色区域表示图层发生了混合
2、Instrument-选中Core Animation-勾选Color Blended Layers
避免图层混合:
1、确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明
2、如无特殊需要,不要设置低于1的alpha值
3、确保UIImage没有alpha通道
UILabel图层混合解决方法:
iOS8以后设置背景色为非透明色并且设置label.layer.masksToBounds=YES让label只会渲染她的实际size区域,就能解决UILabel的图层混合问题
iOS8 之前只要设置背景色为非透明的就行
为什么设置了背景色但是在iOS8上仍然出现了图层混合呢?
UILabel在iOS8前后的变化,在iOS8以前,UILabel使用的是CALayer作为底图层,而在iOS8开始,UILabel的底图层变成了_UILabelLayer,绘制文本也有所改变。在背景色的四周多了一圈透明的边,而这一圈透明的边明显超出了图层的矩形区域,设置图层的masksToBounds为YES时,图层将会沿着Bounds进行裁剪 图层混合问题解决了
2020-02-16
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
- 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
- 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
- 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...