iOS 基础题

分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?

解答:分类是在已有的类上进行功能的拓展,并且可以在不知道已存在类内部实现的情况下进行功能拓展,与原来的类文件分开;扩展则是在原有类内部实现功能的拓展,与原有类必须共一个文件,比如给类添加一个私有成员变量等。
分类的结构体中包含实例方法列表、实例属性列表、协议列表、类方法列表、主类指针等。结构体如下:

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);
};
讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?

解答:atomic是声明属性的关键字,与noatomic对应,前者是指读写属性的时候原子操作,即线程安全,后者则相反,非原子操作,读写线程不安全。
atomic实现的机制是对属性的setter和getter方法的时候进行加锁(自旋锁)操作,源码如下:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {//偏移为0说明改的是isaif (offset == 0) {object_setClass(self, newValue);return;}id oldValue;id *slot = (id*) ((char*)self + offset);//获取原值//根据特性拷贝if (copy) {newValue = [newValue copyWithZone:nil];} else if (mutableCopy) {newValue = [newValue mutableCopyWithZone:nil];} else {if (*slot == newValue) return;newValue = objc_retain(newValue);}//判断原子性if (!atomic) {//非原子直接赋值oldValue = *slot;*slot = newValue;} else {//原子操作使用自旋锁spinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();oldValue = *slot;*slot = newValue;        slotlock.unlock();}objc_release(oldValue);
}id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {// 取isaif (offset == 0) {return object_getClass(self);}// 非原子操作直接返回id *slot = (id*) ((char*)self + offset);if (!atomic) return *slot;// 原子操作自旋锁spinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();id value = objc_retain(*slot);slotlock.unlock();// 出于性能考虑,在锁之外autoreleasereturn objc_autoreleaseReturnValue(value);
}

正如上面所说的,只是保证了读写操作的完整性,但是不能保证整个对象的线程安全,例如:
如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,有3种可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。所以atomic可并不能保证对象的线程安全。
因此要保证多线程数据的一致性,需要额外的同步操作。同时需要强调的是:加锁是非常消耗资源的事情,我们一般情况下不要轻易使用automic关键字。

被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?

解答:被weak修饰的对象,被释放的时候,系统会清空对象对应的弱引用表中存储的弱引用指针,并将指针的值置为nil。
具体的实现及数据结构参考iOS weak底层原理及源码解析

关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将其指针置空么?

解答:关联对象我们一般应用到分类属性实现上。我们知道,类的属性是成员变量+getter方法+setter方法,由于类的内存结构在编译期就决定了,在运行期是不能添加成员变量的,而分类是一种运行时为类添加功能的机制,上面的也提到过分类的结构体,是没有包含成员变量列表的,因此分类声明的属性是没有自动生成对应的成员变量+getter方法+setter方法的,所以需要手动关联实现。如下:

#import "DKObject+Category.h"
#import <objc/runtime.h>@implementation DKObject (Category)- (NSString *)categoryProperty {return objc_getAssociatedObject(self, _cmd);//_cmd这里就是指@selector(categoryProperty)
}- (void)setCategoryProperty:(NSString *)categoryProperty {objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//@selector(categoryProperty)也可以用其他的const void * 代替,建议使用@selector(categoryProperty)这样的形式,即能够确保唯一,又不要另外声明
}@end

当对象释放obj dealloc时候会调用object_dispose,检查有无关联对象,有的话_object_remove_assocations删除,不需要手动管理置为nil。

KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?

