Mochangxing


  • Home

  • Archives

iOS性能优化之帧率优化

Posted on 2018-08-27

影响帧率的原因

屏幕显示图像的原理:

一般而言,计算机系统中CPU、GPU以及显示器以以上这种方式协同工作。CPU计算好需要会显示的内容提交给GPU,GPU渲染完成后将渲染结果存入缓存区,随后视频控制器根据VSync信号,将缓冲区的内容显示到显示器上。

在这个过程中,CPU的主要职责是计算,GPU的主要职责是渲染

影响帧率的原因

* CPU的资源消耗原因
  1. 对象的创建

对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗资源。尽量用轻量级的对象代替重量对象,可以对内存有所优化。比如CALayer比UIView要轻量很多,因为不需要相应触摸事件。如果对象不涉及UI操作尽量放到后台线程去创建。,另外通过Storyboard创建视图对象时,要比直接通过代码创建对象消耗非常多的资源。

  1. 对象的调整

    对象的调整也是消耗CPU资源的原因之一。特别指出:CALayer内部并没有属性,当调用属性方法时,它内部是通过Runtime的resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存在内部的一个Dictionary中,同时还会通知delegate、创建动画等等,非常消耗资源,UIView关于显示相关属性的调整实际上都是对CALayer属性的调整。

  2. 对象的销毁

对象销毁虽然对资源的消耗不多,但是积累起来也不容忽视。通常容器类持有大量对象时,其销毁时资源的消耗就非常明显。如果对象能放到后台释放,那就放到后台释放。为了避免编译器警告,把对象捕获到Block,再扔到后台队列随便发送个消息就OK。

1
2
3
4
5
SArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
  1. 布局计算

布局计算是App种最为常见的资源消耗的地方,如果能在后台提前计算好布局、并对布局进行缓存,那么就不会产生性能问题了。还有要尽量避免多次频繁的计算UIView.frame/bounds/center 等属性。因为调整这些属性非常消耗性能。

  1. 文本计算

如果一个页面中包含大量文本,文本的宽高会占用很大一部分资源。可以使用[NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。

  1. 文本渲染

常见的文本控件(UILabel、UITextView等)、其排版和绘制都是在主线程进行的,当文本数量很大时,CPU压力非常大。对此的解决方案是,自定义文本控件,用TextKit或者CoreText对文本异步绘制。可以参考YYTextKit。

  1. 图片的解码

UIImage或者CGImageSource创建的图片,并不会立刻解码。当图片设置到UIImageView或者CALayer.contents中,且CALayer被提交到CPU前,CGImage才会被解码,这一步发生在主线程。想要绕开这个机制,常见的做法是在后台线程将图片绘制到CGBitmapContext中,然后冲Bitmap中直接创建图片。目前常见的图片库都带有这个功能。

  1. 图像的绘制

图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。大致过程如下:

1
2
3
4
5
6
7
8
9
10
11
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
* GPU的资源消耗
  1. 纹理的渲染

所有的bitmap,包括图片、文本、栅格化的内容,最终都是有内存提交到显存,绑定为GPU Texture。不论是提交显存的过程,还是GPU调整和渲染Texture的过程都要消耗不少GPU资源。 短时间内显示大量图片时(比如UITableView 存在大量图片并快速滑动时),CPU占用了率很低,GPU却非常高,界面仍然会掉帧。针对这种情况只能将减少短时间显示大量图片,尽可能将多张图片合成一张进行显示。
另外当图片过大,超过GPU的最大纹理尺寸时,需要先交由CPU进行预处理。这对CPU个GPU都会带来额外的消耗。

  1. 视图的混合

当多个视图重叠在一起显示时,GPU首先会把它们混合到一起,再进行渲染。如果视图结构过于复杂,混合过程也会消耗很多GPU资源。最好的办法是,尽量减少视图层级和数量,并在不透明的视图里标明opaque属性以避免无用的Alpha通道合成

  1. 图形的生成

离屏渲染,CALayer的 border、圆角、阴影、遮罩,CASharpLayer的矢量图显示,通常会触发离屏渲染。而离屏渲染通常发生在GPU中。开启 CALayer.shouldRasterize属性会使视图渲染内容被缓存起来,下次绘制的时候可以直接显示缓存。而最彻底的解决办法是:把需要显示的图型在后台线程绘制为图片,避免使用圆角、阴影遮罩等属性。

如何发现被定位卡顿问题

在了解了影响帧率的原因以及解决办法后,我们可以在开发过程中应该提高警惕,尽量避免写影响性能的代码。但是当我们接手一个新的业务模块时,应该如何发现卡顿问题并定位呢?

卡顿检测工具

目前业界比较通用的卡顿检测方案是:FPS检测 + 主线程RunLoop检测

其主要原理是,开辟一个子线程检测主线程RunLoop,检测kCFRunLoopBeforeSources与kCFRunLoopAfterWaiting这两个状态间的耗时,并综合FPS指标的来检测卡顿。

监听主线程RunLoop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)start
{

// 创建信号 用于阻塞子线程
semaphore = dispatch_semaphore_create(0);

// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

// 开启子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 16*NSEC_PER_MSEC));
if (st != 0)
{
//主线程超时,综合FPS判断是佛卡顿,并dump堆栈信息
}
}
});
}

FPS 检测基于DisplayLink计算帧率。
卡顿堆栈信息获取,这里就不展开了,可以参考这篇文章获取任意线程调用栈的那些事,分析的很详细

买家版帧率优化实践

买家版feed流中最为常见的卡片,包含了大量的图片以及文字,卡片视图数量很多,层级也很复杂。大量图片以及文本的绘制可以说是影响帧率的最大因素。因此初步的优化方案是,引入ASDisplayKit异步渲染框架,替换文本控件和图片控件。ASDisplayKit的核心思想是将所有能异步执行的操作如:文本和布局的计算、渲染、图片的解码、绘制等通通从主线程移到子线程。

在引入ASDisplayKit后,页面帧率有所提升,但卡顿情况仍较为严重。而页面的卡片类型多大七八种,而每个卡片的实现代码少则大大几百行,多则一千多行。如果单纯代码Review查找性能瓶颈的代码,是一件相当低效的事情,因此引入了卡顿检测工具。引入卡顿检测工具后,发现:虽然使用了FDTemplateLayoutCell缓存了Cell高度避免重复计算,但是卡片所有文本宽高计算,并且没有缓冲,每次视图进入屏幕时都需要重新计算,而文本宽高的计算恰恰又是影响性能重要原因之一。

AutoLayout也是影响性能的一个因素,买家版的卡片也大量使用了AutoLayout,如果将卡片的布局移到异步线程,那么性能还会有所提升。

综上,买家版的性能瓶颈主要在于:文本宽高的计算、大量文本的绘制、大量图片的解码绘制以及AutoLayout带来的性能影响。

卡顿检测发现的其他问题:1. 主线程读取缓存并反序列化;2. VDTrick 遍历UIController.view 的所有子view;3. 网络请求回调时,主线程进行耗时的卡片高度计算操作。3.API的使用不当

RAC rac_signalForSelector 如何实现对象方法的hook

Posted on 2018-08-26

RAC rac_signalForSelector 如何实现对象方法的hook

重温Objective-C的消息机制

