iOS高级面试题及部分答案

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说明改的是isa
    if (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) {
    // 取isa
    if (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();
    
    // 出于性能考虑,在锁之外autorelease
    return 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 

@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 *)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;//上一个page
 AutoreleasePoolPage *child;//下一个page
 uint32_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;//指向父类,元类对象只想元类对象的父类,直到根元类NSObject
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags,这里面保存着方法,属性,协议,成员变量等信息
	...
}

其关系iOS的官方图如下:iOS高级面试题及部分答案_第1张图片
至于实例对象的方法存在类对象的结构体里面的原因,我想应该是为了节约资源,所有实例共享一份内存,达到资源重复利用,类对象在内存中只有一份,实例对象可以根据需求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 categories
    methodizeClass(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 allocation

    if (supercls  &&  (supercls->info & CLS_CONSTRUCTING)) {
        // Can't make subclass of an in-construction class
        return 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做了什么骚操作呢?我们看一下下面的代码:iOS高级面试题及部分答案_第2张图片
底层原理:

#include 

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;//创建block
    impl.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 loop
    pthread_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;//timer
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet; //port
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_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运行的官方图:iOS高级面试题及部分答案_第3张图片
每个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);
}

运行的结果如下:iOS高级面试题及部分答案_第4张图片
我们看到testThread这个方法没执行。
我们把testrun方法里面的启动runloop的代码[[NSRunLoop currentRunLoop] run]打开,执行看下结果如下:iOS高级面试题及部分答案_第5张图片
我们看到的结果是执行了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内,完成上述过程,如下图:
iOS高级面试题及部分答案_第6张图片

何为离屏渲染:

如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的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面试)