解答:
底层实现:1、为被观察的类声明了一个子类,并将父类的isa指针指向这个子类(同时系统也重写了class相关方法,隐藏了isa的指向);2、通过之类重写setter方法,在改变之前调用willChangeValueForKey方法存储旧值,改变之后调用didChangeValueForKey触发- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context。
取消系统默认的KVO并手动触发:
1、重写下面的方法,默认返回yes,自动触发,改为no就是不自动触发;

  • (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
    2、在其他的地方手动调用下面两个方法即可
- (void)willChangeValueForKey:(NSString*)key;- (void)didChangeValueForKey:(NSString*)key;

更多可以参考iOS KVO原理用法及自定义KVO

Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

解答:Autoreleasepool没有具体的数据结构,实质是AutoreleasePoolPage进行管理,而AutoreleasepoolPage则是一个双向链表。源码如下:

class AutoreleasePoolPage
{...magic_t const magic;id *next;//指向下一个Autorelease对象pthread_t const thread;//对应的线程AutoreleasePoolPage * const parent;//上一个pageAutoreleasePoolPage *child;//下一个pageuint32_t const depth;//链表深度uint32_t hiwat;
...
}
讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?

解答:对象:

struct objc_object {Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//isa指向类对象
};

类对象结构体继承自对象结构体(类对象也是一个对象):

struct objc_class : objc_object {// Class ISA;//指向元类对象,元类对象的isa指向根元类NSObject,NSObject的isa指针指向自己Class superclass;//指向父类,元类对象只想元类对象的父类,直到根元类NSObjectcache_t cache;             // formerly cache pointer and vtableclass_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags,这里面保存着方法,属性,协议,成员变量等信息...
}

其关系iOS的官方图如下:
至于实例对象的方法存在类对象的结构体里面的原因,我想应该是为了节约资源,所有实例共享一份内存,达到资源重复利用,类对象在内存中只有一份,实例对象可以根据需求new很多份。

class_ro_t 和 class_rw_t 的区别?

解答:从字面上理解,class_ro_t是只读,class_rw_t可写可读。这两个变量共同点都是存储类的属性、方法、协议等信息的,不同的有两点:1、class_ro_t还存储了类的成员变量,而class_rw_t则没有,从这方面也验证了类的成员变量一旦确定了,就不能写了,就是分类不能增加成员变量的原因;2、class_ro_t是在编译期就确定了固定的值,在整个运行时都只读不可写的状态,在运行时调用resizeclass方法将class_ro_t复制到class_rw_t对应的变量上去。

struct class_ro_t {...const uint8_t * ivarLayout;const char * name;method_list_t * baseMethodList;protocol_list_t * baseProtocols;const ivar_list_t * ivars;const uint8_t * weakIvarLayout;property_list_t *baseProperties;....
}
struct class_rw_t {// Be warned that Symbolication knows the layout of this structure.uint32_t flags;uint32_t version;const class_ro_t *ro;method_array_t methods;property_array_t properties;protocol_array_t protocols;Class firstSubclass;Class nextSiblingClass;....
}
/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls)
{runtimeLock.assertLocked();const class_ro_t *ro;class_rw_t *rw;Class supercls;Class metacls;bool isMeta;if (!cls) return nil;if (cls->isRealized()) return cls;assert(cls == remapClass(cls));// fixme verify class is not in an un-dlopened part of the shared cache?ro = (const class_ro_t *)cls->data();if (ro->flags & RO_FUTURE) {// This was a future class. rw data is already allocated.rw = cls->data();ro = cls->data()->ro;cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);} else {// Normal class. Allocate writeable class data.rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);rw->ro = ro;rw->flags = RW_REALIZED|RW_REALIZING;cls->setData(rw);}...// Attach categoriesmethodizeClass(cls);
}
iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?

解答:内省(Introspection)是面向对象语言和环境的一个强大特性,Objective-C和Cocoa在这个方面尤其的丰富。内省是对象揭示自己作为一个运行时对象的详细信息的一种能力。这些详细信息包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息。NSObject协议和类定义了很多内省方法,用于查询运行时信息,以便根据对象的特征进行识别。
例如:
-(BOOL) isKindOfClass: //判断是否是这个类或者这个类的子类的实例 -(BOOL) isMemberOfClass:// 判断是否是这个类的实例 -(BOOL) respondsToSelector: 判读实例是否有这样方法 +(BOOL) instancesRespondToSelector: 判断类是否有这个方法
class方法和objc_getClass方法有什么区别:
object_getClass:获得的是isa的指向,比如:实例对象的isa是类对象,类对象的isa是元类对象。
self.class:当self是实例对象的时候,返回的是类对象,否则则返回自身。类方法class,返回的是self,所以当查找meta class时,需要对类对象调用object_getClass方法。
代码如下:

+ (Class)class {return self;
}- (Class)class {return object_getClass(self);
}Class object_getClass(id obj)
{if (obj) return obj->getIsa();else return Nil;
}inline Class
objc_object::getIsa()
{if (isTaggedPointer()) {uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;return objc_tag_classes[slot];}return ISA();
}inline Class
objc_object::ISA()
{assert(!isTaggedPointer()); return (Class)(isa.bits & ISA_MASK);
}
在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)?

解答:通过objc_allocateClassPair的源码发现,创建一个新类,会添加到全局的NXHashTable中,而这个全局的NXHashTable的本质就是一个哈希表,里面的存储的元素数组习惯性的叫pairs,源码如下:

Class objc_allocateClassPair(Class supercls, const char *name, size_t extraBytes)
{Class cls, meta;if (objc_getClass(name)) return nil;// fixme reserve class name against simultaneous allocationif (supercls  &&  (supercls->info & CLS_CONSTRUCTING)) {// Can't make subclass of an in-construction classreturn nil;}// Allocate new classes. if (supercls) {cls = _calloc_class(supercls->ISA()->alignedInstanceSize() + extraBytes);meta = _calloc_class(supercls->ISA()->ISA()->alignedInstanceSize() + extraBytes);} else {cls = _calloc_class(sizeof(objc_class) + extraBytes);meta = _calloc_class(sizeof(objc_class) + extraBytes);}objc_initializeClassPair(supercls, name, cls, meta);return cls;
}
Class objc_initializeClassPair(Class superclass, const char *name, Class cls, Class meta)
{// Fail if the class name is in use.if (look_up_class(name, NO, NO)) return nil;mutex_locker_t lock(runtimeLock);// Fail if the class name is in use.// Fail if the superclass isn't kosher.if (getClassExceptSomeSwift(name)  ||!verifySuperclass(superclass, true/*rootOK*/)){return nil;}objc_initializeClassPair_internal(superclass, name, cls, meta);return cls;
}
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{runtimeLock.assertLocked();...addClassTableEntry(cls);//添加到全局的hashTable中
}/***********************************************************************
* addClassTableEntry
* Add a class to the table of all classes. If addMeta is true,
* automatically adds the metaclass of the class as well.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void addClassTableEntry(Class cls, bool addMeta = true) {runtimeLock.assertLocked();// This class is allowed to be a known class via the shared cache or via// data segments, but it is not allowed to be in the dynamic table already.assert(!NXHashMember(allocatedClasses, cls));if (!isKnownClass(cls))NXHashInsert(allocatedClasses, cls);if (addMeta)addClassTableEntry(cls->ISA(), false);
}
void *NXHashInsert (NXHashTable *table, const void *data) {HashBucket   *bucket = BUCKETOF(table, data);unsigned   j = bucket->count;const void    **pairs;const void  **newt;__unused void *z = ZONE_FROM_PTR(table);if (! j) {bucket->count++; bucket->elements.one = data; table->count++; return NULL;};if (j == 1) {if (ISEQUAL(table, data, bucket->elements.one)) {const void   *old = bucket->elements.one;bucket->elements.one = data;return (void *) old;};newt = ALLOCPAIRS(z, 2);newt[1] = bucket->elements.one;*newt = data;bucket->count++; bucket->elements.many = newt; table->count++; if (table->count > table->nbBuckets) _NXHashRehash (table);return NULL;};pairs = bucket->elements.many;while (j--) {/* we don't cache isEqual because lists are short */if (ISEQUAL(table, data, *pairs)) {const void    *old = *pairs;*pairs = data;return (void *) old;};pairs ++;};/* we enlarge this bucket; and put new data in front */newt = ALLOCPAIRS(z, bucket->count+1);if (bucket->count) bcopy ((const char*)bucket->elements.many, (char*)(newt+1), bucket->count * PTRSIZE);*newt = data;FREEPAIRS (bucket->elements.many);bucket->count++; bucket->elements.many = newt; table->count++; if (table->count > table->nbBuckets) _NXHashRehash (table);return NULL;}
一个int变量被__block修饰与否的区别?