消息转发机制:

  1. 首先在该类的缓存方法列表cache_method_list中查找,是否存在相关方法
  2. 上一步中若没有命中,则从方法列表 objc_method_list中查找
  3. 上一步中若没有命中,则从父类super的方法列表 objc_method_list中查找,直至根类NSObject
  4. 上一步中若没有命中,则进入消息转发流程,一共分为三步:类的动态方法解析、备用接收者对象、完整消息转发
  5. 动态方法解析:也就是 +(BOOL)resolveClassMethod:方法或+(BOOL)resolveInstanceMethod:(SEL)sel方法。该方法允许向当前对象添加方法实现。
1
2
3
4
5
6
7
8
9
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(doFoo:))
{
class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod];
}
  1. 备用接收者对象:– (id)forwardingTargetForSelector:(SEL)aSelector 方法,
    该方法提供一次机会引导Objective-C RunTime 到备用接收者对象上。
1
2
3
4
5
6
7
-(id)forwardingTargetForSelector:(SEL)aSelector{
if(aSelector == @selector(doFoo:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];

}
  1. 完整消息转发:– (void)forwardInvocation:(NSInvocation *)anInvocation方法,NSInvocation 是Objective-C 消息的对象形式,它包含了消息的所有信息,这也就意味着,一旦有了NSInvocation 对象,你就可以改变这个消息的所有信息,包括目标对象、selector以及参数。例如可以这样做(当然RAC与Aspect所做的事情远远不止这么简单啦):
1
2
3
4
5
6
7
8
9
-(void)forwardInvocation:(NSInvocation *)invocation
{
SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) {
[invocation invokeWithTarget:altObject];
} else {
[self doesNotRecognizeSelector:invSEL];
}
}

rac_signalForSelector 源码走读

- (RACSignal *)rac_signalForSelector:(SEL)selector方法位于NSObject+RACSelectorSignal 这个category下。先来看一下.h 头文件。头文件只对外暴露了以下两个方法。

1
2
3
4
- (RACSignal *)rac_signalForSelector:(SEL)selector;


