解答:分类是在已有的类上进行功能的拓展,并且可以在不知道已存在类内部实现的情况下进行功能拓展,与原来的类文件分开;扩展则是在原有类内部实现功能的拓展,与原有类必须共一个文件,比如给类添加一个私有成员变量等。
分类的结构体中包含实例方法列表、实例属性列表、协议列表、类方法列表、主类指针等。结构体如下:
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是声明属性的关键字,与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修饰的对象,被释放的时候,系统会清空对象对应的弱引用表中存储的弱引用指针,并将指针的值置为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。
解答:
底层实现:1、为被观察的类声明了一个子类,并将父类的isa指针指向这个子类(同时系统也重写了class相关方法,隐藏了isa的指向);2、通过之类重写setter方法,在改变之前调用willChangeValueForKey方法存储旧值,改变之后调用didChangeValueForKey触发- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary
取消系统默认的KVO并手动触发:
1、重写下面的方法,默认返回yes,自动触发,改为no就是不自动触发;
- (void)willChangeValueForKey:(NSString*)key;
- (void)didChangeValueForKey:(NSString*)key;
更多可以参考iOS KVO原理用法及自定义KVO
解答: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的官方图如下:
至于实例对象的方法存在类对象的结构体里面的原因,我想应该是为了节约资源,所有实例共享一份内存,达到资源重复利用,类对象在内存中只有一份,实例对象可以根据需求new很多份。
解答:从字面上理解,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);
}
解答:内省(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的源码发现,创建一个新类,会添加到全局的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;
}
解答:如果一个block外部的auto变量,需要被block内部引用并且赋值,则需要在变量前加上__block修饰,否则会编译器会直接报错。那么__block做了什么骚操作呢?我们看一下下面的代码:
底层原理:
#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与他要捕获的外部变量有相互引用时,会造成循环引用,从而造成内存泄漏。为了解决循环引用,在ARC环境下,我们的通用做法是通过__weak关键字来修饰block需要捕获的外部变量。这里面实现的原理是:__weak(具体的底层逻辑参考iOS weak底层原理及源码解析)修饰变量后,不会导致对象的引用计数器+1,对对象的释放没有影响,从打破循环引用。但是当我们block里面执行的任务是一个延时的操作,任务还没执行时,外部的变量已经释放掉了,当执行任务时,weak指针已经为nil,虽然不会导致crash,但是会影响业务逻辑的执行。解决这个问题的方式就是在block的内部再一次对捕获的weak指针进行强引用,即用__strong修饰下,这样既能打破循环引用,又能延迟对象的释放,保证业务逻辑的完整性。
解答: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,一一分析。
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种:
每个mode都有对应的observers,timer,source0,source1,port等变量。给一个runloop运行的官方图:
每个mode的运行机制如下:
上面的源码可以看出,线程跟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。
我们知道,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,那么这个过程被称之为离屏渲染。
离屏渲染涉及到两个缓冲区上下文环境的切换,这个过程是非常消耗性能的,因此需要尽量的避免离屏渲染。
xxx.layer.cornerRadius = 20.0;
xxx.layer.masksToBounds = YES;
虽然iOS 9.0以后,苹果系统对此做了改进,UIImageView的加载的图片格式是PNG的,使用这种方式不会导致离屏渲染,但是其他控件依旧存在这个问题。
解决方案:
在所需切圆角的控件上加一层,利用贝塞尔切割出不同角所需要的圆角半径。具体参考笔记-圆角四种方法的对比以及性能检测
注意:还有很多其他的方式解决圆角性能的问题,但是有两个方式值得提一下,一个是在drawRect方法里面用CoreGraphics的API画圆弧,这个方式的缺点是drawRect会非常占用内存。另外一个是用CAShaperLayer结合贝塞尔画圆角,这个本质还是利用了mask,mask也会导致离屏渲染。所以这两种方式都不推荐。
shadow/mask:
解决方案:建议不要使用shadow/mask。
光栅化:
光栅化需要视情况而定,当无法避免离屏渲染,且当内容为静态的时,没有动画之类的操作,即前后可以重复利用,将光栅化设置为true,反而对性能的提升有很好的效果。当然如果没有发生离屏渲染,就不要打开了。
抗锯齿:
综合考虑,是否开关。
Group opacity组合透明度:
iOS7以后默认是设置为开的,建议关掉。
未完待续。。。