1.、请简单的介绍下APNS发送系统消息的机制
APNS优势:杜绝了类似安卓那种为了接受通知不停在后台唤醒程序保持长连接的行为,由iOS系统和APNS进行长连接替代。
APNS的原理:
1). 应用在通知中心注册,由iOS系统向APNS请求返回设备令牌(device Token)
2). 应用程序接收到设备令牌并发送给自己的后台服务器
3). 服务器把要推送的内容和设备发送给APNS
4). APNS根据设备令牌找到设备,再由iOS根据APPID把推送内容展示
2、什么是 Method Swizzle(黑魔法),什么情况下会使用?
1). 在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法 Method Swizzle。
2). Method Swizzle 指的是改变一个已存在的选择器对应的实现的过程。OC中方法的调用能够在运行时通过改变,通过改变类的调度表中选择器到最终函数间的映射关系。
3). 在OC中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用OC的动态特性,可以实现在运行时偷换selector对应的方法实现。
4). 每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的方法实现。
5). 我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP。
6). 我们可以利用 class_replaceMethod 来修改类。
7). 我们可以利用 method_setImplementation 来直接设置某个方法的IMP。
8). 归根结底,都是偷换了selector的IMP。
3、通信底层原理(OSI七层模型)
物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
4、描述下SDWebImage里面给UIImageView加载图片的逻辑
SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前会先显示占位图片,当真实图片被加载出来后再替换占位图片。
加载图片的过程大致如下:
1.首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存
2.如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来
3.如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片
4.下载后的图片会加入缓存中,并写入磁盘中
5.整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来
SDWebImage原理:
调用类别的方法:
1. 从内存(字典)中找图片(当这个图片在本次使用程序的过程中已经被加载过),找到直接使用。
2. 从沙盒中找(当这个图片在之前使用程序的过程中被加载过),找到使用,缓存到内存中。
3. 从网络上获取,使用,缓存到内存,缓存到沙盒。
5、算法排序
1. 选择排序将已排序部分定义在左端,然后选择未排序部分的最小元素和未排序部分的第一个元素交换。
2. 冒泡排序将已排序部分定义在右端,在遍历未排序部分的过程执行交换,将最大元素交换到最右端。
3. 插入排序将已排序部分定义在左端,将未排序部分元的第一个元素插入到已排序部分合适的位置。
/**
* 【选择排序】:最值出现在起始端
*
* 第1趟:在n个数中找到最小(大)数与第一个数交换位置
* 第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置
* 重复这样的操作...依次与第三个、第四个...数交换位置
* 第n-1趟,最终可实现数据的升序(降序)排列。
*
*/
void selectSort(int *arr, int length) {
for (int i = 0; i < length - 1; i++) { //趟数
for (int j = i + 1; j < length; j++) { //比较次数
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
/**
* 【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾
* 第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置
* 第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置
* …… ……
* 第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置
*/
void bublleSort(int *arr, int length) {
for(int i = 0; i < length - 1; i++) { //趟数
for(int j = 0; j < length - i - 1; j++) { //比较次数
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
/**
* 折半查找:优化查找时间(不用遍历全部数据)
*
* 折半查找的原理:
* 1> 数组必须是有序的
* 2> 必须已知min和max(知道范围)
* 3> 动态计算mid的值,取出mid对应的值进行比较
* 4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
* 5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
*
*/
// 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置
int findKey(int *arr, int length, int key) {
int min = 0, max = length - 1, mid;
while (min <= max) {
mid = (min + max) / 2; //计算中间值
if (key > arr[mid]) {
min = mid + 1;
} else if (key < arr[mid]) {
max = mid - 1;
} else {
return mid;
}
}
return -1;
}
6、 UITableView 的优化
1). 正确的复用cell。
2). 设计统一规格的Cell
3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;
4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
4). 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!
5). 减少子视图的层级关系
6). 尽量使所有的视图不透明化以及做切圆操作。
7). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示。
8). 使用调试工具分析问题。
7、.你用过NSOperationQueue么?如果用过或者了解的话,你为什么要使用NSOperationQueue,实现了什么?请描述它和G.C.D的区别和类似的地方(提示:可以从两者的实现机制和适用范围来描述)。
答:使用NSOperationQueue用来管理子类化的NSOperation对象,控制
其线程并发数目。GCD和NSOperation都可以实现对线程的管理,区别
是 NSOperation和NSOperationQueue是多线程的面向对象抽象。项目中
使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中
使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,
是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接
口简单,建议在复杂项目中使用。
项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线
程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议
在简单项目中使用。
8、 runtime 相关
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。
Runtime消息传递
一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:
首先,通过obj的isa指针找到它的 class ;
在 class 的 method list 找 foo ;
如果 class 中没到 foo,继续往它的 superclass 中找 ;
一旦找到 foo 这个函数,就去执行它的实现IMP 。
但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。
objec_msgSend的方法定义如下: OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体:
//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
系统首先找到消息的接收对象,然后通过对象的isa找到它的类。
在它的类中查找method_list,是否有selector方法。
没有则查找父类的method_list。
找到对应的method,执行它的IMP。
转发IMP的return值。
IMP
看下IMP的定义
/// A pointer to the function of a method implementation. 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);
#endif
就是指向最终实现程序的内存地址的指针。
在iOS的Runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
类缓存(objc_cache)
当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。
Runtime消息转发
前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。
1动态方法解析
2备用接收者
3完整消息转发
Runtime应用
Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。
1关联对象(Objective-C Associated Objects)给分类增加属性
2方法魔法(Method Swizzling)方法添加和替换和KVO实现
3消息转发(热更新)解决Bug(JSPatch)
4实现NSCoding的自动归档和自动解档
5实现字典和模型的自动转换(MJExtension)
9、 KVO基于runtime
全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。
KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。
Apple 使用了 isa-swizzling 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
NSKVONotifying_A 类剖析
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);
在建立KVO监听前,打印结果为:
self->isa:A
self class:A
在建立KVO监听之后,打印结果为:
self->isa:NSKVONotifying_A
self class:A
在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被KVO 机制修改为指向系统新创建的子类NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。
子类setter方法剖析
KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangeValueForKey: ,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;
当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用
}
10、ios 内存管理和属性修饰
retain/assign/strong/weak/copy的区别
这几个关键字在声明变量的时候经常遇到,那么它们有什么区别,应该如何选择呢?
这些关键字的主要区别,在于对内存的管理。
Objective-C的内存管理机制和引用类别
首先,要介绍一下Objective-C的内存管理机制。Objective-C对内存的管理,以一种叫做“retain count”的方式来管理。对于一个对象,当它被new/alloc/retain的时候,它的retain count会加1;当它被release的时候,它的retain count会减1;当它的retain count达到0的时候,就表示没有其它对象再引用它,这时候编译器会自动调用其dealloc方法,销毁掉这个对象。
另外,对于一个对象,其它对象引用它的方式,有2种方式,一种叫强引用,这个时候retain count会加1;另一种叫弱引用,这个时候retain count不变;这就产生一种结果:对于一个对象A,如果A的强引用都被释放掉了,即使还有其它的弱引用,A的retain count还是会变成0,也就是被销毁掉。这也就是弱引用的“弱”所在。
关键字的区别
有了以上2个概念,就可以区分出以上关键字的区别了。
在MRR(即手动管理内存)阶段,retain表示强引用,assign表示弱引用。用retain修饰的对象,赋值的时候会调用一次retain,将对象的retain count加1;而assign修饰的对象,则直接赋值,retain count不变;
对于int/float等原生的数据类型,可以使用assign;而对于其它需要在多个地方使用的对象,需要用到retain;
iOS5的时候,苹果推出了一个新特性,ARC,这个特性的好处就是编译器会在适当的时候自动添加retain/release/autorelease语句,就避免了人工管理这些代码。简单的理解,strong就是ARC中的retain,而weak就是ARC阶段的assign;
weak和assign的区别
weak和assign很像,它们的区别主要在于:当weak对象指向的内存被释放掉后,它会自动被置为nil,避免了crash;而assign指针在这种情况下会变成野指针,比较危险。
copy关键字
copy关键字比较复杂。在对于不可变类型时,它会和strong一样,拷贝指针;而对于可变类型时,它就会将内容拷贝一份,并指向新的内容。比如:对于NSString,copy的结果和strong一样;而对于NSMutableString,copy就会复制一份出来。
从以上copy的表现来看,可以看出,拷贝的情况有2种:一种是拷贝指针,而指向同一块内存,这种拷贝叫浅拷贝;另外一种,则是将内存拷贝一份,然后指向新的内存,这种拷贝叫深拷贝。
强引用导致的retain cycle
强引用会出现一种导致内存泄漏的结果:retain cycle。简单来说,就是A强引用了B,而B也强引用了A。这个时候,A等着B释放A,B也等着A释放B,就会导致A和B都无法释放掉,就造成了内存泄漏。这种情况会在以下情况发生:
父类/子类之间;
使用delegate的时候;
代码块block中。block本身是一个强引用,而且它会建立变量的快照,即强引用处于block里面的变量,这也会导致retain cycle;
如何避免?
对于1和3,可以通过weak关键字,将其中一个引用转成弱引用,这样就打破了retain cycle;
对于2,一般用”__weak”来修饰。很多时候是使用一个weak的self,来防止self类和block形成retain cycle。
注意:UIView的animation block不会导致retain cycle,因为self并没有引用这个block。
最后,对于内存泄漏,如何调试呢?可以用Instruments里的Leak工具来调试,比较方便。
11、ios 内存几大区域
说到iOS的内存管理,大家首先想到的可能是引用计数相关的东西,而跟引用计数相关的内存都是分布在堆区(heap),也就是说我们平时关注最多的部分都是堆区的内存。其实在iOS系统中(其他操作系统也一样),内存的分布区域大致可以分为三个部分:栈区(stack),堆区(heap),和全局静态区(global)。其中栈区主要存放局部变量和函数参数等相关变量,超出作用域之后自动释放,堆区存放alloc,new等关键字生成的对象;全局静态区主要存放静态数据,全局数据和常量,程序运行之后一直存在。分布如下图所示:
![image.png](https://img.haomeiwen.com/i2483162/5f63faaf88710070.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
12、ios block 为啥用copy
内存的栈区 : 由编译器自动分配释放, 存放函数的参数值, 局部变量的值等. 其操作方式类似于数据结构中的栈.
内存的堆区 : 一般由程序员分配释放, 若程序员不释放, 程序结束时可能由OS回收. 注意它与数据结构中的堆是两回事, 分配方式倒是类似于链表.
首先, block是一个对象, 所以block理论上是可以retain/release的. 但是block在创建的时候它的内存是默认是分配在栈(stack)上, 而不是堆(heap)上的. 所以它的作用域仅限创建时候的当前上下文(函数, 方法...), 当你在该作用域外调用该block时, 程序就会崩溃.
注意:
1.一般情况下你不需要自行调用copy或者retain一个block. 只有当你需要在block定义域以外的地方使用时才需要copy. Copy将block从内存栈区移到堆区.
2.其实block使用copy是MRC留下来的也算是一个传统吧, 在MRC下, 如上述, 在方法中的block创建在栈区, 使用copy就能把他放到堆区, 这样在作用域外调用该block程序就不会崩溃.
3.但在ARC下, 使用copy与strong其实都一样, 因为block的retain就是用copy来实现的, 所以block使用copy还能装装逼, 说明自己是从MRC下走过来的
----------------------------
关于block在内存中的位置
block快的存储位置(block入口的地址)可能存放在3个地方:代码区(全局区)、堆区、栈区(ARC情况下回自动拷贝到堆区、因此ARC下只有两个地方:代码区和堆区)。
代码区:不访问出去栈区的变量(如局部变量),且不访问堆区的变量(如用alloc创建的对象)时,此时block存放在代码区;
堆区:如果访问了处于堆区的变量(如局部变量),或堆区的变量(如用alloc创建的对象),此时block存方在堆区;--需要注意
-1 实际是放在栈区,在ARC情况下油自动拷贝到堆区,如果不是ARC则存放在栈区,所在函数执行完毕就回释放,想再外面调用需要用copy指向它,这样就拷贝到了堆区,strong属性不会拷贝、会造成野指针错区。(需要理解ARC是一种编译器特性,即编译器在编译时在核实的地方插入retain、release、autorelease,而不是iOS的运行时特性)。
-2 此外代码存在堆区时,需要注意,因为堆区不像代码区不变化,堆区是动态的(不断的创建销毁),当没有强指针指向的时候就会被销毁,如果再去访问这段代码时,程序就会崩溃!所以此种情况在定义block属性时需要指定为strong or copy。block是一段代码,即不可变,所以使用copy也不会深拷贝。
总结
根据上面提到的block存放位置可知,当使用strong类型的有可能造成野指针问题,而copy的时候是没有问题的。
13、ios 性能调优
iOS性能调优总结
性能调优的方式:
1、通过专门的性能调优工具
2、通过优化代码
1. 性能调优工具:
1.1 静态分析工具Analyze
相信IOS开发者在App进行Build或Archive时,会产生很多编译警告,这些警告是编译时产生的,静态分析的过程也类似,在XCode Product菜单下,点击Analyze对App进行静态分析。
Analyze主要分析以下四种问题:
1\. 逻辑错误:访问空指针或未初始化的变量等;
2\. 内存管理错误:如内存泄漏等;
3\. 声明错误:从未使用过的变量;
4\. Api调用错误:未包含使用的库和框架。
1.2 内存泄漏分析工具–Leaks
点击XCode的Product菜单Profile启动Instruments,使用Leaks开始动态分析。
选择Leaks,会自动启动Leaks工具和IOS模拟器,Leaks启动后会开始录制,随着对模拟器运行的App的操作,可以在Leaks中查看内存占用的情况。
注意:其中黑色头像代表了最可能的位置
注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的。
开启ARC后,内存泄漏的原因:
开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。
在IOS 中使用malloc分配的内存,ARC是不会处理的,需要自己进行处理。
例子中的 CGImageRef 也是一个Image的指针,ARC也不会进行处理。
1.3 不合理内存分析工具–Allocation
关于内存的问题,除了内存泄漏以外,还可能存在内存不合理使用的情况,也会导致IOS内存警告。
内存的不合理使用往往比内存泄漏更难发现,内存泄漏可以更多借助于工具的判断,
而内存的不合理运用更多需要开发者结合代码、架构来进行分析。
明确说明一下两者的区别:
- 内存泄漏:是指内存被分配了,但程序中已经没有指向该内存的指针,导致该内存无法被释放,产生内存泄漏。
- 内存不合理运用:苹果官方称这种情况为Abandoned Memory,也就是存在已分配内存的引用,但实际上程序中不会使用,比如图片等对象加入了缓存,但缓存中的对象一直没有被使用。
- XCode提供的Instruments中的Allocation工具可以用来帮你了解内存的分配情况,当你的App收到内存警告时,首先应该用Allocation进行内存分析,了解哪些对象占用了太多内存。
1.4 干掉僵尸对象 Zombies
-
僵尸对象,也就是我们会遇到的EXC_BAD_ACCESS错误,由于内存已经被释放,而这个对象仍旧保留这那个坏地址而导致的。
-
在MRC的开发中,这个错误比较常见,ARC下面在使用到C++的代码也会遇到。
-
不过这个工具比较简单,遇到这类错误,打开这个位于Instruments下的工具,直接就能帮你定位到,这里就不赘述了。
【重点】1.5 性能提升工具 Time Profile
既然是性能调优,那么怎么提升代码的运行效率其实才是我们程序员最直接的诉求,而这个工具可以辅助我们办到这件事。
Time Profiler分析原理:
它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,
并获得一个近似值。它将各个方法消耗的时间统计起来,形成了我们直接定位需要进行优化的代码的好帮手。
选择Time Profiler工具开始测试,这时会自动启动模拟器和Time Profiler录制。
先进行一些App的操作,让Time Profiler收集足够的数据,尤其是你觉得那些有性能瓶颈的地方。
需要关注的:显示的堆栈查看;看到cpu运行的时间都消耗在哪里;通过对应用的操作,可以在详细面板中看到那些最耗时的操作是哪些图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方,可以逐渐向下展开,找到产生的根本原因。
Time Profiler参数设置
这里边几个选项的含义如下:
-
Separate by Thread:
每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的”重”线程
-
Invert Call Tree:
从上倒下跟踪堆栈,这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中耗费时间最深的方法.也就是说FuncA{FunB{FunC}} 勾选此项后堆栈以C->B-A 把调用层级最深的C显示在最外面
-
Hide Missing Symbols:
如果dSYM无法找到你的app或者系统框架的话,那么表中看不到方法名只能看到十六进制的数值,如果勾线此项可以隐藏这些符号,便于简化数据
-
Hide System Libraries:
勾选此项你会显示你app的代码,这是非常有用的. 因为通常你只关心cpu花在自己代码上的时间不是系统上的
-
Show Obj-C Only:
只显示oc代码,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的 Flatten Recursion: 递归函数, 每个堆栈跟踪一个条目 Top Functions: 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。
上面的参数在实践中合理设置,也没有什么太多技巧,就是通过数据的隐藏、显示让我们更关注于想找到的数据。
2. 性能调优的代码优化:
下面介绍一下在开发中可以直接进行的代码优化的方面。
2.1 views设置为不透明(opaque=yes)
(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES, 渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。
如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。如果这个属性为NO,GPU会利用图层颜色合成公式去合成真正的色值,这在ScrollView或者动画中是很消耗性能的。
2.2 不要阻塞主线程
永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。
一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。
大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。
通常建议这些操作都使用GCD的方式直接异步执行,并将UI相关操作在主线程进行回调,像这样:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 切换到全局队列,异步执行耗时操作 dispatch_async(dispatch_get_main_queue(), ^{
// 切换到主线程,更新你的UI。 }); });
2.3 提前调整ImageView中的图片大小(同图片和动画的渲染)
如果要在UIImageView中显示一个图片,你应保证图片的大小和UIImageView的大小相同。
因为在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。
如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。
这个类比到图片和动画的渲染中,是通用的。
具体方法参考上面的GCD操作。
2.4 正确使用容器的特性
Arrays: 有序的一组值。使用index来查找很快,使用value 查找很慢, 插入/删除很慢。
Dictionaries: 存储键值对。 用键来查找比较快。
Sets: 无序的一组值。用值来查找很快,插入/删除很快。
2.5 大文件传输使用gzip
大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。
减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
当然,现在苹果已经自动支持了,你只需要告诉你们服务端的同学,传输大文件的时候记得用gzip就完了。
2.6 View的重用和懒加载
更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
重用就是模仿UITableView和UICollectionView的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
当需要使用View的时候,去可重用队列里面找一找有没有可以被复用的View。
这里我的一份框架中曾经使用过类似的方法去创建一个图片浏览器,大家可以稍做参考。View的重用
懒加载就是在程序启动时并不进行加载,只有当用到这个对象的时候,才进行加载。
这个不仅在属性中可以进行这样的使用,在View上面也是一样,不过实现稍有不同。
懒加载会消耗更少内存,但是在View的显示上会稍有滞后。
2.7 Cache
一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。
我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。
NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。
下面是一个可用的代码段,你可以可以用它去为一个基本不会改变的图片创建一个NSURLRequest并缓存它:
+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
// this will make sure the request always returns the cached image
request.HTTPShouldHandleCookies = NO;
request.HTTPShouldUsePipelining = YES;
[request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
return request;
}
注意你可以通过 NSURLConnection 获取一个URL request, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。
如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。
2.8 记得处理内存警告
一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:
- 如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.
幸运的是,UIKit提供了几种收集低内存警告的方法:
在app delegate中使用applicationDidReceiveMemoryWarning:的方法在你的
自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning 注册并接收
UIApplicationDidReceiveMemoryWarningNotification 的通知
一旦收到这类通知,你就需要释放任何不必要的内存使用。
例如,UIViewController的默认行为是移除一些不可见的view, 它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。
这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。
然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。
2.9 重用大的开销对象
这里的大开销是指一些初始化很慢的objects,如:NSDateFormatter和NSCalendar。但是,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。
想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。
下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:
// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;
// inside the implementation (.m)
// When you need, just use self.formatter -
(NSDateFormatter *)formatter {
if (! _formatter) {
_formatter = [[NSDateFormatter alloc] init];
_formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// twitter date format
}
return _formatter;
}
还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。
2.10 避免反复的处理数据
许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。
比如你需要数据来展示一个tableview,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。
类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。
2.11 正确设定背景图片
在View里放背景图片就像很多其它iOS编程一样有很多方法:
- 使用UIColor的 colorWithPatternImage来设置背景色;
- 在view中添加一个UIImageView作为一个子View。
- 如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存:
// You could also achieve the same result in Interface Builder
UIImageView *backgroundView = [ [UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.view addSubview:backgroundView];
- 如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
2.12 试试苹果最新的WKWebView来处理web
UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。
但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation 为特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要调整下你的HTML了。
第一件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。
当然,上面是针对你在使用UIWebView的情况下,需要尽量减少使用web的特性,而苹果最近刚推出的Safari的底层框架WKWebView也许能帮我们规避掉很多这样的性能问题。
2.13 优化你的TableView
Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。
为了保证tableview平滑滚动,确保你采取了以下的措施:
正确使用reuseIdentifier来重用cells
尽量使所有的view opaque,包括cell自身
避免渐变,图片缩放,后台选人 缓存行高
如果cell内现实的内容来自web,使用异步加载,
缓存请求结果 使用shadowPath来画阴影
减少subviews的数量
尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
使用正确的数据结构来存储数据 使用rowHeight, sectionFooterHeight 和 sectionHeaderHeight来设定固定的高,不要请求delegate
2.14 选择正确的数据存储方式
存储方式:
使用NSUerDefaults,使用XML, JSON,或者 plist 使用NSCoding存档,使用类似SQLite的本地SQL数据库 使用 Core Data
-
NSUserDefaults的问题是什么?
虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了
-
XML这种结构化档案呢?
总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。
NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。 -
使用SQLite 或者CoreData比较好
使用这些技术你用特定的查询语句就能只加载你需要的对象。
在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graphmodel,但SQLite就是一个DBMS。
Apple在一般情况下建议使用Core Data,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。
如果你使用SQLite,你可以用FMDB(https://github.com/ccgus/fmdb)这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。
2.15 把Xib换成Storyboard吧
当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。
Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.
当加载XIB时,所有图片都被缓存,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:
当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。
在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,
仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage 的`imageNamed:`方法来获取图片资源。
很明显,同样的事情也发生在storyboards中,但我并没有找到任何支持这个结论的文档。另外,快速打开app是很重要的,特别是用户第一次打开它时,对app来讲,第一印象太太太重要了。你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据,解析数据。
还是那句话,避免过于庞大的XIB,因为他们是在主线程上加载的。所以尽量使用没有这个问题的Storyboards吧!注意,用Xcodedebug时watchdog并不运行,一定要把设备从Xcode断开来测试启动速度
2.16 学会手动创建Autorelease Pool
NSAutoreleasePool负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。
假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。
好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:
NSArray *urls = [@"url1",@"url2"];
for (NSURL *url in urls){
@autoreleasepool{
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL: url encoding: NSUTF8StringEncoding error: &error];
/* Process the string, creating and autoreleasing more objects. */
}
}
这段代码在每次遍历后释放所有autorelease对象
14、http 2.0 1.0
HTTP 2.0与HTTP 1.1区别
1、什么是HTTP 2.0
HTTP/2(超文本传输协议第2版,最初命名为HTTP 2.0),是HTTP协议的的第二个主要版本,使用于万维网。HTTP/2是HTTP协议自1999年HTTP 1.1发布后的首个更新,主要基于SPDY协议(是Google开发的基于TCP的应用层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验)。
2、与HTTP 1.1相比,主要区别包括
- HTTP/2采用二进制格式而非文本格式
- HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个连接即可实现并行
- 使用报头压缩,HTTP/2降低了开销
- HTTP/2让服务器可以将响应主动“推送”到客户端缓存中
网友评论