- (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol;

再来看一下.m 文件

1
2
3
4
5
6
7
8
9
10
11
12
13

- (RACSignal *)rac_signalForSelector:(SEL)selector {
NSCParameterAssert(selector != NULL);

return NSObjectRACSignalForSelector(self, selector, NULL);
}

- (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol {
NSCParameterAssert(selector != NULL);
NSCParameterAssert(protocol != NULL);

return NSObjectRACSignalForSelector(self, selector, protocol);
}

这两方法最终都调用了C函数NSObjectRACSignalForSelector。

  1. 获取Selector的别名aliasSelector
  2. 是否存在关联对象,有则跳至步骤8
  3. 替换类RACSwizzleClass(self)
  4. 获取替换的类,这一步主要是替换了原类中forwardInvocation:的实现。
  5. 创建RACSubject对象,并设置关联对象
  6. 获取原方法class_getInstanceMethod(class, selector);
  7. 若原方法不存在,则向该类添加方法 class_addMethod(class, selector, _objc_msgForward, typeEncoding)。值得注意的是,方法体为_objc_msgForward,即上一节中提到的完整消息转发方法的方法体。
  8. 若原方法存在,则向该类添加aliasSelector,其实现即为原方法的实现,并将原方法的实现替换为_objc_msgForward
  9. 返回RACSubject对象

到此为止,rac_signalForSelector 的全部工作便是将目标selector的实现替换成了消息转发。

接下来,看看消息转发的实现部分,也就是步骤2中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

static void RACSwizzleForwardInvocation(Class class) {
SEL forwardInvocationSEL = @selector(forwardInvocation:);
Method forwardInvocationMethod = class_getInstanceMethod(class, forwardInvocationSEL);

// Preserve any existing implementation of -forwardInvocation:.
void (*originalForwardInvocation)(id, SEL, NSInvocation *) = NULL;
if (forwardInvocationMethod != NULL) {
originalForwardInvocation = (__typeof__(originalForwardInvocation))method_getImplementation(forwardInvocationMethod);
}


id newForwardInvocation = ^(id self, NSInvocation *invocation) {
BOOL matched = RACForwardInvocation(self, invocation);
if (matched) return;

if (originalForwardInvocation == NULL) {
[self doesNotRecognizeSelector:invocation.selector];
} else {
originalForwardInvocation(self, forwardInvocationSEL, invocation);
}
};

class_replaceMethod(class, forwardInvocationSEL, imp_implementationWithBlock(newForwardInvocation), "v@:@");
}

源码很简单,就是hook了forwardInvocation:方法,当触发完整消息转发时,首先交由RACForwardInvocation响应,若RACForwardInvocation响应了则结束消息转发,否则走原消息转发流程。

接下来看看RACForwardInvocation的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

static BOOL RACForwardInvocation(id self, NSInvocation *invocation) {
SEL aliasSelector = RACAliasForSelector(invocation.selector);
RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);

Class class = object_getClass(invocation.target);
BOOL respondsToAlias = [class instancesRespondToSelector:aliasSelector];
if (respondsToAlias) {
invocation.selector = aliasSelector;
[invocation invoke];
}

if (subject == nil) return respondsToAlias;

[subject sendNext:invocation.rac_argumentsTuple];
return YES;
}
  1. 获取selector的别名 aliasSelector
  2. 获取关联对象subject
  3. 执行 aliasSelector,并通过subject将返回值以RACTuple的形式发送出去。

总结一下RAC实现原理:RAC利用RunTime机制将所要监听的方法,全部转发到forwardInvocation:,并像class添加了别名方法aliasSelector,其方法体即原方法的方法体。那么当外部调用原方法时,就会触发消息转发流程。而RAC拦截了forwardInvocation:,并执行别名方法aliasSelector,最后将返回结果发送出去。

RAC在实现过程中,对Runtime的使用相当的深入。针对各种情况的考虑也是相当的周全,其实现也相当严谨,特别值得学习。

深度改造Runtime的弊端

RAC 通过深度改造对象的消息机制以达到AOP的目的,对于日常开发来说相当便利。不过值得注意的是:当一个项目内存在多个库深度改造对象的消息机制,就会产生不可避免的冲突,比如ASpect这个库,它的实现原理有RAC完全一样,应该都是借鉴了KVO的实现方式,唯一的不同点在于ASpect的消息forwardInvocation:实现比RAC稍微多了一步:当对象无法响应selector时,会调用 doesNotRecognizeSelector: 抛出异常。

若同时使用这两个库对同一对象的同一方法Hook,那么该方法将无法被执行,并存在Crash隐患。

经过以上的研究,对Runtime有了更深入的了解。

iOS性能优化之启动优化

Posted on 2018-08-20

这一篇写一写关于iOS的启动,都是前人的文章,自己实践记录一下,加深理解和认识,文末附上参考链接。

App启动流程

main() 函数作为一个应用程序的入口,从App的icon 被点击,到程序进入main函数这段时间内,系统已经完成了很多工作。iOS的启动过程大致可分为两部分:pre-main time ,post-main time

Pre-main time

下图是Apple在WWDC上展示pre-main 阶段的流程图。

  1. Load dylibs

    加载动态库,系统首先会可执行文件,然后加载动态链接库dyld,dyld是一个用来专门加载动态链接库的库(😂)。加载完dyld,dyld从可执行文件的依赖开始,递归的加载所有依赖的动态链接库。

  2. Rebase & Bind

    由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这两步来修正。Rebase就是用来修正动态库内部指针的指向。而动态库之间存在依赖关系,在刚加载完所有动态库后,它们之间还是相互独立的状态。Bind就是将动态库内部符号表指向与所依赖的动态库的资源

  3. Objc

    这一步开始进入Objc阶段,启动Runtime。这一步的主要工作有:注册Objc类,把category的定义插入方法列表,确保每一个selector的唯一

  4. Initializers

    这一步的工作主要有:Objc的+load()函数、C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()、非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度

  • 动态库的load方法早于主二进制的所有load方法被调用

post-main time

post-main time的统计口径:从进入main函数开始,到willFinishLaunch结束。大多数App都会在这个阶段完成相关SDK的初始化工作。

如何精确测量App启动速度

Xcode 测量 pre-main 耗时

Xcode 提供了一个很赞的方法,只需要在 Edit scheme -> Run -> Arguments 中将环境变量 DYLD_PRINT_STATISTICS 设为 1,就可以看到 main 之前各个阶段的时间消耗。

Total pre-main time: 2.1 seconds (100.0%)
         dylib loading time: 106.81 milliseconds (4.9%)
        rebase/binding time: 192.29 milliseconds (8.9%)
            ObjC setup time: 855.42 milliseconds (39.7%)
           initializer time: 995.53 milliseconds (46.2%)
           slowest intializers :
             libSystem.B.dylib :  29.33 milliseconds (1.3%)
    libMainThreadChecker.dylib : 101.49 milliseconds (4.7%)
          libglInterpose.dylib : 297.92 milliseconds (13.8%)
         libMTLInterpose.dylib :  60.73 milliseconds (2.8%)
                       ModelIO :  69.89 milliseconds (3.2%)
              MediaLibraryCore :  44.31 milliseconds (2.0%)
                   WDBuyerUniv : 363.40 milliseconds (16.9%)

以上为买家版的pre-main阶段的详细耗时,可以看到大头是在ObjC setup和initializer阶段

如何统计线上 pre-main 时间

统计全部load方法的耗时

dyld会优先初始化动态库,然后初始化App的可执行文件。只要在最早加载的动态库中Hook App中全部的load函数,然后打点统计就可以了。那么问题就变成了如何找到最早加载的动态库?

动态库的 load 顺序是与 Load Commands 顺序和依赖关系息息相关的。如图所示:

如何调整Load Command 顺序, 让自定义的动态库率先被加载?

思路1:Mach-O 本质是还是一个文件,想要让上图中的AAAAAHookLoad被第一个加载,那么只要把Load Command 与第一个动态库的Load Command 替换即可。该方案需要对Mach-O的文件格式有足够的了解

思路2:编译过程,指定Load Command的顺序。按理说,Mach-O中Load Command的顺序应该是按照一定的规则写入的。只要能找到修改响应规则的方法,应该比思路1容易得多。目前还没找到(😂)

使用思路1的方案调整Load Command 加载顺序后,AAAAAHookLoad 被调整到了最前面

自定义动态库,已Embed framework的方式加入App工程 -> 自定义的某个class load 方法中 hook 全部类的load方法 -> hook方法中统计load方法耗时 -> 输出

注意事项:

1. 不能为了统计性能,自己却造成了性能问题,获取所有的类并且 Hook load 函数还是比较耗时的,控制不好反而增加了启动时间

2. 不当某个class 与其 category 同时实现了 load方法时,只能hook category 中的方法,需要考虑如何同时hook。

度量 C++ Static Initializers

Runtime 会从 mod_init_func 这个 section 中读取所有的函数指针,然后执行函数调用,这些函数指针对应的正是 C++ Static Initializers 和 attribute((constructor))修饰的函数。

因为它们的执行顺序在 load 函数之后,所以可以在 load 函数中把 mod_init_func 中的地址都替换成我们的 hook 函数指针,然后再把原函数指针保存到一个全局数据中,当执行我们的 hook 函数时,从全局数组中取出原函数地址执行。

在这里张贴下主要代码,更多可以参考这个链接:https://github.com/everettjf/Yolo/blob/master/HookCppInitilizers/hook_cpp_init.mm

优化

pre-main 阶段优化

  1. Load dylibs 阶段

    • 这一阶段,优化点主要在减少不必要的动态库,因为动态链接比较耗时,
    • 另外一点就是,尽可能把多个动态库合并成一个。系统的动态库有共享缓存等优化方案,但是我们的动态库变多了的话会非常耗时,所以合并动态库是一个有效且可行的方案
    • 也可以尝试对一些非启动阶段所必须的动态库,延时加载(仅想法,未验证)。
  2. Rebase & Bind

    • 对于Objc,减少class,selector和category这些元数据的数量。
    • 对于C++,减少虚方法,因为虚方法会创建vtable。
  3. Initializers

    • 减少load方法的耗时,尽量将load方法的工作移到initialize。
    • 不要使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用时才初始化,推迟了一部分工作耗时。

main 阶段优化

启动任务分层,不需要及时初始化,不需要在主线程初始化的,都选择异步延时加载。

参考文章

如何精确度量 iOS App 的启动时间

优化 App 的启动时间

今日头条iOS客户端启动速度优化

AVPlayer 视频缓存

Posted on 2018-08-11

AVPlayer 视频缓存

背景

目前买家版发布视频动态以及自动播放功能已经上线,视频白名单也已经全量开发。如果直接使用URL通过AVPlayer播放,系统并不会进行缓存,每次播放都必须重新下载。对于用户而言,流量是极大的消耗,播放体验也不够友好。

AVAssetResourceLoaderDelegate

AVAssetResourceLoaderDelegate:专门用来处理AVAsset资源加载的协议。
使用方式:

1
2
AVURLAsset *urlAsset = ...
[urlAsset.resourceLoader setDelegate:<AVAssetResourceLoaderDelegate> queue:dispatch_get_main_queue()];

设置了resourceLoader的delegate 后,AVPlayer将把视频资源的下载工作交给AVAssetResourceLoaderDelegate来完成。

AVAssetResourceLoaderDelegate 主要有以下两个方法,分别会在资源开始下载以及取消时被执行:

是否拦截资源下载请求

1
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;

取消请求

1
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest

AVAssetResourceLoadingRequest 分析

AVAssetResourceLoadingRequest携带了AVPlayer的请求信息,主要有以下几个属性:

  • NSURLRequest *request: 视频的URL相关信息

  • AVAssetResourceLoadingContentInformationRequest *contentInformationRequest: 视频文件的信息请求,主要是为了获取视频类型、视频大小等相关信息,该请求会在视频第一个AVAssetResourceLoadingRequest中携带。

  • AVAssetResourceLoadingDataRequest *dataRequest: 视频数据请求,指定了视频请求的数据范围,每次请求完成后,都要通过dataRequest将response塞给AVPlayer

AVAssetResourceLoaderDelegate 实现视频请求流程图

如何实现视频缓存

AVAssetResourceLoadingRequest视频请求实际上就是Http文件下载过程,需要通过URLSession创建一个DataTask请求数据。通过以上的分析,我们知道视频下载是一个分片下载的过程,因此视频缓存也应该是基于分片的缓存。而因为有了缓存的存在,当AVPlayer发起一个AVAssetResourceLoadingRequest请求了,就有可能一部分数据命中缓存,而一部分则没有。这时候我们就需要就AVAssetResourceLoadingRequest请求拆分成本地请求与远端请求了,拆分请求需要根据已缓存的数据来进行,因此需要一个缓存配置模块来存储,已缓存视频的信息。如下图所示:

内存缓存还是磁盘缓存?

视频文件一般都比较大,如果采用内存缓存会对内存造成很大的压力,因此采用磁盘缓存,虽然磁盘缓存的读取速度要比内存慢,但对于视频来说其实可以忽略不计了。

其他

同一个url的视频缓存在同一个文件中,为了避免多线程同步的问题,因此同一时间同一个视频url不能存在多个下载请求

整体架构设计

缓存策略

既然是缓存,那么缓存策略就是必不可少的了。这里视频缓存参考了SDWebImage的缓存方案,采用了LRU缓存。主要通过过期时间与最大缓存空间两个维度来清理缓存。

iOS性能监控

Posted on 2018-07-02

iOS性能监控

背景

移动互联网的增长红利已经结束,如何运营好存量用户也变得越来越重要。而随着随着移动互联网的不断发展,用户也越来越关心应用的体验。随着业务不断快速迭代,复杂度不断增加,应用的性能问题势必会随之积累。而在传统的开发流程中,通常需要:线上用户反馈 -> 分析问题原因 -> 解决性能问题 -> 验证等4步。我们必须思考,如何快速发现并解决日渐复杂的业务导致的功能不断迭代所产生的性能问题。近两年,APM技术快速发展,国内厂商更是百花齐放。最近对APM技术做了一个调研,希望以此文章作为一个总结,以加深认识。

APM

AMP即 Application Performance Manager,定位于帮助开发团队快速精确地定位性能问题,进而推动应用的性能和用户体验的提升。那么APM关心的性能指标都有哪些呢?如下所示:

  • 界面卡顿、卡顿堆栈
  • 网络请求:成功率、状态码、流量、网络响应时间、HTTP与HTTPS的 DNS 解析、TCP握手、SSL握手(HTTP除外)、首包时间等时间
  • 交互监控:页面加载时间
  • 启动时间
  • 崩溃率、崩溃堆栈
  • Abort 率:也就是由于内存过高的等原因,被系统杀死的情况
  • 其他:内存、帧率、CPU使用率、启动时间、电量等

性能指标如何采集?数据的采集最好尽量避免侵入业务代码,业务无感知的情况下完成采集工作,所以做好的方式是面向切面编程。
有了性能日志后,还需要对数据进行分析并定位问题。
整体APM流程如下图所示:

实现原理

卡顿监控方案:

  1. FPS监控:通过监控一段连续的FPS计算丢帧率来衡量页面流畅性。

  2. 主线程监控:开辟一个子线程检测主线程RunLoop,当kCFRunLoopBeforeSources与kCFRunLoopAfterWaiting这两个状态间的耗时超过阈值时,就记为一次卡顿

  3. 综合监控:由于FPS与主线程卡顿监控容易发生抖动,业界又提出了一种综合方案,结合主线程监控、FPS监控、以及CPU等,作为卡顿的综合判断标准。

卡顿日志采集上报: 参考PLcrashreporter,构造crash.log日志上报,结合符号表文件进行符号化

网络监控

方案1:继承NSURLProtocol 拦截Http请求

方案2:方法替换,动态代理,针对NSURLSession、NSURLConnection

方案3:使用fishhook库hook CFNetwork

冷启动时间

t(App 总启动时间) = t1(main()之前的加载时间) + t2(main()之后的加载时间)。

t1 = 系统的 dylib (动态链接库)和 App 可执行文件的加载时间

下面的步骤构成了 t1 的时间线:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

t1比较难统计,目前业界的方案是在+load方法中打点统计,也就是上文时间线的Objc setUp阶段开始统计,但是这种方式是不够精确的。而且t1的大头其实是在Load dylibs阶段。目前只能通过xcode配置,在控制台打印的方式来统计。

t2 = main函数执行之后到 AppDelegate 类中的applicationDidFinishLaunching:withOptions:方法执行结束前这段时间

页面响应时间

hook ViewController的生命周期

崩溃率

Abort率

目前对于内存过高被杀死的情况是没有办法直接统计的,一般通过排除法来做百分比的统计,原理如下

  • 程序启动,设置标志位
  • 程序正常退出,清楚标志
  • 程序Crash,清楚标志
  • 程序电量过低导致关机,这个也没办法直接监控,可以加入电量检测来辅助判断
  • 第二次启动,标志位如果存在,则代表Abort一次,上传后台做统计

其他性能数据监控:FPS、CPU使用率、内存占用

谈谈ReactiveCocoa 函数响应式框架

Posted on 2018-04-20

高阶函数

提纲:

什么是纯函数(Pure Function)

副作用指的是,函数的行为受系统的全局状态所影响

为什么要有纯函数

什么是函数式编程

什么是副作用(Side Effect),副作用能做什么。

什么是函数响应式编程

什么Monad以及为什么要引入Monad

bind

return

ReactiveCocoa是如何实现响应式编程的

ReactiveCocoa bind的实现

ReactiveCocoa return的实现

ReactiveCocoa封装的一系列操作符

Functor:任何类别只要能执行fmap操作,就是一个Functor
fmap就是把一个函数应用在一个盒子里的值上,然后返回一个封装在盒子里的新值;

对于空值也就是Nothing来说,执行fmap操作后返回的还是Nothing

对于List而言,执行fmap,会将函数应用在List的每一个值都上,并返回一个新的List

总结一下:Functor是能将函数应用在值上的一种类型。换句话说,Functor就是支持将一个值映射成另一个新值的类型

那如果要将一个函数应用在另一个函数上呢?

也就是说一个函数执行了fmap操作后可以映射成一个新的函数,因此说函数也是Functor
函数的fmap 操作就是函数的复合

Functor:
将一个普通函数应用到被封装的值上

Applicative:
将一个封装的函数应用在封装的值上

Monad:
将一个“接收一个普通值并返回一个被封装的值”的函数应用在一个被封装的值上,这一任务由函数 >>=(读作“bind”)完成。

ReactiveCocoa是如何实现Monad的

RACStream被定义为一种Monad类型,定义了两个基本操作bind个return

RACStreamBindBlock可以看做是“接收一个普通值并返回一个被封装的值”函数

IO操作:

ReactiveCoCoa 内存管理

Posted on 2018-04-17

概述

ReactiveCoCoa的内存管理相当的复杂,不过这导致的最终结果是:你可以不通过持有信号来处理他们

如果ReactiveCoCoa框架要求你持有每一个信号,它使用起来将会很笨重,对于像网络请求等这些在未来使用的一次性信号来说,更是如此。你必须将长期存在的信号保存在属性中,当你完成相关操作后,还要确保将其清除,这很没意思。

订阅者(Subscribers)

在进一步讨论之前,需要注意的是:subscribeNext:error:completed:(及其变体)会在block内隐式创建订阅者。因此在block中持有的任何对象,都会被订阅者持有。就像其他任何对象一样,self如果没有直接或间接引用它,它们 将不会被保留。

有限或者短期的信号(Finite Or Short-Lived Signals)

RAC内存管理最重要的指导原则是,订阅关系会在完成或者发生错误后终结,并且订阅者会被移除。

举个栗子,假如你有如下代码在view controller里

1
2
3
self.disposable = [signal subscribeCompleted:^{
doSomethingPossiblyInvolving(self);
}];

内存管理就像这样:

1
view controller -> RACDisposable -> RACSignal -> RACSubscriber -> view controller

然而,一旦signal结束这种RACSignal -> RACSubscriber关系就会被拆除 ,从而打破了引用环。
通常,这正是你所需要的,因为RACSignal的生命周期自然会匹配事件流的逻辑生命周期。

无限信号(Infinite Signals)

无线信号(或者存活时间太久以致它们可能是无限的信号),并不会自动释放。这时候disposables就派上用场了
处理(也就是调用disposable的dispose方法)订阅关系将会移除关联的订阅者,,并将优雅地清理相关的资源。对于订阅者而言,就好像信号已经完成或者发生了错误。其他的订阅者并不受影响。
如果您必须手动管理订阅的生命周期,那么可能有更好的方法来执行您想要的操作。

来自于self的信号(Signals Derived from self)

虽然,这里仍然存在一些棘手的中间情况。任何时候信号的生命周期都与调用范围联系在一起,你将有一个更难打破的引用环。
这通常发生在使用RACObserve()关联self的key path,并需要使用block捕获self的时候。
最简单的办法是使用__weak

1
2
3
4
5
__weak id weakSelf = self;
[RACObserve(self, username) subscribeNext:^(NSString *username) {
id strongSelf = weakSelf;
[strongSelf validateUsername];
}];

或者,在引入 EXTScope.h 头文件后

1
2
3
4
5
@weakify(self);
[RACObserve(self, username) subscribeNext:^(NSString *username) {
@strongify(self);
[self validateUsername];
}];

(如果对象不支持弱引用,分别使用,unsafe_unretained或者@unsafeify替换weak或@weakify)

不过,你可以使用更好的模式。例如,上面的例子可以写成

1
[ self  rac_liftSelector:@selector(validateUsername:)withSignals:RACObserve(self,username),nil ];

或者

1
2
3
4
RACSignal * validated = [ RACObserve(self,username)map: ^(NSString * username){
//将验证逻辑放在这里。
return @ YES ;
}];

跟无限信号一样,这可以避免在信号流的block中持有self


为了有效使用ReactiveCocoa,以上就是你所需要的全部信息。然而,还有一点需要解决,只是出于技术上的好奇,或者任何对RAC有贡献的人。

“不需要持有”的设计目标引出一个问题:我们怎么知道一个信号何时被释放?如果只是创建而没有被持有呢?
答案是我们无法知道,但是我们通常可以假设调用者会在当前运行循环迭代中保留信号,如果他们想保留它的话。
所以:

  1. 创建的信号会自动添加到全局活动信号集。
  2. 该信号将等待主线程RunLoop的一次循环,然后如果它没有订户,则从活动集中移出。除非信号以某种方式保留,否则它将在此处释放
  3. 如果信号在RunLoop迭代中被订阅了,它将会被保留在信号集里
  4. 当所有订阅者都被移除了,步骤2就会被触发

如果RunLoop以递归的方式运转,这将适得其反。不过大多数或全部情况下ReactiveCocoa框架会简化使用者的代码。

ReactiveCocoa 基本操作符

Posted on 2018-04-15

最近打算好好学习下ReactiveCocoa,个人认为最好的学习方式就是看官方文档。这篇基本操作符的介绍,也是翻译自官方文档。

基本操作符

本文档解释了ReactiveCocoa中最常用的操作符,并包含了演示其用法的的示例
适用于sequences 和 signal的操作符被称为stream的操作符

执行信号产生的副作用

大多数信号都是冷信号,这意味着直到被订阅它们才会生效。
订阅后,信号或者他的订阅者可以执行,例如打印到控制台、发起网络请求、更新UI等操作。

###订阅(Subscription)

-subscribe系列方法可以访问signal的当前以及未来的值:

1
2
3
4
5
6
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// Outputs: A B C D E F G H I
[letters subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

对于冷信号来说,每一次订阅都将执行一次副作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__block unsigned subscriptions = 0;

RACSignal *loggingSignal = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
subscriptions++;
[subscriber sendCompleted];
return nil;
}];