解答:如果一个block外部的auto变量,需要被block内部引用并且赋值,则需要在变量前加上__block修饰,否则会编译器会直接报错。那么__block做了什么骚操作呢?我们看一下下面的代码:
底层原理:

#include <stdio.h>int main(){__block int a = 10;void(^block)(void) = ^{printf("this is a block test %d",a);};block();return 0;
}

终端输入clang -rewrite-objc block.c命令生成block.cpp文件,打开文件和说明:

struct __block_impl {void *isa;//isa指针,因此从这方面讲block本质上也是一个对象int Flags;int Reserved;void *FuncPtr;//函数指针
};//block结构体
...
struct __Block_byref_a_0 {void *__isa;
__Block_byref_a_0 *__forwarding;int __flags;int __size;int a;
};//声明了一个__Block_byref_a_0的结构体struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;//其他附加信息,例如block占内存大小,变量捕获,释放等相关信息__Block_byref_a_0 *a; // by ref 引用指针__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {impl.isa = &_NSConcreteStackBlock;//创建blockimpl.Flags = flags;impl.FuncPtr = fp;//函数指针Desc = desc;}
};//根据具体block再次封装的结构体
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_a_0 *a = __cself->a; // bound by ref 用block捕获到的a的地址赋值printf("this is a block test %d",(a->__forwarding->a));}//block具体执行函数static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}//block copy变量的辅助函数,有编译器生成static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}//block dispose变量的辅助函数,有编译器生成static struct __main_block_desc_0 {size_t reserved;size_t Block_size;void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);//copy函数指针void (*dispose)(struct __main_block_impl_0*);//dispose函数指针///简单来说,当Block引用了 1) C++ 栈上对象 2)OC对象 3) 其他block对象 4) __block修饰的变量,并被拷贝至堆上时则需要copy/dispose辅助函数。辅助函数由编译器生成。
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(){__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};//__block将局部变量转化成__Block_byref_a_0类型的结构体void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));//这里的__Block_byref_a_0类型a地址传递((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);//block函数执行调用return 0;
}

通过上面比较我们发现:
当变量使用__block修饰时,则变为变量内存地址的传递,
我们在block内部就具有了修改变量的权限!

为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?

解答:我们知道,当block与他要捕获的外部变量有相互引用时,会造成循环引用,从而造成内存泄漏。为了解决循环引用,在ARC环境下,我们的通用做法是通过__weak关键字来修饰block需要捕获的外部变量。这里面实现的原理是:__weak(具体的底层逻辑参考iOS weak底层原理及源码解析)修饰变量后,不会导致对象的引用计数器+1,对对象的释放没有影响,从打破循环引用。但是当我们block里面执行的任务是一个延时的操作,任务还没执行时,外部的变量已经释放掉了,当执行任务时,weak指针已经为nil,虽然不会导致crash,但是会影响业务逻辑的执行。解决这个问题的方式就是在block的内部再一次对捕获的weak指针进行强引用,即用__strong修饰下,这样既能打破循环引用,又能延迟对象的释放,保证业务逻辑的完整性。

RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)

解答:RunLoop从字面上理解,就是一个循环。对于iOS而言,一个app之所以不停的运行,就是因为app在启动时,就创建了一个runloop,这个runloop做的事情就是通过注册的observers不停的监听事件(source0和source1)、timer和port,如果没有事件、timer和port到达,就进入休眠,反之,有事件、timer或者port发生就被唤醒处理这些事件或timer,runloop一直不停的这样休眠被唤醒,他的周期跟屏幕刷新的频率一致60fps,即一个循环1/60s。下面我们看看runloop源码:

struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock;           /* locked for accessing mode list */__CFPort _wakeUpPort;           // used for CFRunLoopWakeUp Boolean _unused;volatile _per_run_data *_perRunData;              // reset for runs of the run looppthread_t _pthread;   //跟线程一一对应uint32_t _winthread;CFMutableSetRef _commonModes; //common模式CFMutableSetRef _commonModeItems;CFRunLoopModeRef _currentMode;//当前模式CFMutableSetRef _modes; ///模式列表struct _block_item *_blocks_head;struct _block_item *_blocks_tail;CFAbsoluteTime _runTime;CFAbsoluteTime _sleepTime;CFTypeRef _counterpart;
};

从源码可以看出每个runloop包含有对应的thread线程及各种模式CFRunLoopModeRef,一一分析。

CFRunLoopModeRef 模式:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;struct __CFRunLoopMode {CFRuntimeBase _base;pthread_mutex_t _lock;  /* must have the run loop locked before locking this */CFStringRef _name;Boolean _stopped;char _padding[3];CFMutableSetRef _sources0;//事件CFMutableSetRef _sources1;//事件CFMutableArrayRef _observers;//监听列表CFMutableArrayRef _timers;//timerCFMutableDictionaryRef _portToV1SourceMap;__CFPortSet _portSet; //portCFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERSdispatch_source_t _timerSource;dispatch_queue_t _queue;Boolean _timerFired; // set to true by the source when a timer has firedBoolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOOmach_port_t _timerPort;Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWSDWORD _msgQMask;void (*_msgPump)(void);
#endifuint64_t _timerSoftDeadline; /* TSR */uint64_t _timerHardDeadline; /* TSR */
};

从上面的源码可以看出,一个runloop有多个mode,主要包括5种:

  1. NSDefaultRunLoopMode // App默认Mode通常主线程是在这个mode下运行
  2. UITrackingRunloopMode //界面跟踪Mode用于scrollView追踪触摸界面滑动时不受其他Mode影响
  3. UIinitializationRunloopMode //在app一启动进入的第一个Mode,启动完成后就不再使用
  4. GSEventRecieveRunloopMode //苹果使用绘图相关,系统内核调用,开发者使用不到
  5. NSRunLoopCommonModes   //占位模式
    而app运行过程中就是在这些模式之间不停的切换。

每个mode都有对应的observers,timer,source0,source1,port等变量。给一个runloop运行的官方图:
每个mode的运行机制如下:

RunLoop与线程的关系:

上面的源码可以看出,线程跟runloop是一一对应的,我们app启动的主线程对应的runloop是mainRunLoop,默认是开启的,而其他线程的runloop则是没有开启,因此后台线程执行一次任务后就会被线程池回收,看下面的例子:

- (void)viewDidLoad {[super viewDidLoad];NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadrun) object:nil];[thread start];self.thread = thread;[self performSelector:@selector(testThread) onThread:self.thread withObject:nil waitUntilDone:NO];NSLog(@"%s thread:%@",__func__, thread);
}
- (void)threadrun{NSThread* thread = [NSThread currentThread];NSLog(@"%s current thread:%@",__func__, thread);
//    [[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] run];}
- (void)testThread{NSThread* thread = [NSThread currentThread];NSLog(@"%s current thread:%@",__func__, thread);
}

运行的结果如下:
我们看到testThread这个方法没执行。
我们把testrun方法里面的启动runloop的代码[[NSRunLoop currentRunLoop] run]打开,执行看下结果如下:
我们看到的结果是执行了testThread。
对比两个执行结果,我们可以看出,后台线程的runloop是默认是没有开启的,如想要后台线程常驻,则需要手动开启对应的runloop。

RunLoop与内存管理(AutoReleasePool)的关系:

我们知道,iOS现在的内存管理是ARC,ARC主要是通过AutoReleasePool管理对象的引用计数,当对象的被new开始,即引用计数不为零,就会被默认的加入到AutoReleasePool中,当对象的引用计数为0时,就会从AutoReleasePool中移除释放对象。这个里面的加入到AutoReleasePool中和从AutoReleasePool中移除的时机就跟RunLoop息息相关。我们看到在程序刚启动的时候,runloop启动时,会添加各种不同的observers,其中就有一个observer叫_wrapRunLoopWithAutoreleasePoolHandler
注册了_wrapRunLoopWithAutoreleasePoolHandler这个observer,这个就是AutoreleasePool与RunLoop的关系点,第一次创建runloop的时候,会调用objc_autoreleasePoolPush,此时objc_autoreleasePoolPush的优先级最高,将所有引用计数不为0的对象全部入栈,当runloop进入休眠的时候,会调用objc_autoreleasePoolPop,此时objc_autoreleasePoolPop的优先级最低,将所有引用计数为0的对象全部退栈,完成后继续调用objc_autoreleasePoolPush重复上述过程。
Runloop其他详情参考iOS 透过CFRunloop源码分析runloop底层原理及应用场景

哪些场景可以触发离屏渲染?(知道多少说多少)

解答:我们知道,iOS设备的渲染流程是,CPU解压数据后并计算好视图的布局,然后相关接口提交给GPU渲染,而iOS设备的屏幕刷新率是60fps,就是在1/60s内,完成上述过程,如下图:

何为离屏渲染:

如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法一次性把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域进行预合成,之后再写入frame buffer,那么这个过程被称之为离屏渲染。
离屏渲染涉及到两个缓冲区上下文环境的切换,这个过程是非常消耗性能的,因此需要尽量的避免离屏渲染。

哪些会导致离屏渲染:
  1. 圆角:
 xxx.layer.cornerRadius = 20.0;xxx.layer.masksToBounds = YES;

虽然iOS 9.0以后,苹果系统对此做了改进,UIImageView的加载的图片格式是PNG的,使用这种方式不会导致离屏渲染,但是其他控件依旧存在这个问题。
解决方案:
在所需切圆角的控件上加一层,利用贝塞尔切割出不同角所需要的圆角半径。具体参考笔记-圆角四种方法的对比以及性能检测
注意:还有很多其他的方式解决圆角性能的问题,但是有两个方式值得提一下,一个是在drawRect方法里面用CoreGraphics的API画圆弧,这个方式的缺点是drawRect会非常占用内存。另外一个是用CAShaperLayer结合贝塞尔画圆角,这个本质还是利用了mask,mask也会导致离屏渲染。所以这两种方式都不推荐。

  1. shadow/mask:
    解决方案:建议不要使用shadow/mask。

  2. 光栅化:
    光栅化需要视情况而定,当无法避免离屏渲染,且当内容为静态的时,没有动画之类的操作,即前后可以重复利用,将光栅化设置为true,反而对性能的提升有很好的效果。当然如果没有发生离屏渲染,就不要打开了。

  3. 抗锯齿:
    综合考虑,是否开关。

  4. Group opacity组合透明度:
    iOS7以后默认是设置为开的,建议关掉。

未完待续。。。

iOS高级面试题及部分答案相关推荐

  1. 出一套 iOS 高级面试题

    一千个读者眼中有一千个哈姆雷特,一千名 iOS 程序员心目中就有一千套 iOS 高级面试题.本文就是笔者认为可以用来面试高级 iOS 程序员的面试题. 这套题的题目跟公司和业务都没有关系,而且也并不代 ...

  2. JAVA高级面试题汇总及答案

    JAVA高级面试题汇总及答案 1.hashaMap原理源码 2.synchronize关键字1.6之后的优化,(偏向轻量级锁,重量级锁) 3.双亲委派是什么 4.类加载过程中可以动态改字节码吗? 5. ...

  3. android中 cdf文件的作用是什么意思,行情艰难,Android初中高级面试题,附详细答案...

    原标题:行情艰难,Android初中高级面试题,附详细答案 作者:夜猫少年 链接:https://juejin.im/post/5c8211fee51d453a136e36b0 Activity篇 1 ...

  4. java高级面试题收集及答案

    2021 java高级面试题收集及答案 以下问题是个人面试时收集,答案纯属个人见解,如有错误请不吝赐教,感激不尽. 这里写目录标题 2021 java高级面试题收集及答案 一级目录 二级目录 三级目录 ...

  5. Java 高级 面试题 及 参考答案

    一.面试题基础总结 1. JVM结构原理.GC工作机制详解 答:具体参照:JVM结构.GC工作机制详解     ,说到GC,记住两点:1.GC是负责回收所有无任何引用对象的内存空间. 注意:垃圾回收回 ...

  6. java面试题高级_Java高级面试题整理(附答案)

    java java8 java开发 Java高级面试题整理(附答案) 这是我收集的10道高级Java面试问题列表.这些问题主要来自 Java 核心部分 ,不涉及 Java EE 相关问题.你可能知道这 ...

  7. iOS开发面试题整理

    前言 本文借鉴整理了iOS高级开发常见的面试题,并且分博客一一分析,希望能和大家一起进步学习. 欢迎大家关注我的 Github?以及相关博客 Github.io 简书 大家的鼓励是我前进的动力? iO ...

  8. Dubbo面试题及答案整理,Dubbo面试题大全带答案(2021最新版)

    本套Dubbo面试题分了6套,都有Dubbo面试题汇总带答案 Dubbo面试题及答案[最新版]Dubbo高级面试题大全(2021版),发现网上很多Dubbo面试题及答案整理都没有答案,所以花了很长时间 ...

  9. 2020,300道高级iOS开发面试题(最新整理)

    这个栏目将持续更新–请iOS的小伙伴关注! 一:知名大厂iOS开发面试题篇 1.腾讯-最新iOS面试题总结 2.百度-最新iOS面试题总结 3.头条-最新iOS面试题总结 4.阿里-最新iOS面试题总 ...

最新文章

  1. 最大限度地减少块输出中间结果的计算和存储
  2. mysql 5.5 编译参数_Mysql 5.5 编译参数
  3. [云炬创业基础笔记]第六章商业模式测试16
  4. OpenStack-Pike(一)
  5. 如何在WP-Config中设置WordPress错误日志
  6. 前端学习(1698):前端系列javascript之原型链和instance
  7. curl查看swift状态命令_HTTP 请求与响应包括哪些,如何用Chrome查看 HTTP 请求与响应内容和curl 命令的使用...
  8. 建立副本名称冲突_包的建立(一)
  9. 查询没有走索引_关于MySQL种的in函数到底走不走索引、我和同事差点大打出手!...
  10. 做人的36条常情世故
  11. iPhone8用的全面屏是什么屏幕?
  12. Nginx启动/重启脚本详解
  13. StanfordDB class自学笔记 (15) Recursion
  14. c语言窗口炸弹代码,C语言实现宾果消消乐
  15. Excel数据分析和建模
  16. CTF warmup
  17. html tooltips效果,CSS3+jQuery轻松实现工具提示(Tooltips)
  18. 微信会员卡跳转小程序实现
  19. 斑凶鹰来袭,斑鸠逃生
  20. 四个Channel的讲解

热门文章

  1. SQL16号统计1~15号数据,1号统计上月15~月底数据
  2. 怎么查看网站数据库服务器ip地址,怎么查看网站数据库服务器ip地址吗
  3. 百度智能云 API鉴权总结
  4. java获取当前时间的前一个小时
  5. 怎么用VBA在excel中指定位置画图表? 如怎么用VBA在 A7:G13位置画出图标
  6. 如何在公众号添加付费链接
  7. 联想拯救者R720双系统如何进bios
  8. 整理一些全志 D1 / D1s 的 DisplayEngine / LCD / HDMI 常用骚操作
  9. html5 微网页 点餐_使用HTML5和微数据向网页添加电话号码
  10. c语言opencv所用库函数,初窥Opencv