// Outputs:
// subscription 1
[loggingSignal subscribeCompleted:^{
NSLog(@"subscriptions %lu", subscriptions);
}];

// Outputs:
// subscription 2
[loggingSignal subscribeCompleted:^{
NSLog(@"subscriptions %lu", subscriptions);
}];

这种行为可以使用过connection来改变。

###注入副作用(Injecting effect)

-do系列方法可以给signal添加副作用,而不需要订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__block unsigned subscriptions = 0;

RACSignal *loggingSignal = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
subscriptions++;
[subscriber sendCompleted];
return nil;
}];

// Does not output anything yet
[loggingSignal doCompleted:^{
NSLog(@"about to complete subscription %u", subscriptions);
}];

// Outputs:
// about to complete subscription 1
// subscription 1
[loggingSignal subscribeCompleted:^{
NSLog(@"subscriptions %lu", subscriptions);
}];

变换Streams

变换操作符可以将单个stream变换成一个新的stream

映射(Mapping)

-map:方法用于将stream中的值转换,并基于转换后的新值创建一个新的stream

1
2
3
4
5
6
7
RACSequence *letters = [@"A, B,C,D,E,F G,H, I" componentsSeparatedByString:@" "].rac_sequence;

// Contains: AA BB CC DD EE FF GG HH II
[letters map:^id(NSString * value) {
value = [value stringByAppendingString:value];
return value;
}];

过滤(Filtering)

-filter:方法使用一个block,来筛选出符合条件的值,并生成新的stream

1
2
3
4
5
6
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 2 4 6 8
RACSequence *filtered = [numbers filter:^BOOL(NSNumber * value) {
return value.intValue % 2 == 0;
}];

结合流(Combining Streams)

结合操作符可以将多个stream合并成单个stream

连接(Concatenating)

-concat:方法可以将一个stream拼接在另一个stream后面

1
2
3
4
5
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *concatenated = [letters concat:numbers];

扁平化(Flattening)

-flatten操作符用于包含stream的stream,将stream中的值也就是stream,组合成一个新的stream

序列的连接

1
2
3
4
5
6
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *sequenceOfSequences = @[letters, numbers].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *flattenedSequence = [sequenceOfSequences flatten];

信号的合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:letters];
[subscriber sendNext:numbers];
[subscriber sendCompleted];
return nil;
}];

RACSignal *flattened = [signalOfSignals flatten];

// Outputs: A 1 B C 2
[flattened subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];

映射和扁平化(Mapping and Flattening)

扁平化本身并不是很意思,但是理解其工作原理对于flattenMap操作符来说很重要

-flattenMap:操作符用于将流中的每一个值转换成一个新的流,然后再将所有返会的流扁平化成单个的流。换句话说,就是先执行-flatten操作再执行-map:操作。

-flattenMap:操作符可以用来拓展和编辑序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
RACSequence *extended = [numbers flattenMap:^RACStream *(NSNumber * value) {
return @[value, value].rac_sequence;
}];

// Contains: 1_ 3_ 5_ 7_ 9_
RACSequence*edited = [numbers flattenMap:^RACStream *(NSString *value) {
if(value.intValue %2 == 0){
return [RACSequence empty];
} else {
return [RACSequence return:[value stringByAppendingString:@"_"]];
}
}];

也可以用来创建多个自动重组的信号

1
2
3
4
5
6
7
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

[[letters flattenMap:^(NSString *letter) {
return [RACSignal return:[letter stringByAppendingString:letter]];
}] subscribeCompleted:^{
NSLog(@"All database entries saved successfully.");
}];

组合信号(Combining Signal)

连接操作符可以将多个信号组合成单个信号

序列化(Sequencing)

-then:操作符开始于一个原始信号,并等待其完成,然后只转发新信号的值。

1
2
3
4
5
6
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
RACSignal *sequenced = [[letters doNext:^(id x) {
NSLog(@"%@",x);
}] then:^RACSignal * {
return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
}];

合并(Merging)

+merge:操作符将那些从多个信号到达的值,转发到单个信号流中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *merged = [RACSignal merge:@[ letters, numbers ]];

// Outputs: A 1 B C 2
[merged subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];

合并最新值(Combining latest values)

+combineLatest: 和 +combineLatest:reduce:方法将观察多个信号的变化,当其中一个信号发生变化时,就将所有信号的最新值发送出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
combineLatest:@[ letters, numbers ]
reduce:^(NSString *letter, NSString *number) {
return [letter stringByAppendingString:number];
}];

// Outputs: B1 B2 C2 C3
[combined subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];

注意,组合信号只会在所有输入发送至少一个输入时发送其第一个值。在上面的示例中,@”A”从未转发,因为numbers尚未发送值。

切换

-switchToLatest操作符作用于“信号的信号”,并且总是转发从最新的信号的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSubject *signalOfSignals = [RACSubject subject];

RACSignal *switched = [signalOfSignals switchToLatest];

// Outputs: A B 1 D
[switched subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

[signalOfSignals sendNext:letters];
[letters sendNext:@"A"];
[letters sendNext:@"B"];

[signalOfSignals sendNext:numbers];
[letters sendNext:@"C"];
[numbers sendNext:@"1"];

[signalOfSignals sendNext:letters];
[numbers sendNext:@"2"];
[letters sendNext:@"D"];

以上就是ReactiveCocoa最常用的操作符的介绍

找一个靠谱的iOS面试题

Posted on 2018-04-07

1. 风格纠错题 知识点官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef enum {
UserSex_Man,
UserSex_Woman
} UserSex;


@interface UserModel : NSObject

@property(nonatomic, strong) NSString *name;
@property(assign, nonatomic) int age;
@property (nonatomic, assign) UserSex sex;

-(id) initUserModelWithUserName:(NSString*)name withAge:(int) age

-(void) doLogin;

考查知识点:

  • enum NS_ENUM NS_OPTIONS
  • 不可变类型数据(NSString、NSArray、NSDictionary)的修饰符的使用
  • 函数命名规范
  • 应避免使用基本类型,建议使用Foundation数据类型

###知识点:

enum枚举类型建议使用 NS_ENUM 或者 NS_OPTIONS 宏来定义

NS_ENUM和NS_OPTIONS提供了一种简明、简单的方法来定义基于C语言的枚举和选项,这些宏提高了Xcode中的代码的完成性,并明确指定了枚举和选项的类型和大小。

  • NS_ENUM和NS_OPTIONS 的区别

如果需要以按位或操作来组合的枚举应该使用NS_OPTIONS,;如枚举不需要相互组合,则可以使用NS_ENUM来定义。也就是NS_OPTIONS适合多挣枚举类型同时存在的情况,而NS_ENUM适合只同时只存在一种枚举的情况

不可变类型数据(NSString、NSArray、NSDictionary)的修饰符的使用,属性的修饰符

不可变类型数据(NSString、NSArray、NSDictionary),都有其对应的可变类型,而且是不可变类型的子类,如果使用 strong来修饰NSString,那么NSMutableString也可以赋值给该属性,如果NSMutableString赋值给NSString后,又被修改了,那么就会出现跟预想不一致的情况了,而copy修饰NSString,在setter方法中只会将NSMutableString复制一份,而不会保留,就算NSMutableString后来改变了也没有关系

函数命名规范

  1. instancetype,使用instancetype关键字作为返回类的实例的方法的返回类型,而不是使用id作为返回类型,这可以提高代码的安全性
  2. 连接第二参数不要使用with
  3. 函数命名不要使用两个动词

应避免使用基本类型,建议使用Foundation数据类型

Foundation数据在不同系统上长度是不一样的,在32位系统上int占4个字节 NSInterge也是4个字节,但是在64位系统上NSInterge占8个字节。总的来说Foundation数据兼容性会更好一下,使用Foundation数据会比使用基本类型,代码更健壮一点

####修改后答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef NS_ENUM (NSInteger, CYSex) {
CYSexMan,
CYSexWoman
};


@interface UserModel : NSObject

@property(nonatomic, copy, readonly) NSString *name;
@property(nonatomic, assign, readonly) NSUInterge age;
@property(nonatomic, assign, readonly) CYSex sex;

-(instancetype) userModelWithUserName:(NSString*)name age:(int) age

-(void) login;

2. 什么情况下使用weak关键字,相比assign有什么不同

  • weak关键字的作用:weak关键字是为了解决strong带来的循环引用的问题而生的,它不会使所引用的对象的引用计数+1,所以也就打破了引用环。weak关键字修饰的属性在其指向的对象销毁了,该属性也会被置为nil。
  • assign关键字也不会使所引用的对象引用计数+1,和weak的不同点在于,其引用的对象销毁时,assign关键字修饰的属性不会被置为nil,仍然指向了原来的地址,形成野指针。assign赋值方式:复制数据而不是复制引用

结论:weak关键字适用于可能出现循环引用的地方

3.怎么使用copy关键字?

  1. NSString、NSArray、NSDictionary等经常使用copy关键字,因为他们有对应的可变数据类型
  2. block也经常使用copy关键字,因为在方法体内block是被分配在栈上的,使用copy之后就会分配在堆上。然后就可以在作用域外访问了

4. 这个写法会出什么问题: @property (copy) NSMutableArray *array;

  1. copy修饰的话,在赋值时会变成NSArray不可变对象,那么进行增删改操作时会crash
  2. property默认为 atomic,这会影响性能。

一般情况下并不需要把属性声明为atomic,因为这并不能保证线程安全,只能保证读写的原子性。例如:一个线程连续多次读取属性值时,也有可能读到不同的值。

5.如何让自己的类用copy修饰符?如何重写带copy关键字的setter?

  1. 若想让自己的类的对象具备copy功能,则需要实现NSCopy协议,如果自定义的对象分为可变和不可变两个版本,那么还应该实现NSMutableCopy协议
  2. 重写如下:
1
2
3
-(void) setName:(NSString *)name{
_name = [name copy];
}

6.@property的本质是什么?ivar、getter、和setter是如何生成并添加到这个类中的?

@property本质上等于 实例变量 + 存取方法(getter、setter)

ivar、getter、和setter是编译器通过autosynthesis(自动合成)添加到类中的

7. protocol 和category中如何使用@property?

  1. protocol中使用@property,只会生成getter和setter方法声明,在protocol中使用property的目的是,希望实现该协议的类能实现该属性
  2. category中使用@property也是只会生成getter和setter方法声明,如果真的需要给category增加属性需要使用一下两个函数:objc_setAssociatedObject 和 objc_getAssociatedObject

8.Runtime如何实现weak属性

Runtime其实是通过一张weak表来实现weak属性的,weak表其实是一张Hash表,key为weak指向的对象的内存地址,value则是所有weak指针的组成的数组。当weak指向的对象销毁时,Runtime就会从weak表中索引到所有指向改地址的weak指针,并全部置为nil

  • 子问题:如何让不使用weak修饰的property,拥有weak的效果?
  1. 重写setter方法,使用
1
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);

这样相当于使用assign修饰了property

  1. 第二步需要在object对象销毁时,将property对应的实例变量_object置为nil;这一步的实现需要借助category以及关联对象,原理是当一个对象被销毁时,它的关联对象也会被销毁,那么只要在关联对象的的dealloc方法里执行block就可以达到将 实例变量_object置为nil的目的了

setter的实现

1
2
3
4
5
6
7
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}

category的实现

1
2
3
4
-(void) cyl_runAtDealloc:(voidBlock) block{
CYLBlockExecutor *executor = [CYLBlockExecutor initWithBlock:block];
objc_setAssociatedObject(self, "runAtDeallocBlockKey", executor, OBJC_ASSOCIATION_RETAIN);
}

CYLBlockExecutor的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id)initWithBlock:(voidBlock)aBlock
{
self = [super init];
if (self) {
_block = [aBlock copy];
}
return self;
}

- (void)dealloc
{
_block ? _block() : nil;
}

9.@property中有哪些属性关键字

  1. 原子性:atomic、nonatomic
  2. 读写权限:readwrite、readonly
  3. 内存管理语义:strong、weak、assign、copy、unsafe_unretained
  4. 方法名: getter=, setter=

10.weak属性需要在dealloc中置为nil吗?

不需要,ARC会自动处理的

11.@synthesize和@dynamic分别有什么作用?

  1. @property有两个对应的词:@synthesize和@dynamic。如果@synthesize和@dynamic都没写,那么默认的就是@synthesize var = _var;
  2. @synthesize的语义是如果你没有实现getter和setter那么编译器会自动帮你实现这两个方法
  3. @dynamic是告诉编译器:属性的getter和setter由开发者自己实现。@dynamic一般用来实现动态绑定,需要重写+ (BOOL) resolveInstanceMethod:(SEL)sel方法

12. ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?

13. 用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

14. @synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自动合成新变量么?

15. 在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?

16. objc中向一个nil对象发送消息将会发生什么?

objc在想一个对象发送消息时,runtime会根据对象的isa指针找到该对象所属的类,然后再该类的方法列表和其父类的方列表里查找响应的方法,然后再发送消息的时候,objc_msgSend不会有返回值,所谓的返回值都是在发生方法调用的时候执行的。如果向nil对象发送消息,首先就会在寻找对象的isa指针时就返回了,也就不会发生任何错误了

17. objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?

[objc foo]会在编译时,被转意为:objc_msgSend(obj, @selector(foo))

18. 什么情况下会报unrecognized selector异常

这就涉及到消息的发送流程了:

  1. objc在向一个对象发送消息时,Runtime会根据对象的isa指针找到对象所属的类,然后从方法缓存里查找对应的方法
  2. 若上一步找不到对应的方法,则会从方法列表里查找,如果还是找不到就会到父类的方法列表里查找直到NSObject类
  3. 这时如果还是找不到,就会触发Method resolution,这时你有机会提供一个函数的实现,添加到类中,如果添加了实现,这时就会重新启动一次消息发送流程,Method resolution没有提供实现的函数的话,就会触发消息转发
  4. Fast Forwarding:如果目前对象实现了-forwardingForSelector方法,Runtime就会调用该方法,让目标对象提供一个可以响应该消息的对象,只要不返回nil或者self,就会重新触发消息发送
  5. Normal Forwarding:Fast Forwarding失败了的话,就是启动Normal Forwarding。首先Runtime会发送-methodForSignaturForSelector:消息获取函数的参数和返回值,然后创建一个NSInvocation对象并发送 -forwardInvocation:消息给目标对象

如果以上流程走完,仍没有响应消息的函数,则会报unrecognized selector异常

19. 一个objc对象如何进行内存布局?(考虑有父类的情况)

  • 父类的成员变量和自己的成员变量都存放在该对象的存错空间中
  • 对象的内部有一个isa指针,指向他的类对象。类对象中存放着:

    * 每个对象的方法列表
    * 成员变量的列表
    * 属性的列表
    * 类对象的内部也有一个isa指针指向元类对象(meta class),元类对象中存放着类方法列表,类对象内部还有一个superclass指针指向父类对象
    

20.一个objc对象的isa的指针指向什么?有什么作用?

指向类对象,可以查找对象上的方法

21. 下面的代码输出什么

1
2
3
4
5
6
7
8
@implementation Son:Father
-(id) init{
self = [super init];
if(self){
NSLog(@"%@", NSStringFromClass[self class]);
NSLog(@"%@", NSStringFromClass[super class]);
}
}

考察的知识点:self与super关键字

self是类的隐藏参数,指向当前调用方法的类的实例
super并不是指向父类的指针,它和self是指向同一个接收者,不同点在于:

* [self class] 编译后 objc_msgSend(obj, @selector(class));
* [super class] 编译后 objc_msgSendSuper(obj, @selector(class));

但是最后调用的都是NSObject的这个方法

1
2
3
- (Class)class {
return object_getClass(self);
}

所以最后输出都是 Son

22. runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

23. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

不需要
它们会在NSObject -dealloc调用的object_dispose()方法中释放掉

对象的内存释放时间表:

1. 调用release:引用计数变为0
2. 子类调用 -dealloc
3. NSObject 调用-dealloc

4.调用object_dispose()
  * 为C++实例变量们(ivars)调用析构函数
  * 为了ARC状态下的 实例变量们(ivars)调用releas
  * 释放所有使用runtime associate 方法关联的对象
  * 释放所有__weak引用
  * 调用free

24. objc中的类方法和实例方法有什么本质区别和联系?

25. _objc_msgForward函数是做什么的?直接调用他会发生什么?

_objc_msgForward是用来实现消息转发的:向一个对象发送消息,但它并没有对应的实现的时候,_objc_msgForward会尝试消息转发

直接调用_objc_msgForward是非常危险的事,直接调用_objc_msgForward,会跳过查找IMP的过程,直接出发消息转发,如果用不好会直接导致程序Crash。但是如果用得好,能做很多非常酷的事,比如热修复。

26. runtime如何实现weak变量的自动置nil?

同第8题 weak的实现

27能否向编译后得到的类增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

  • 不能向编译后的类增加实例变量
  • 可以向运行时创建的类添加实例变量

解释:
编译后的类,类结构体中的objc_ivar_list 实例变量链表和实例变量的内存大小 instance_size都已经确定了,所以不能向编译后的类增加实例变量

运行时创建的类可以增加实力变量。不过得在调用objc_allocateClassPair之后,objc_registerClassPair之前。

增加方法则没有限制

28 RunLoop与线程的关系

RunLoop与线程的紧密相连的,每个线程都有一个对应的RunLoop。
不过只有主线程的RunLoop默认是开启的

其他线程的RunLoop默认不开启 必要时得手动获取:

1
NSRunLoop*runloop = [NSRunLoop currentRunLoop];

29.RunLoop的mode的作用是什么?

mode是用来处理不同分组的Source/Timer/Obsever事件的,系统提供的mode有5种:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认。空闲状态

  • UITrackingRunLoopMode:ScrollView的滑动时

  • GSEventReceiveRunLoopMode

  • UIInitializationRunLoopMode:启动时
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

系统公开提供的Mode有两个:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认。空闲状态
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

RunLoop只能运行在一种Mode下,如果要切换Mode,需要退出RunLoop再重新启动,当ScrollView滑动时,主线程RunLoop会由NSDefaultRunLoopMode切换为UITrackingRunLoopMode,而以+ scheduledTimerWithTimeInterval…的方式触发的timer,实质上是注册到NSDefaultRunLoopMode的,所以这时候timer会被暂停回调。要想不被暂停,可以将timer注册到NSRunLoopCommonModes下,因为NSDefaultRunLoopMode和UITrackingRunLoopMode都在NSRunLoopCommonModes mode集合中

30. 以+ scheduledTimerWithTimeInterval…的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

同上一题

31. 猜想runloop内部是如何实现的?

do-while 循环

  1. 通知Observer即将进入RunLoop
  2. 通知Observer即将处理Timer
  3. 通知Observer即将处理Source0
  4. 处理Source0
  5. 如果有Source1跳到9
  6. 通知Observer线程即将休眠
  7. 休眠等待被唤醒
  8. 通知Observer线程刚被唤醒
  9. 处理唤醒时受到的消息,跳回2
  10. 通知Observer即将退出RunLoop

苹果在主线程注册了两个Observer:

  • 第一个Observer监听即将进入RunLoop事件:创建自动释放池AutoreleasePool
  • 第二个Observer监听两个事件:
    • 即将进入休眠 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新的池;
    • 退出RunLoop调用 _objc_autoreleasePoolPush() 释放自动释放池

      32.objc使用什么机制管理对象内存?

      引用计数机制来管理对象内存

33. ARC通过什么方式帮助开发者管理内存?

编译期自动插入retain/release/autorelease 运行期

UIViewController、UINavigationController以及UITabBarController

Posted on 2018-04-01

UIViewController

  • UIViewController 是管理View的一般管理器的基类,它提供了一些View出现或者消失时的回调方法,也就是生命周期。

  • 子类可以通过重载 loadView方法来创建一个自定义的view层级结构

UINavigationController

概述

UINavigationController 用来管理UIViewController,在多UIViewController中常用。它以栈的形式管理UIViewController。

一般情况下,UINavigationController至少有一个视图控制器,即最少拥有一个根视图控制器。如果不给它添加根视图控制器,界面上也有视图,因为UINavigationController继承自UIViewController,不过此时.view.backgroundColor为nil,即透明

常用函数

  • push方法将某个视图控制器压入栈
1
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated;
  • 使用setViewControllers一次压入多个控制器vc1->vc2->vc3,会显示最后的控制器vc3(处于栈顶)
  • 使用pop方法可以移除栈顶控制器
  • popToRootViewControllerAnimated回到根控制器
  • 获取被管理的控制器
1
2
3
4
5
6
7
8
/// 当前管理的所有的控制器
@property(nonatomic,copy) NSArray<__kindof UIViewController *> *viewControllers;

/// 栈顶控制器
@property(nullable, nonatomic,readonly,strong) UIViewController *topViewController;

/// 当前可见的VC,可能是topViewController,也可能是当前topViewController present(modal)出来的VC,总而言之就是可见的VC
@property(nullable, nonatomic,readonly,strong) UIViewController *visibleViewController;

导航条

UINavigationController是用来导航的,具体的大部分操作都有导航条来完成。导航条的内容有UIViewController的navigationItem属性决定

UIToolBar 底部工具条

UINavigationController自带了一个工具栏,通过[self.navigationController setToolbarHidden:NO];来显示工具栏,工具栏中的内容可以通过viewController的toolbarItems来设置,显示的顺序和设置的NSArray中存放的顺序一致,每一个UIBarButtonItem对象都可以设定点击事件,可以使用系统提供的很多常用风格的对象,也可以根据需求进行自定义

UINavigationBar、UINavigationItem、UIToolbar与UIBarButtonItem四者关系

  • NavigaitonBar是导航栏,位于屏幕的上方,管理整个NavigationController的navigationItem,它类似navigationcontroller一样提供了一个栈来管理UINavigationItem,在编程时,一般只设置每个控制器的navigationItem属性
  • 一个导航控制器管理多个视图控制器(多个视图控制器共享一个导航控制器),而一个导航控制器只有一个UINavigationBar,被管理的多个视图控制器共享这一个UINavigationBar,只要一个视图控制器改变了UINavigationBar的属性则影响是全局的。每个视图控制器都会有属于自己的UINavigationItem,系统会以懒加载的方式创建一个UINavigationItem显示在UINavigationBar中,改变UINavigationItem只会在当前控制器起作用,不会影响其它控制器。
  • Toolbar显示在屏幕底部,是导航控制器的工具栏,一个导航控制器只有一个,在任何被管理的视图控制器地方改变则会都改变。可以一次性添加多个UIBarButtonItem或按钮(包装成UIBarButtonItem后添加),有一个items数组属性。
  • UIBarButtonItem是UINavigationItem或者Toolbar具体的一个按钮。

UITabBarController 标签控制器

标签控制器,管理一个选择展示界面,可以控制多个controller,无层级关系,标签栏中的每一个标签关联一个自定义的controller,选择标签切换显示相应的controller根视图

UITabBar

标签栏,每个UITabBarController只有一个标签栏

1
2
3
4
5
6
7
8
9
@interface UITabBar : UIView

@property(nullable, nonatomic, weak) id<UITabBarDelegate> delegate; // weak reference. default is nil

//UITabBarController中每个controller对应一个UITabBarItem
@property(nullable, nonatomic, copy) NSArray<UITabBarItem *> *items; // get/set visible UITabBarItems. default is nil. changes not animated. shown in order
@property(nullable, nonatomic, weak) UITabBarItem *selectedItem; // will show feedback based on mode. default is nil

@end

模态视图 modal

模态视图的作用

  1. 收集用户输入信息
  2. 临时呈现一些内容
  3. 临时改变工作模式
  4. 相应设备方向变化(用于针对不同方向分别是想两个ViewController的情况)
  5. 显示一个新的view层级

这几种情形都会暂时中断程序正常的执行流程,主要作用是收集或者显示一些信息。

12

Mochangxing

15 posts
6 tags
© 2018 Mochangxing
Powered by Hexo v3.7.1
|
Theme — NexT.Muse v6.1.0