0%

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 开源的一个用于保持 iOS 界面流畅的库。在我博客其它章节也提到过。
ASDK 的作者是 Scott Goodson (Linkedin),
他曾经在苹果工作,负责 iOS 的一些内置应用的开发,比如股票、计算器、地图、钟表、设置、Safari 等,当然他也参与了 UIKit framework 的开发。后来他加入 Facebook 后,负责 Paper 的开发,创建并开源了 AsyncDisplayKit。目前他在 Pinterest 和 Instagram 负责 iOS 开发和用户体验的提升等工作。

ASDK 自 2014 年 6 月开源,10 月发布 1.0 版。目前 ASDK 即将要发布 2.0 版。
V2.0 增加了更多布局相关的代码,ComponentKit 团队为此贡献很多。
现在 Github 的 master 分支上的版本是 V1.9.1,已经包含了 V2.0 的全部内容。

官方教程

ASDK的基本原理

ASDK 认为,阻塞主线程的任务,主要分为上面这三大类。文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但 UIKit 和 Core Animation 相关操作必需在主线程进行。ASDK 的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。

为了达成这一目标,ASDK 尝试对 UIKit 组件进行封装:

这是常见的 UIView 和 CALayer 的关系:View 持有 Layer 用于显示,View 中大部分显示属性实际是从 Layer 映射而来;Layer 的 delegate 在这里是 View,当其属性改变、动画产生时,View 能够得到通知。UIView 和 CALayer 不是线程安全的,并且只能在主线程创建、访问和销毁。

ASDK 为此创建了 ASDisplayNode 类,包装了常见的视图属性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等),然后它用 UIView->CALayer 相同的方式,实现了 ASNode->UIView 这样一个关系。

当不需要响应触摸事件时,ASDisplayNode 可以被设置为 layer backed,即 ASDisplayNode 充当了原来 UIView 的功能,节省了更多资源。

与 UIView 和 CALayer 不同,ASDisplayNode 是线程安全的,它可以在后台线程创建和修改。Node 刚创建时,并不会在内部新建 UIView 和 CALayer,直到第一次在主线程访问 view 或 layer 属性时,它才会在内部生成对应的对象。当它的属性(比如frame/transform)改变后,它并不会立刻同步到其持有的 view 或 layer 去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过某个机制一次性设置到内部的 view 或 layer。

通过模拟和封装 UIView/CALayer,开发者可以把代码中的 UIView 替换为 ASNode,很大的降低了开发和学习成本,同时能获得 ASDK 底层大量的性能优化。为了方便使用, ASDK 把大量常用控件都封装成了 ASNode 的子类,比如 Button、Control、Cell、Image、ImageView、Text、TableView、CollectionView 等。利用这些控件,开发者可以尽量避免直接使用 UIKit 相关控件,以获得更完整的性能提升。

ASDK 的图层预合成

有时一个 layer 会包含很多 sub-layer,而这些 sub-layer 并不需要响应触摸事件,也不需要进行动画和位置调整。ASDK 为此实现了一个被称为 pre-composing 的技术,可以把这些 sub-layer 合成渲染为一张图片。开发时,ASNode 已经替代了 UIView 和 CALayer;直接使用各种 Node 控件并设置为 layer backed 后,ASNode 甚至可以通过预合成来避免创建内部的 UIView 和 CALayer。

通过这种方式,把一个大的层级,通过一个大的绘制方法绘制到一张图上,性能会获得很大提升。CPU 避免了创建 UIKit 对象的资源消耗,GPU 避免了多张 texture 合成和渲染的消耗,更少的 bitmap 也意味着更少的内存占用。

ASDK 异步并发操作

自 iPhone 4S 起,iDevice 已经都是双核 CPU 了,现在的 iPad 甚至已经更新到 3 核了。充分利用多核的优势、并发执行任务对保持界面流畅有很大作用。ASDK 把布局计算、文本排版、图片/文本/图形渲染等操作都封装成较小的任务,并利用 GCD 异步并发执行。如果开发者使用了 ASNode 相关的控件,那么这些并发操作会自动在后台进行,无需进行过多配置。

Runloop 任务分发

Runloop work distribution 是 ASDK 比较核心的一个技术,ASDK 的介绍视频和文档中都没有详细展开介绍,所以这里我会多做一些分析。如果你对 Runloop 还不太了解,可以看一下我之前的文章 RunLoop,里面对 ASDK 也有所提及。

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。具体代码见这个文件:ASAsyncTransactionGroup

通过这种机制,ASDK 可以在合适的机会把异步、并发的操作同步到主线程去,并且能获得不错的性能。

其他

ASDK 中还有封装很多高级的功能,比如滑动列表的预加载、V2.0添加的新的布局模式等。ASDK 是一个很庞大的库,它本身并不推荐你把整个 App 全部都改为 ASDK 驱动,把最需要提升交互性能的地方用 ASDK 进行优化就足够了。

不够尽兴的朋友请移步博客

SDWebImage介绍

  • 提供 UIImageView, UIButton, MKAnnotationView 的分类,用来加载网络图片,并进行缓存管理;

  • 异步方式来下载网络图片

  • 异步方式: memory (内存)+ disk (磁盘) 来缓存网络图片,自动管理缓存;

  • 后台图片解码,转换及压缩;

    • 空间换时间https://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/
  • 同一个 URL 不会重复下载;

  • 失效的 URL 不会被无限重试;

  • 支持 GIF动画 及 WebP 格式;

  • 开启 子线程 进行耗时操作,不阻塞主线程;

下载完图片的储存发生在SDWebImageManager里面(下载operation也是这里面创建的),可以作为提前缓存图片的方式

SDWebImageContext

SDWebImageContextSetImageOperationKey

  1. 简单来说,这个key对应的value用于指定当前的id保存在NSMapTable中的key,后续的cancel、remove都需要通过这个key来找到对应的线程。
  2. 当指定同一个key加载图片时会先cancel之前存在的线程。
  3. SDWebImageContextSetImageOperationKey并不是在所有地方使用都生效的。

比如UIButton的sd_setImageWithURL:和sd_setBackgroundImageWithURL:系列方法。

在其内部需要通过这个这值来保存、区分不同状态(UIControlState)的图片加载线程,所以即使设置了SDWebImageContextSetImageOperationKey也会被覆盖:

SDWebImageContextCustomManager

可以传入一个自定义的SDWebImageManager,默认使用[SDWebImageManager sharedManager]

SDWebImageContextImageTransformer

可以传入一个id类型用于转换处理加载出来的图片。

  1. 在SDWebImageManager中也可以设置一个id默认为nil,但是只有SDWebImageContext没有配置SDWebImageContextImageTransformer,才会使用它。
    也就是配置优先级 SDWebImageContext>SDWebImageManager
  2. 如果设置了id不会缓存原始图片,只缓存处理后的图片。
  3. 对于同个图片、不同参数的id会被认为是不同的图片:会产生不同的缓存文件、会重复下载。

SDWebImageContextImageScaleFactor

在NSData -> UIImage时对图片放大比例,是个大于1的CGFloat值,默认值:

SDWebImageContextStoreCacheType

定义图片缓存规则具体看 SDImageCacheType中的定义。

SDWebImageContextDownloadRequestModifier

可以传入一个id,用于在加载图片前修改NSURLRequest。

  1. SDWebImageContextDownloadRequestModifier协议比较简单,只需要实现一个方法,返回一个修改后的NSURLRequest即可:
  2. 内建了一个SDWebImageDownloaderRequestModifier对象,可以使用Block方便的修改NSURLRequest

SDWebImageContextCacheKeyFilter

可以传入一个id,指定图片的缓存key。

  1. SDWebImageCacheKeyFilter协议也比较简单,只需要实现一个方法,返回一个对应的缓存key字符串即可
  • (nullable NSString *)cacheKeyForURL:(nonnull NSURL *)url;
  1. 内建了一个SDWebImageCacheKeyFilter对象,可以使用Block方便的返回缓存key

SDWebImageContextCacheSerializer

  1. 可以传入一个id,转换需要缓存的图片格式。
  2. 在SDWebImageManager中也可以设置一个id默认为nil,但是只有SDWebImageContext没有配置SDWebImageContextCacheSerializer,才会使用它。
    也就是配置优先级 SDWebImageContext>SDWebImageManager
  3. 通常用于需要缓存的图片格式与下载的图片格式不相符的时候,如:下载的时候为了节约流量、减少下载时间使用了WebP格式,但是如果缓存也用WebP,每次从缓存中取图片都需要经过一次解压缩,这样是比较影响性能的,就可以使用id,实现其中的协议方法:

SDWebImageContextLoaderCachedImage

可以传入一个UIImage的缓存图像。

  1. 这个值比较特殊,它是定义在SDImageLoader.m中的。
  2. 这个值可以认为是SDWebImage是内部用来从SDWebImageManager向SDWebImageDownloader(id)传递缓存图像的,自定义实现SDImageLoader协议可能会用到这个值,其他情况一般不会用到。
  3. 这个属性只有在 SDWebImageOptions包含SDWebImageRefreshCached策略时才生效,也就是他是SDWebImageRefreshCached这个策略的配套值。
  4. SDWebImageRefreshCached这个策略用于那些图片URL是静态的(图片更新时URL是不变的,SD给的例子是 Facebook graph api profile pics),这个时候它会根据HTTP header的 cache-control 字段来控制缓存并且使用NSURLCache来缓存图片,SDWebImageDownloader(id)中判断SDWebImageContextLoaderCachedImage存在并且策略是SDWebImageRefreshCached的情况,仍然会发起请求。

SDWebImageDownloader

  • self.URLOperations 包含所有的下载队列

这句话很重要 url和callback关联dic
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.

downloadOperationCancelToken 返回绑定当前operation的callback、progressBlock, 是一个可变字典

operation.loaderOperation = SDWebImageDownloadToken

  • addHandlersForProgress

其实是向 SDWebImageDownloaderOperation 里面的callbackBlocks注册回调callback

SDWebImageDownloadToken 其实包含

  • url
  • request
  • downloadOperationCancelToken (这是一个dic,又包含)
    • callback
    • progressBlock

cancel的时候

其中一个队列SDWebImageDownloaderOperation,包含所有url对应的回调

  • self.callbackBlocks

options参数:

SDWebImageOptions属性 说明
SDWebImageRetryFailed 默认情况下,如果一个url在下载的时候失败了,那么这个url会被加入黑名单并且library不会尝试再次下载,这个flag会阻止library把失败的url加入黑名单
SDWebImageLowPriorit 默认情况下,图片会在交互发生的时候下载(例如你滑动tableview的时候),这个flag会禁止这个特性,导致的结果就是在scrollview减速的时候,才会开始下载
SDWebImageProgressiveLoad 这个flag启动渐进式下载图像,类似浏览器加载图像那样逐步显示,默认情况下,图像仅是在下载完成后显示
SDWebImageRefreshCached 一个图片缓存了,还是会重新请求.并且缓存侧略依据NSURLCache而不是SDWebImage。即在URL没变但是服务器图片发生更新时使用
SDWebImageContinueInBackground 启动后台下载,实现原理是通过向系统询问后台的额外时间来完成请求的。 如果后台任务到期,则操作将被取消。
SDWebImageHandleCookies 当设置了NSMutableURLRequest.HTTPShouldHandleCookies = YES时,可以控制存储NSHTTPCookieStorage中的cookie
SDWebImageAllowInvalidSSLCertificates 允许不安全的SSL证书,用于测试环境,在正式环境中谨慎使用
SDWebImageHighPriority 默认情况下,image在装载的时候是按照他们在队列中的顺序装载的(就是先进先出)。这个flag会把他们移动到队列的前端,并且立刻装载,而不是等到当前队列装载的时候再装载。
SDWebImageDelayPlaceholder 默认情况下,占位图会在图片下载的时候显示.这个flag开启会延迟占位图显示的时间,等到图片下载完成之后才会显示占位图.
SDWebImageTransformAnimatedImage 通常不会在可动画的图像上调用 transformDownloadedImage 代理方法,因为大多数转换代码会破坏动画文件,这个flag为尝试转换
SDWebImageAvoidAutoSetImage 图片在下载后被加载到imageView。但是在一些情况下,我们想要设置一下图片(引用一个滤镜或者加入透入动画)这个flag来手动的设置图片在下载图片成功后
SDWebImageScaleDownLargeImages 默认情况下,图像将根据其原始大小进行解码。 在iOS上,此flat会将图片缩小到与设备的受限内存兼容的大小。 但如果设置了SDWebImageAvoidDecodeImage则此flat不起作用。 如果设置了SDWebImageProgressiveLoad它将被忽略。
SDWebImageQueryMemoryData 默认情况下,当图像已缓存在内存中时,我们不会查询图像数据。 此flat则强制查询图像数据。 此查询是异步的,除非指定SDWebImageQueryMemoryDataSync
SDWebImageQueryMemoryDataSync 结合SDWebImageQueryMemoryData设置同步查询图像数据(一般不建议这么使用,除非是在同一个runloop里避免单元格复用时发生闪现)
SDWebImageQueryDiskDataSync 如果内存查询没有的时候,强制同步磁盘查询(这三个查询可以组合使用,一般不建议这么使用,除非是在同一个runloop里避免单元格复用时发生闪现)
SDWebImageFromCacheOnly 默认情况下,当缓存丢失时,SD将从网络下载图像。 此flat可以防止这样,使其仅从缓存加载。
SDWebImageFromLoaderOnly 默认情况下,SD在下载之前先从缓存中查找,此flat可以防止这样,使其仅从网络下载
SDWebImageForceTransition 默认情况下,SD在图像加载完成后使用SDWebImageTransition进行某些视图转换,此转换仅适用于从网络下载图像。 此flat可以强制为内存和磁盘缓存应用视图转换。
SDWebImageAvoidDecodeImage 默认情况下,SD在查询缓存和从网络下载时会在后台解码图像,这有助于提高性能,因为在屏幕上渲染图像时,需要首先对其进行解码。这发生在Core Animation的主队列中。然而此过程也可能会增加内存使用量。 如果由于过多的内存消耗而遇到问题,可以用此flat禁止解码图像。
SDWebImageDecodeFirstFrameOnly 默认情况下,SD会解码动画图像,该flat强制只解码第一帧并生成静态图。
SDWebImagePreloadAllFrames 默认情况下,对于SDAnimatedImage,SD会在渲染过程中解码动画图像帧以减少内存使用量。 但是用户可以指定将所有帧预加载到内存中,以便在大量imageView共享动画图像时降低CPU使用率。这实际上会在后台队列中触发preloadAllAnimatedImageFrames(仅限磁盘缓存和下载)。

SDImageTransformer的类型

SDImageCache

NSCache

  • 自动删除机制:当系统内存紧张时,NSCache会自动删除一些缓存对象
  • 线程安全:从不同线程中对同一个 NSCache 对象进行增删改查时,不需要加锁
  • 不同于 NSMutableDictionary,NSCache存储对象时不会对 key 进行 copy 操作

小Tip

  1. 运行时存取关联对象:
  2. 数组的写操作需要加锁(多线程访问,避免覆写)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //给self.runningOperations加锁
    //self.runningOperations数组的添加操作
    @synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
    }

    //self.runningOperations数组的删除操作
    - (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    @synchronized (self.runningOperations) {
    if (operation) {
    [self.runningOperations removeObject:operation];
    }
    }
    }
  3. 确保在主线程的宏:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    dispatch_main_async_safe(^{
    //将下面这段代码放在主线程中
    [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
    });

    //宏定义:
    #define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
    block();\
    } else {\
    dispatch_async(dispatch_get_main_queue(), block);\
    }
    #endif
  4. 设置不能为nil的参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    - (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
    _imageCache = cache;
    _imageDownloader = downloader;
    _failedURLs = [NSMutableSet new];
    _runningOperations = [NSMutableArray new];
    }
    return self;
    }
  5. 容错,强制转换类型
    1
    2
    3
    if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
    }

UIImageView分类方法,分流到了UIView的分类方法,注释如下

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
使用图片的 URL 和可选的 placeholder image 设置 imageView 的 image

下载是异步和缓存的

@param url image的URL
@param placeholder 初始图像,直至image数据请求完成
@param options image下载的时候的选择项-SDWebImageOptions,下方有详细介绍
@param context 为了补充options枚举没有的选择想,下方有详细介绍
@param setImageBlock 用于自定义设置image
@param progressBlock 在图片下载ing状态调用,注:在后台队列上执行
@param completedBlock 在操作完成后调用,返回类型为
completedBlock(image, data, error, cacheType, finished, url)

*/

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
//将可变对象copy为不可变的
context = [context copy]; // copy to avoid mutable object

//取出context设置中的operationKey
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
//如果operationKey为nil,就采用他自身的类的字符串作为key
if (!validOperationKey) {
validOperationKey = NSStringFromClass([self class]);
}
//取消之前绑定的operation,保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
//为自身绑定一个URL
self.sd_imageURL = url;
//如果不是延迟显示placeholder的情况
if (!(options & SDWebImageDelayPlaceholder)) {
//dispatch_main_async_safe 异步线程安全,下面有源码介绍
dispatch_main_async_safe(^{
//在图片下载下来之前,添加临时的占位图
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}

if (url) {
// reset the progress
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;

#if SD_UIKIT || SD_MAC
// check and start image indicator
// 检查是否设置了image indicator,如果有则启动,下面有方法的源码
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
// 判断context中是否自定义了manager,如果没有则使用默认的
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}

// 设置image加载进度Block(已接收size,预计总size,image的URL)
__weak __typeof(self)wself = self;
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
// 使用__strong __typeof是防止self在这个执行过程中释放,下方有详细介绍
__strong __typeof (wself) sself = wself;
NSProgress *imageProgress = sself.sd_imageProgress;
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
#if SD_UIKIT || SD_MAC
//加载指示器是否实现了updateIndicatorProgress方法
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = imageProgress.fractionCompleted;
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif //返回image加载进度Block
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
//下载图片
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
if (!sself) { return; }

// 如果progress没有更新,则标记其为完成状态
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
//const int64_t SDWebImageProgressUnitCountUnknown = 1LL; (LL是 long long 类型的缩写)
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}

#if SD_UIKIT || SD_MAC
// 检查并停止 image indicator
if (finished) {
[self sd_stopImageIndicator];
}
#endif
// 下载完成后是否自动加载图片 (options & SDWebImageAvoidAutoSetImage)根据枚举名取枚举中的值,get了
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
// 设置completedBlock
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, data, error, cacheType, finished, url);
}
};

// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}

UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}

#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
// 检查image的过渡动画效果
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
#endif
// 设置image的过渡动画效果
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
callCompletedBlockClojure();
});
}];
//在操作缓存字典(operationDictionary)里添加operation,表示当前的操作正在进行,源码见下
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
// 没有url,停止Image Indicator
#if SD_UIKIT || SD_MAC
[self sd_stopImageIndicator];
#endif
dispatch_main_async_safe(^{
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
}
});
}
}

main()调用之前的加载过程

App开始启动后,系统首先加载可执行文件(自身App的所有.o文件的集合),然后加载动态链接库dyld,dyld是一个专门用来加载动态链接库的库。 执行从dyld开始,dyld从可执行文件的依赖开始, 递归加载所有的依赖动态链接库。
动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。
其实无论对于系统的动态链接库还是对于App本身的可执行文件而言,他们都算是image(镜像),而每个App都是以image(镜像)为单位进行加载的,那么image究竟包括哪些呢?

系统使用动态链接有几点好处:

代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份。 易于维护:由于被依赖的 lib 是程序执行时才链接的,所以这些 lib 很容易做更新,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升级直接换成libSystem.C.dylib 然后再替换替身就行了。 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多。

如上图所示,不同进程之间共用系统dylib的_TEXT区,但是各自维护对应的_DATA区。
所有动态链接库和我们App中的静态库.a和所有类文件编译后的.o文件最终都是由dyld(the dynamic link editor),Apple的动态链接器来加载到内存中。每个image都是由一个叫做ImageLoader的类来负责加载(一一对应),那么ImageLoader又是什么呢?

什么是ImageLoader

image 表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。
两步走: 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)。 再从可执行文件 image 递归加载所有符号。
当然所有这些都发生在我们真正的main函数执行前。

动态链接库加载的具体流程

动态链接库的加载步骤具体分为5步:

  • load dylibs image 读取库镜像文件

  • Rebase image

  • Bind image

  • Objc setup

  • initializers

load dylibs image

在每个动态库的加载过程中, dyld需要:

  • 分析所依赖的动态库
  • 找到动态库的mach-o文件
  • 打开文件
  • 验证文件
  • 在系统核心注册文件签名
  • 对动态库的每一个segment调用mmap()

通常的,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载。 针对这一步骤的优化有:

  • 减少非系统库的依赖
  • 合并非系统库
  • 使用静态资源,比如把代码加入主程序

rebase/bind

由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。 rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。
通过命令行可以查看相关的资源指针:

xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp

优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:

  • 减少Objc类数量, 减少selector数量

  • 减少C++虚函数数量

  • 转而使用swift stuct(其实本质上就是为了减少符号的数量)

Objc setup

这一步主要工作是:

  • 注册Objc类 (class registration)
  • 把category的定义插入方法列表 (category registration)
  • 保证每一个selector唯一 (selctor uniquing)

由于之前2步骤的优化,这一步实际上没有什么可做的。

initializers

以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有:

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

Objc的load函数和C++的静态构造函数采用由底向上的方式执行,来保证每个执行的方法,都可以找到所依赖的动态库。

上图是在自定义的类XXViewController的+load方法断点的调用堆栈,清楚的看到整个调用栈和顺序:

  • dyld 开始将程序二进制文件初始化
  • 交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
  • 由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
  • runtime 接手后调用 mapimages 做解析和处理,接下来 loadimages 中调用 callloadmethods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法

至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)。
整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存, 动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。
如果程序刚刚被运行过,那么程序的代码会被dyld缓存,因此即使杀掉进程再次重启加载时间也会相对快一点,如果长时间没有启动或者当前dyld的缓存已经被其他应用占据,那么这次启动所花费的时间就要长一点

main()之前的加载时间如何衡量

那么问题就来了,那怎么衡量main()之前也就是time1的耗时呢,苹果官方提供了一种方法,那就是在真机调试的时候勾选DYLD_PRINT_STATISTICS选项。

会得到如下形式的输出:

1
2
3
4
5
6
7
8
9
10
Total pre-main time:  42.89 milliseconds (100.0%)
dylib loading time: 38.75 milliseconds (90.3%)
rebase/binding time: 411015771.6 seconds (80806899.0%)
ObjC setup time: 3.69 milliseconds (8.6%)
initializer time: 17.68 milliseconds (41.2%)
slowest intializers :
libSystem.B.dylib : 3.07 milliseconds (7.1%)
libBacktraceRecording.dylib : 2.43 milliseconds (5.6%)
libMainThreadChecker.dylib : 9.07 milliseconds (21.1%)
AppProject : 1.56 milliseconds (3.6%)

由此可见,最多的用时还是在image加载和OC类的初始化,共占用总时长的79.3%,精简framework的引入和OC类有优化的空间。
总结一下:对于main()调用之前的耗时我们可以优化的点有:

  • 减少不必要的framework,因为动态链接比较耗时

  • check framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查

  • 合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类如下:

  • 删减一些无用的静态变量

  • 删减没有被调用到或者已经废弃的方法

    1
    2
        方法见:http://stackoverflow.com/questions/35233564/how-to-find-unused-code-in-xcode-7
    https://developer.Apple.com/library/ios/documentation/ToolsLanguages/Conceptual/Xcode_Overview/CheckingCodeCoverage.html
  • 将不必须在+load方法中做的事情延迟到+initialize中

  • 尽量不要用C++虚函数(创建虚函数表有开销)

main()调用之后的加载时间

在main()被调用之后,App的主要工作就是初始化必要的服务,显示首页内容等。而我们的优化也是围绕如何能够快速展现首页来开展。 App通常在AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中创建首页需要展示的view,然后在当前runloop的末尾,主动调用CA::Transaction::commit完成视图的渲染。
而视图的渲染主要涉及三个阶段:

  • 准备阶段 这里主要是图片的解码
  • 布局阶段 首页所有UIView的- (void)layoutSubViews()运行
  • 绘制阶段 首页所有UIView的- (void)drawRect:(CGRect)rect运行
    再加上启动之后必要服务的启动、必要数据的创建和读取,这些就是我们可以尝试优化的地方

因此,对于main()函数调用之前我们可以优化的点有:

  • 不使用xib,直接视用代码加载首页视图
  • NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,这个影响需要评估,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)
  • 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
  • 梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求

如何找到拖慢启动应用时长的瓶颈

为了找到瓶颈,我们在启动之后的didFinishLauhcning方法开始执行到首页列表页的NewsListViewController的viewDidAppear方法,几乎每个可能比较耗时的流程进行拆分和统计,得到统计数据之后发现: 主要耗时在首页UI构造和渲染(storyboard加载,tabBar/topBar渲染,开屏广告加载/cell注册/日志模块初始化这几个步骤)。

具体优化点

因此,可以优化的点如下:

  • 纯代码方式而不是storyboard加载首页UI。
  • 对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
    对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载
  • 对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
  • 上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。

官方文档,Understanding and Analyzing Application Crash Reports

前言

当app发生crash时,系统会生成crash report并存储在设备上.

  • Crash Report, 尤其是堆栈信息,在被符号化之前是不可读的。所谓符号化就是把内存地址用可读的函数名和行数来替换。 注意使用 .crash文件
  • Low Memory Report 它没有堆栈信息

符号化

编译代码的时候,通过build setting里的Debug Information Format(DEBUG_INFORMATION_FORMAT)
临时介绍一下:

  • 关于DWARF与dSYM
    • DWARF与dSYM的关系是,DWARF是文件格式,而dSYM往往指一个单独的文件。在Xcode中如果不做特殊制定,debug information是被保存在executable文件中,可以使用dsymutil从executable中提取dSYM文件。
  • dsymutil
    • dsymutil is a tool to manipulate archived DWARF debug symbol files. 使用dsymutil可以对dSYM文件进行如下操作:从exe_path中提取成dSYM文件、将executable或者object文件中的symbol table dump出来、更新dSYM文件以让dSYM文件包含最新的accelerator tables and other DWARF optimizations。
  • Debug Info Format
    • 在Xcode中可以选择DWARF和DWARF with dSYM file,推荐的设置是Debug用DWARF;Release使用DWARF with dSYM file。
  • 使用dSYM file
    • 如果我们有若干的build,有若干dSYM文件,而名字又有点乱,想知道哪个dSYM跟哪个build匹配,从而可以使用它们呢?办法就是查看UUID。
  1. 一般来说,debug模式构建的app会把Debug符号表存储在编译好的binary信息中,而release模式构建的app会把debug符号表存储在dSYM文件中以节省体积。

同一次构建,app+dSYM+UUID是一套的。如果这几个文件不属于同一次构建,即便是相同的源代码,互相之间在符号化这个事情上也无法互相工作。

  1. 你为了分发app而选择Archive(存档)时,Xcode会把app的二进制信息和.dYSM文件存储在你的home文件夹下的某个地方。你可以在Xcode的Organizer里面通过”Archived”选项找到所有你存档过的app。

注意:想要解析来自于测试、app review或者客户的crash report,你需要保留分发出去的那些构建过的archive文件。

  1. 如果你是通过App Store分发app或者是Test Flight分发的beta版本的app,你将在上传archive到ITC(iTunes Connect)时看见一个“是否将dSYM一起上传”的选项。在上传对话框中,请勾选”在app中包含app符号表”。上传你的dYSM文件对于从TestFlight用户和客户以及愿意分享诊断信息的客户那边接收crash report是很有必要的。

    注意:接收自App Review的crash report是不会被符号化的,及时你再上传你的app到ITC时勾选了包含dSYM文件。任何来自于App Review的crash report都需要在Xcode里做符号化。

  2. 当你的app 发生crash时,一个没有被符号化的crash report会被创建并存储在设备上。

  3. 用户可以通过调试已部署的iOS APP里提到的方法来直接从他们的设备里获得crash report。如果你通过AdHoc或者企业证书分发app,这是你唯一能从用户获取crash report的方法。

  4. 从设备上直接获取的crash report是没有被符号化的,你需要通过Xcode来符号化。Xcode会结合dSYM文件和你app的二进制信息把堆栈里的每一个地址对应到源代码中。处理后的结果就是一个符号化过的crash report。

  5. 如果用户愿意和Apple共享诊断信息,或者用户通过TestFlight下载了你的beta版本app,那crash report会被上传到App Store。

  6. App Store在符号化crash report后会把内部所有的crash reports做汇总并分组,这种聚合(相似crash report)的方法叫做crash聚类。

  7. 这些符号化后的crash report可以在你的Xcode的Crash Organizer中进行查看。

Bitcode

Bitcode(位编码)是一个编译好的项目的中间表现形式。当你在允许bitcode的前提下Archive一个app时,编译器会在二进制中包含bitcode而不是机器码。一旦binary信息被上传到App Store中,bitcode会被再次编译成机器码。也许App Store会在将来二次编译bitcode

虽然当你Archive你的app时会创建dSYM文件,但它们只能用在bitcode binary信息中,并不能用于符号化crash report。 App Store允许你从Xcode或者ITC网站中下载这些随着bitcode编译而产生的dSYM文件。 为了解析从App Review或者给你发送crash report的用户的crash report,你必须要下载这些dSYM文件,这样才能符号化crash report。 如果是从crash reporting service那里接收crash report,符号化会自动完成。

意思就是说,选择了Bitcode,你得从AppStore下载dSYM, 才是真正和binary信息匹配的,而不是你的中间码(提交的原始文件)

把”隐藏的”符号名还原成原始名

你把一个带有bitcode的app上传到App Store时,你也许在提交对话框中并没有勾选“上传你的app的符号表信息以便从Apple那边接收符号化过的 report”的选项。 当你选择不发送符号表信息给Apple时,Xcode会在你发送app到ITC之前用晦涩难懂的符号例如”_hidden#109”等来替换你的app里的dSYM文件。Xcode会创建一个原始符号和”隐藏”符号的对照表,并且将其存储在Archive的app文件中的一个bcsymbolmap文件里。每一个dSYM文件都会有一个对应的bcsymbolmap文件。

在符号化crash report之前,你需要把那些从ITC中下载下来的dSYM文件中的晦涩信息给解析一下。 如果你使用Xcode中的下载dSYM按钮,这步解析会自动完成。但是,如果你通过ITC网站来下载dSYM的话,你需要打开Terminal并且手动输入下面的命令来做解析(把example的path信息和sSYM信息替换一下)

xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/2017-11-23/MyGreatApp\ 11-23-17,\ 12.00\ PM.xcarchive/BCSymbolMaps ~/Downloads/dSYMs/3B15C133-88AA-35B0-B8BA-84AF76826CE0.dSYM

针对每一个dSYMs文件夹下的dSYM文件都运行一次这条命令。

用Xcode符号化iOS的Crash report

一般来说,Xcode会自动尝试符号化它所有的Crash report。所以你只需要把crash report加到Xcode Organizer就可以了。
Note:Xcode只认.crash后缀的crash report。如果你收到的crash report没有后缀名或者后缀是txt,在执行下列步骤之前先把它改成.crash。

  • 把iOS设备连接到你的Mac
  • 从Window菜单栏选择Devices
  • 在Devices左侧,选择一个设备
  • 点击右边在“Device Information“ 下面的 ”View Device Logs” 按钮
  • 把你的Crash report拖拽到左侧panel中
  • Xcode会自动符号化Crash report并且显示结果

为了符号化一个Crash report,Xcode需要去定位如下信息:

  • 崩溃的app的binary信息以及dSYM文件
  • 所有app关联的自定义framework的binary信息以及dSYM文件。如果是从app构建出来的framework,它们的dYSM会随着app的dSYM文件一起拷贝到archive中。如果是第三方的framework,你需要去找作者要dYSM文件。
  • 发生crash时app所依赖的OS的符号表信息。这些符号表包含了特定OS版本(例如iOS9.3.3)上的framework所需调试信息。 OS 符号表的架构具有独特性——一个64位的iOS设备不会包含armv7的符号表。Xcode将要自动拷贝你连接到的特定版本的Mac的符号表。

在上述任何一处,如果没有Xcode,你将无法符号化一个crash report,或者只能部分符号化一个crash report。

用atos符号化Crash report

atos命令可以把地址里的数字替换成等价的符号。如果调试符号信息是完备的,则atos的输出信息将会包含文件名和对应的资源行数。atos命令可以被用来单独符号化那些未符号化或者部分符号化过的crash report(中的堆栈信息里的地址)。
想要使用atos符号化crash report可以按如下方式操作:

  1. 找到你想要符号化的那一行,记下第二列的binary信息名,以及第三列的地址。
  2. 从crash report底部的binary信息名列表中找到那个名字,记下来架构名和加载的地址。

atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>

使用atos命令的样例,以及结果输出

$ atos -arch arm64 -o TheElements.app.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc

-[AtomicElementViewController myTransitionDidStop:finished:context:]

利用符号化排查问题

xcrun dwarfdump –uuid
注意:你必须保存你最开始上传到App Store的发生crash的app的归档文件。dSYM文件和app二进制文件是一一对应,且每次构建都不相同。即便通过相同的源码和配置,再执行一次构建,生成的dSYM文件也无法和之前的crash report做符号化匹配。
如果你不在存有这个归档文件,你应该重新提交一次有归档的新版本,以确保再发生crash的时候你可以符号化crash report。

关于异常信息分析,可以查看我写的另外一片怎么抓取崩溃信息那篇文章

去看

先注意!!!

dispatch_barrier_async要在自定义并发队列里!!!!
全局和串形达不到我们要的效果。
苹果文档中指出,如果使用的是全局队列或者创建的不是并发队列,则dispatch_barrier_async实际上就相当于dispatch_async。

另外dispatch_barrier_sync会阻塞当前线程,无意义。

使用场景

需求: 有n个任务, 需要开启多条线程去执行。 有一个特殊任务m, 需要在n1,n2任务执行完后, 再执行n3, n4任务。

没了,还能有啥,怎么写吗?

iOS App 签名机制

先说一下名词

Certificates

证书是用来给应用程序签名的,只有经过签名的应用程序才能保证他的来源是可信任的,并且代码是完整的, 未经修改的。
众所周知,我们申请一个Certificate之前,需要先申请一个Certificate Signing Request (CSR) 文件,而这个过程中实际上是生成了一对公钥和私钥,保存在你Mac的Keychain中。代码签名正是使用这种基于非对称秘钥的加密方式,用私钥进行签名,用公钥进行验证。如下图所示,在你Mac的keychain的login中存储着相关的公钥和私钥,而证书中包含了公钥。你只能用私钥来进行签名,所以如果没有了私钥,就意味着你不能进行签名了,所以就无法使用这个证书了,此时你只能revoke之前的证书再申请一个。因此在申请完证书时,最好导出并保存好你的私钥。当你想与其他人或其他设备共享证书时,把私钥传给它就可以了。私钥保存在你的Mac中,而苹果生成的Certificate中包含了公钥。当你用自己的私钥对代码签名后,苹果就可以用证书中的公钥来进行验证,确保是你对代码进行了签名,而不是别人冒充你,同时也确保代码的完整性等。

证书主要分为两类:Development和Production,Development证书用来开发和调试应用程序,Production主要用来分发应用程序(根据证书种类有不同作用),下面是证书的分类信息:(括号内为证书有效期)

(注:不同类型的开发者账户所能创建的证书种类不同)

  • Development

    • App Development (1年):用来开发和真机调试应用程序。
    • Push Development (1年):用来调试Apple Push Notification
  • Production

    • In-House and Ad Hoc (3年):用来发布In-House和AdHoc的应用程序。
    • App Store :用来发布提交App Store的应用程序。
    • MDM CSR
    • Push Production (1年):用来在发布版本中使用Apple Push Notification。
    • Pass Type ID Certificate
    • Website Push ID Certificate

App ID

App ID用于标识一个或者一组App,App ID应该是和Xcode中的Bundle ID是一致的或者匹配的。App ID主要有以下两种:

Explicit App ID:唯一的App ID,这种App ID用于唯一标识一个应用程序,例如com.ABC.demo1,标识Bundle ID为com.ABC.demo1的程序。
Wildcard App ID:通配符App ID,用于标识一组应用程序。例如*可以表示所有应用程序,而com.ABC.*可以表示以com.ABC开头的所有应用程序。

如果你的App使用上述的任何一种service,就要按照要求去配置。

Device

Device最简单了,就是iOS设备。Devices中包含了该账户中所有可用于开发和测试的设备。 每台设备使用UDID来唯一标识。

每个账户中的设备数量限制是100个。Disable 一台设备也不会增加名额,只能在membership year 开始的时候才能通过删除设备来增加名额。

Provisioning Profile

一个Provisioning Profile文件包含了上述的所有内容:证书、App ID、设备。而且这个Provisioning Profile文件会在打包时嵌入.ipa的包里。

言归正传说说签名原理

参考大神博客
直接呈现大神总结的:

再列一遍整个流程:

  1. 在你的 Mac 开发机器生成一对公私钥,这里称为公钥L,私钥L。L:Local
  2. 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A。A:Apple
  3. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  4. 在苹果后台申请 AppID,配置好设备 ID 列表和 APP 可使用的权限,再加上第③步的证书,组成的数据用私钥 A 签名,把数据和签名一起组成一个 Provisioning Profile 文件,下载到本地 Mac 开发机。
  5. 在开发时,编译完一个 APP 后,用本地的私钥 L 对这个 APP 进行签名,同时把第④步得到的 Provisioning Profile 文件打包进 APP 里,文件名为 embedded.mobileprovision,把 APP 安装到手机上。
  6. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证 embedded.mobileprovision 的数字签名是否正确,里面的证书签名也会再验一遍。
  7. 确保了 embedded.mobileprovision 里的数据都是苹果授权以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,AppID 是否对应得上,权限开关是否跟 APP 里的 Entitlements 对应等。

开发者证书从签名到认证最终苹果采用的流程大致是这样,还有一些细节像证书有效期/证书类型等就不细说了。

概念和操作

上面的步骤对应到我们平常具体的操作和概念是这样的:

  • 第 1 步对应的是 keychain 里的 “从证书颁发机构请求证书”,这里就本地生成了一对公私钥,保存的 CertificateSigningRequest 就是公钥,私钥保存在本地电脑里。
  • 第 2 步苹果处理,不用管。
  • 第 3 步对应把 CertificateSigningRequest 传到苹果后台生成证书,并下载到本地。这时本地有两个证书,一个是第 1 步生成的,一个是这里下载回来的,keychain 会把这两个证书关联起来,因为他们公私钥是对应的,在XCode选择下载回来的证书时,实际上会找到 keychain 里对应的私钥去签名。这里私钥只有生成它的这台 Mac 有,如果别的 Mac 也要编译签名这个 App 怎么办?答案是把私钥导出给其他 Mac 用,在 keychain 里导出私钥,就会存成 .p12 文件,其他 Mac 打开后就导入了这个私钥。
  • 第 4 步都是在苹果网站上操作,配置 AppID / 权限 / 设备等,最后下载 Provisioning Profile 文件。
  • 第 5 步 XCode 会通过第 3 步下载回来的证书(存着公钥),在本地找到对应的私钥(第一步生成的),用本地私钥去签名 App,并把 Provisioning Profile 文件命名为 embedded.mobileprovision 一起打包进去。这里对 App 的签名数据保存分两部分,Mach-O 可执行文件会把签名直接写入这个文件里,其他资源文件则会保存在 _CodeSignature 目录下。
  • 第 6 – 7 步的打包和验证都是 Xcode 和 iOS 系统自动做的事。

这里再总结一下这些概念:

  • 证书:内容是公钥或私钥,由其他机构对其签名组成的数据包。
  • Entitlements:包含了 App 权限开关列表。
  • CertificateSigningRequest:本地公钥。
  • p12:本地私钥,可以导入到其他电脑。
  • Provisioning Profile:包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

ipa解包后结构:

  • 可执行文件
    • Mach-O 64-bit executable arm64
    • Mach-O executable arm_v7
  • .car,资源打包文件 .nib、.bundle、Localizable.strings
  • _CodeSignature签名文件
    • 文件hash列表:存放每个文件的hash值;
    • XML结构
  • .mobileprovision

可执行文件Mach-O通常有三部分组成

  • 头部 (Header): Mach-O文件的架构 比如Mac的 PPC, PPC64, IA-32, x86-64,ios的arm系列。
  • 加载命令(Load commands): 在虚拟内存中指定文件的逻辑结构和布局。
  • 原始段数据(Raw segment data):可以拥有多个段(segment),每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间。
  1. XCode开启编译选项Write Link Map File
    XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为YES,并指定好linkMap的存储位置
    特别提醒:打包发布前记得还原为NO

  2. 编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File位于

    1
    2
    3
    4
    ~/Library/Developer/Xcode/DerivedData/XXX-XXXXXXXXXXXX/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/

    //example
    /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/4dBookCity-LinkMap-normal-arm64.txt

    这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。

LinkMap结构

  1. 首先列出来的是目标文件列表(中括号内为文件编号):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # Path: /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Products/Debug-iphoneos/4dBookCity.app/4dBookCity
    # Arch: arm64
    # Object files:
    [ 0] linker synthesized
    [ 1] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/Bulk_Arrays_12.o
    [ 2] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRSnapLearnInviteView.o
    [ 3] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRPKHomeCellViewModel.o
    [ 4] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/Bulk_Arrays_5.o
    [ 5] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRBookStoreItemScrollTemplateCell.o
    [ 6] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRAutoReadViewController.o
    [ 7] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRQAExerciseQestionTitleView.o
    [ 8] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRMyTaskController.o
    [ 9] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/UnityView.o
    [ 10] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/main.o
    ...
    [5229] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Products/Debug-iphoneos/libPods-4dBookCity.a(Pods-4dBookCity-dummy.o)
    [5230] /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneos.a(arclite.o)
    [5231] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/usr/lib/libobjc.tbd
    [5232] /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0/lib/darwin/libclang_rt.ios.a(os_version_check.c.o)
    [5233] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/System/Library/Frameworks//AudioToolbox.framework/AudioToolbox.tbd
    [5234] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/System/Library/Frameworks//CoreVideo.framework/CoreVideo.tbd
  2. 接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)

    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
    29
    30
    31
    32
    33
    34
    35
    # Sections:
    # Address Size Segment Section
    0x100005B00 0x0304A29C __TEXT __text
    0x10304FD9C 0x00004BC0 __TEXT __stubs
    0x10305495C 0x000044E8 __TEXT __stub_helper
    0x103058E50 0x0021563C __TEXT __cstring
    0x10326E48C 0x000AD400 __TEXT __objc_methname
    0x10331B88C 0x0000E6BA __TEXT __objc_classname
    0x103329F46 0x000166E3 __TEXT __objc_methtype
    0x103340640 0x002A0B60 __TEXT __const
    0x1035E11A0 0x001346D4 __TEXT __gcc_except_tab
    0x103715874 0x00008C78 __TEXT __ustring
    0x10371E4EC 0x0004D80C __TEXT __unwind_info
    0x10376BCF8 0x00000300 __TEXT __eh_frame
    0x10376C000 0x000015D8 __DATA __got
    0x10376D5D8 0x00003280 __DATA __la_symbol_ptr
    0x103770858 0x00001838 __DATA __mod_init_func
    0x103772090 0x000FF7F8 __DATA __const
    0x103871888 0x0006F9C0 __DATA __cfstring
    0x1038E1248 0x00004778 __DATA __objc_classlist
    0x1038E59C0 0x00000290 __DATA __objc_nlclslist
    0x1038E5C50 0x00000708 __DATA __objc_catlist
    0x1038E6358 0x00000038 __DATA __objc_nlcatlist
    0x1038E6390 0x00000910 __DATA __objc_protolist
    0x1038E6CA0 0x00000008 __DATA __objc_imageinfo
    0x1038E6CA8 0x00206C58 __DATA __objc_const
    0x103AED900 0x00027F28 __DATA __objc_selrefs
    0x103B15828 0x000000C0 __DATA __objc_protorefs
    0x103B158E8 0x000041B8 __DATA __objc_classrefs
    0x103B19AA0 0x000030C0 __DATA __objc_superrefs
    0x103B1CB60 0x0000BB54 __DATA __objc_ivar
    0x103B286B8 0x0002CB00 __DATA __objc_data
    0x103B551C0 0x01D52748 __DATA __data
    0x1058A7920 0x00714878 __DATA __bss
    0x105FBD000 0x0012B978 __DATA __common

    首列是数据在文件的偏移位置,第二列是这一段占用大小,第三列是段类型,代码段和数据段,第四列是段名称。
    每一行的数据都紧跟在上一行后面,如第二行__stubs的地址0x10304FD9C就是第一行__text的地址0x100005B00加上大小0x0304A29C,整个可执行文件大致数据分布就是这样。
    这里可以清楚看到各种类型的数据在最终可执行文件里占的比例,例如__text表示编译后的程序执行语句,__data表示已初始化的全局变量和局部静态变量,__bss表示未初始化的全局变量和局部静态变量,__cstring表示代码里的字符串常量,等等。

  3. 接着就是按上表顺序,列出具体的按每个文件列出每个对应字段的位置和占用空间

    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
    # Symbols:
    # Address Size File Name
    0x100005B00 0x000000EC [ 2] -[MXRSnapLearnInviteView drawRect:]
    0x100005BEC 0x0000024C [ 2] -[MXRSnapLearnInviteView generatorlogoImageQRCode]
    0x100005E38 0x0000005C [ 2] _CGRectMake
    0x100005E94 0x00000034 [ 2] -[MXRSnapLearnInviteView inviteCode]
    0x100005EC8 0x00000050 [ 2] -[MXRSnapLearnInviteView setInviteCode:]
    0x100005F18 0x0000003C [ 2] -[MXRSnapLearnInviteView .cxx_destruct]
    0x100005F54 0x000001D8 [ 3] -[MXRPKHomeCellViewModel initWithModel:]
    0x10000612C 0x0000016C [ 3] -[MXRPKHomeCellViewModel encodeWithCoder:]
    0x100006298 0x00000268 [ 3] -[MXRPKHomeCellViewModel initWithCoder:]
    0x100006500 0x00000040 [ 3] -[MXRPKHomeCellViewModel desc]
    0x100006540 0x00000044 [ 3] -[MXRPKHomeCellViewModel setDesc:]
    0x100006584 0x00000040 [ 3] -[MXRPKHomeCellViewModel name]
    0x1000065C4 0x00000044 [ 3] -[MXRPKHomeCellViewModel setName:]
    0x100006608 0x00000040 [ 3] -[MXRPKHomeCellViewModel pic]
    0x100006648 0x00000044 [ 3] -[MXRPKHomeCellViewModel setPic:]
    0x10000668C 0x00000040 [ 3] -[MXRPKHomeCellViewModel classifyId]
    0x1000066CC 0x00000044 [ 3] -[MXRPKHomeCellViewModel setClassifyId:]
    0x100006710 0x000000B8 [ 3] -[MXRPKHomeCellViewModel .cxx_destruct]
    ...
    0x1060C82D0 0x000000C0 [3391] _jerrenv
    0x1060C8390 0x000204E0 [4793] _GC_arrays
    0x1060E8870 0x00000100 [4793] _GC_bm_table
    0x1060E8970 0x00000008 [4793] _GC_noop_sink

    同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。

  4. 已废弃&多余重复的字段

    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    # Dead Stripped Symbols:
    # Size File Name
    <<dead>> 0x00000001 [ 1] literal string:
    <<dead>> 0x00000005 [ 3] literal string: desc
    <<dead>> 0x00000005 [ 3] literal string: name
    <<dead>> 0x00000004 [ 3] literal string: pic
    <<dead>> 0x0000000B [ 3] literal string: classifyId
    <<dead>> 0x0000000E [ 3] literal string: .cxx_destruct
    <<dead>> 0x0000000B [ 3] literal string: v24@0:8@16
    <<dead>> 0x00000008 [ 3] literal string: v16@0:8
    <<dead>> 0x00000008 [ 3] literal string: @16@0:8
    <<dead>> 0x00000001 [ 4] literal string:
    <<dead>> 0x00000007 [ 4] literal string: System
    <<dead>> 0x0000000C [ 4] literal string: UnityEngine
    <<dead>> 0x0000000A [ 4] literal string: System.IO
    <<dead>> 0x00000008 [ 5] 8-byte-literal
    <<dead>> 0x0000000C [ 5] literal string: PRIMARY KEY
    <<dead>> 0x0000000C [ 5] literal string: FOREIGN KEY
    <<dead>> 0x00000001 [ 5] literal string:
    <<dead>> 0x00000020 [ 5] CFString
    <<dead>> 0x00000020 [ 5] CFString
    <<dead>> 0x00000008 [ 5] _LKSQL_Type_Text
    <<dead>> 0x00000008 [ 5] _LKSQL_Type_Int
    <<dead>> 0x00000008 [ 5] _LKSQL_Type_Double
    <<dead>> 0x00000008 [ 5] _LKSQL_Type_Blob
    <<dead>> 0x00000008 [ 5] _LKSQL_Attribute_NotNull
    <<dead>> 0x00000008 [ 5] _LKSQL_Attribute_PrimaryKey
    <<dead>> 0x00000008 [ 5] _LKSQL_Attribute_Default
    <<dead>> 0x00000008 [ 5] _LKSQL_Attribute_Unique
    <<dead>> 0x00000008 [ 5] _LKSQL_Attribute_Check
    <<dead>> 0x00000008 [ 5] _LKSQL_Attribute_ForeignKey
    <<dead>> 0x00000008 [ 5] _LKSQL_Convert_FloatType
    <<dead>> 0x00000008 [ 5] _LKSQL_Convert_IntType
    <<dead>> 0x00000008 [ 5] _LKSQL_Convert_BlobType
    <<dead>> 0x00000008 [ 5] _LKSQL_Mapping_Inherit
    <<dead>> 0x00000008 [ 5] _LKSQL_Mapping_Binding
    <<dead>> 0x00000008 [ 5] _LKSQL_Mapping_UserCalculate
    <<dead>> 0x00000008 [ 5] _LKDB_TypeKey
    <<dead>> 0x00000008 [ 5] _LKDB_TypeKey_Model
    <<dead>> 0x00000008 [ 5] _LKDB_TypeKey_JSON
    <<dead>> 0x00000008 [ 5] _LKDB_TypeKey_Combo
    ...
    <<dead>> 0x00000004 [4311] 4-byte-literal
    <<dead>> 0x00000004 [4311] 4-byte-literal
    <<dead>> 0x00000004 [4311] 4-byte-literal
    <<dead>> 0x00000004 [4311] 4-byte-literal
    <<dead>> 0x00000008 [4311] 8-byte-literal
    <<dead>> 0x00000008 [4312] 8-byte-literal
    <<dead>> 0x00000014 [4320] __ZN15PxcConvexMeshHLC2EP17PxConvexMeshData_
    <<dead>> 0x00000004 [4320] 4-byte-literal
    <<dead>> 0x00000004 [4320] 4-byte-literal
    <<dead>> 0x00000004 [4319] 4-byte-literal
    <<dead>> 0x00000076 [4319] literal string: /Applications/buildAgent/work/3d1e9e6e6eefaa7f/SDKs/compiler/iphone/../../../LowLevel/common/include/utils/PxcArray.h

    这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。LinkMap里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o最后的大小,属于一个.a静态链接库的.o加起来,就是这个库在APP里占用的空间大小。

关于Xcode的Other Linker Flags

  • 背景

在ios开发过程中,有时候会用到第三方的静态库(.a文件),然后导入后发现编译正常但运行时会出现selector not recognized的错误,从而导致app闪退。接着仔细阅读库文件的说明文档,你可能会在文档中发现诸如在Other Linker Flags中加入-ObjC或者-all_load这样的解决方法。
那么,Other Linker Flags到底是用来干什么的呢?还有-ObjC和-all_load到底发挥了什么作用呢?

  • 链接器

首先,要说明一下Other Linker Flags到底是用来干嘛的。说白了,就是ld命令除了默认参数外的其他参数。ld命令实现的是链接器的工作,详细说明可以在终端man ld查看。
如果有人不清楚链接器是什么东西的话,我可以作个简单的说明。
一个程序从简单易读的代码到可执行文件往往要经历以下步骤:

1
源代码 > 预处理器 > 编译器 > 汇编器 > 机器码 > 链接器 > 可执行文件

源文件经过一系列处理以后,会生成对应的.obj文件,然后一个项目必然会有许多.obj文件,并且这些文件之间会有各种各样的联系,例如函数调用。链接器做的事就是把这些目标文件和所用的一些库链接在一起形成一个完整的可执行文件。

  • 为什么会闪退

Objective-C的链接器并不会为每个方法建立符号表,而是仅仅为类建立了符号表。这样的话,如果静态库中定义了已存在的一个类的分类,链接器就会以为这个类已经存在,不会把分类和核心类的代码合起来。这样的话,在最后的可执行文件中,就会缺少分类里的代码,这样函数调用就失败了。

  • 解决方法

解决方法在背景那块我就提到了,就是在Other Linker Flags里加上所需的参数,用到的参数一般有以下3个:

1
2
3
4
`-ObjC`       : 链接器会把静态库中所有的类和分类都加载到最后的可执行文件中
`-force_load` : 需要指定要进行全部加载的库文件的路径,避免引用多个第三方库时会出现类名重叠的冲突
`-all_load` : 让链接器把所有找到的目标文件都加载到可执行文件中,不建议使用
`-dead_strip` : 删除多余的库符号,不建议使用

在编译Objective-C源文件到目标文件时,编译器并不知道方法的对应实现,只能在运行时才知道,所以编译器只会为类生成链接符号,对类中的方法不会生成链接符号。由于Category方法并不对应一个新类,所以不会生成链接符号,链接器也不会将Category方法合并到原始的类中,最终导致链接器忽略了Category方法,不会将其链接到可执行文件中。

当静态库中只有分类而没有类的时候,-ObjC参数就会失效了。这时候,就需要使用-all_load或者-force_load了。
-all_load会让链接器把所有找到的目标文件都加载到可执行文件中,但是千万不要随便使用这个参数!假如你使用了不止一个静态库文件,然后又使用了这个参数,那么你很有可能会遇到ld: duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件,所以建议在遇到-ObjC失效的情况下使用-force_load参数。
-force_load所做的事情跟-all_load其实是一样的,但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载。
在能拿到静态库源码情况下,建议对.a库重新打包,删除部分重复的symbol。
在拿不到静态库源码情况下 ,只能采用-force_load+库文件路径方法设置Other Linker Flags,逐个加静态库,最终完美解决两个静态库存在同名文件冲突,发现那个静态库无法调用,就采用以下语句添加进去。
-force_load EightPartyCall/standaloneclass/BaiduSocialShare/WX/libWeChatSDK
(-force_load后面为静态库文件路径,根据自己项目对应路径)

也可以拆分静态库

1
2
3
4
5
6
7
8
9
10
11
12
$ cd /LibSDK 
$ ls
libiot.sdk.a
$ lipo -info libiot.sdk.a
Architectures in the fat file: libiot.sdk.a are: armv7 arm64
$ lipo libiot.sdk.a -thin armv7 -output tbv7.a
$ lipo libiot.sdk.a -thin arm64 -output tb64.a
$ ls
libiot.sdk.a tb64.a tbv7.a
$ ar -d tbv7.a AsyncSocket.o
$ ar -d tb64.a AsyncSocket.o
$ lipo -create tbv7.a tbv64.a -output libSun.a

lipo源于mac系统要制作兼容powerpc平台和intel平台的程序。
lipo 是一个在 Mac OS X 中处理通用程序(Universal Binaries)的工具。现在发售或者提供下载的许多(几乎所有)程序都打上了“Universal”标志,意味着它们同时具有 PowerPC 和 Intel 芯片能够处理的代码。不过既然你可能不在意其中的一个,你就能够使用 lipo 来给你的程序“瘦身”。

写了一堆乱七八糟的

参考app包优化
干货|今日头条iOS端安装包大小优化—思路与实践

NSURLSession与NSURLConnection区别

2013 年的 WWDC 大会上,iOS 7.0 推出了 NSURLSession,对 Foundation URL 加载系统进行了彻底的重构,提供了更丰富的 API 来处理网络请求,如:支持 http2.0 协议、直接把数据下载到磁盘、同一 session 发送多个请求、下载是多线程异步处理和提供全局的 session 并可以统一配置等等,提高了 NSURLSession 的易用性、灵活性,更加地适合移动开发的需求。

在iOS9以后,NSURLConnection过期废弃.

NSURLSession 有三种网络操作方案:

  • 普通网络请求 NSURLSessionDataTask
  • 上传 NSURLSessionUploadTask
  • 下载 NSURLSessionDownloadTask

创建的 task 都是挂起状态,需要 resume 才能启动.

普通网络请求 和 上传网络请求

  • 普通: 当服务器返回的数据较小时,NSURLSession 与 NSURLConnection 执行普通任务的操作步骤没有区别。
  • 上传: 执行上传任务时,NSURLSession 与 NSURLConnection 一样需要设置 POST 请求的请求体进行上传。

下载任务方式

  • NSURLConnection下载文件时,先是将整个文件下载到内存,然后再写入到沙盒,如果文件比较大,就会出现内存暴涨的情况。
  • 而使用 NSURLSessionDownloadTask 下载文件,会默认下载到沙盒中的 tmp 文件中,不会出现内存暴涨的情况,但是在下载完成后会把 tmp 中的临时文件删除,需要在初始化任务方法时,在 completionHandler 回调中增加保存文件的代码。

请求方法的控制

  • NSURLConnection 实例化对象,实例化开始,默认请求就发送(同步发送),不需要调用 start 方法。而 cancel 可以停止请求的发送,停止后不能继续访问,需要创建新的请求。
  • NSURLSession 有三个控制方法,取消(cancel)、暂停(suspend)、继续(resume),暂停以后可以通过继续恢复当前的请求任务。

断点续传的方式

  • NSURLConnection 进行断点下载,通过设置访问请求的 HTTPHeaderField 的 Range 属性,开启运行循环,NSURLConnection 的代理方法作为运行循环的事件源,接收到下载数据时代理方法就会持续调用,并使用 NSOutputStream 管道流进行数据保存。

  • NSURLSession 进行断点下载,当暂停下载任务后,如果 downloadTask(下载任务)为非空,调用 cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler 这个方法,这个方法接收一个参数,完成处理代码块,这个代码块有一个 NSData 参数 resumeData,如果 resumeData 非空,我们就保存这个对象到视图控制器的 resumeData 属性中,在点击再次下载时,通过调用[ [self.session downloadTaskWithResumeData:self.resumeData] resume]方法进行继续下载操作。

经过以上比较可以发现,使用 NSURLSession 进行断点下载更加便捷。

配置信息

NSURLSessionConfiguration 类的参数可以设置配置信息,其决定了 cookie安全高速缓存策略最大主机连接数资源管理网络超时等配置。NSURLConnection 不能进行这个配置,相比较与 NSURLConnection 依赖与一个全局的配置对象,缺乏灵活性而言,NSURLSession 有很大的改进了。

有三个方法来创建NSURLSessionConfiguration:

  • defaultSessionConfiguration 使用全局的cache,cookie,使用硬盘来缓存数据。

  • ephemeralSessionConfiguration 临时session配置,与默认配置相比,这个配置不会将缓存、cookie等存在本地,只会存在内存里,所以当程序退出时,所有的数据都会消失

  • backgroundSessionConfiguration 后台session配置,与默认配置类似,不同的是会在后台开启另一个线程来处理网络数据。

一旦创建了NSURLSessionConfiguration就可以给它设置各种属性

看NSURLSessionConfiguration的头文件:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@interface NSURLSessionConfiguration : NSObject <NSCopying>

/* 三种创建方式 */

+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier NS_AVAILABLE(10_10, 8_0);

/* 当使用上述第三种方式创建后台sessionConfiguration时可以读到初始化时传入的唯一标识,其他创建方式都为空 */
@property (nullable, readonly, copy) NSString *identifier;

/*
缓存策略,默认值是NSURLRequestUseProtocolCachePolicy
*/
@property NSURLRequestCachePolicy requestCachePolicy;

/* 给request指定每次接收数据超时间隔,如果下一次接受新数据用时超过该值,则发送一个请求超时给该request。默认为60s */
@property NSTimeInterval timeoutIntervalForRequest;

/* 给指定resource设定一个超时时间,resource需要在时间到达之前完成。默认是7天。 */
@property NSTimeInterval timeoutIntervalForResource;

/* 指定网络传输类型。精切指出传输类型,可以让系统快速响应,提高传输质量,延长电池寿命等。
typedef NS_ENUM(NSUInteger, NSURLRequestNetworkServiceType)
{
NSURLNetworkServiceTypeDefault = 0, // 普通网络传输,默认使用这个
NSURLNetworkServiceTypeVoIP = 1, // 网络语音通信传输,只能在VoIP使用
NSURLNetworkServiceTypeVideo = 2, // 影像传输
NSURLNetworkServiceTypeBackground = 3, // 网络后台传输,优先级不高时可使用。对用户不需要的网络操作可使用
NSURLNetworkServiceTypeVoice = 4 // 语音传输
};
*/
@property NSURLRequestNetworkServiceType networkServiceType;

/* 是否使用蜂窝网络,默认是yes. */
@property BOOL allowsCellularAccess;

/* 是否由系统根据性能自动裁量后台任务。默认值是NO。同sessionSendsLaunchEvent一样,只对后台configuration有效。 */
@property (getter=isDiscretionary) BOOL discretionary NS_AVAILABLE(10_10, 7_0);

/*
如果要为app的插件提供session,需要给这个值赋值
*/
@property (nullable, copy) NSString *sharedContainerIdentifier NS_AVAILABLE(10_10, 8_0);

/*
表示当后台传输结束时,是否启动app.这个属性只对 后台sessionConfiguration 生效,其他configuration类型会自动忽略该值。默认值是YES。
*/
@property BOOL sessionSendsLaunchEvents NS_AVAILABLE(NA, 7_0);

/*
指定了会话连接中的代理服务器。同样地,大多数面向消费者的应用程序都不需要代理,所以基本上不需要配置这个属性,默认为NULL
*/
@property (nullable, copy) NSDictionary *connectionProxyDictionary;

/* 确定是否支持SSLProtocol版本的会话
*/
@property SSLProtocol TLSMinimumSupportedProtocol;

/*
确定是否支持SSLProtocol版本的会话
*/
@property SSLProtocol TLSMaximumSupportedProtocol;

/*
它可以被用于开启HTTP管道,这可以显着降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的
*/
@property BOOL HTTPShouldUsePipelining;

/*
默认为yes,是否提供来自shareCookieStorge的cookie,如果想要自己提供cookie,可以使用HTTPAdditionalHeaders来提供。
*/
@property BOOL HTTPShouldSetCookies;

/* Policy for accepting cookies. This overrides the policy otherwise specified by the cookie storage. */
@property NSHTTPCookieAcceptPolicy HTTPCookieAcceptPolicy;

/*
指定了一组默认的可以设置出站请求的数据头。这对于跨会话共享信息,如内容类型,语言,用户代理,身份认证,是很有用的。
例如:
@{@"Accept": @"application/json",
@"Accept-Language": @"en",
@"Authorization": authString,
@"User-Agent": userAgentString
}
*/
@property (nullable, copy) NSDictionary *HTTPAdditionalHeaders;

/*
同时连接一个host的最大数。iOS默认是4.APP是作为一个整体来看的
*/
@property NSInteger HTTPMaximumConnectionsPerHost;

/*
存储cookie,清除存储,直接set为nil即可。
对于默认和后台的session,使用sharedHTTPCookieStorage。
对于短暂的session,cookie仅仅储存到内存,session失效时会自动清除。
*/
@property (nullable, retain) NSHTTPCookieStorage *HTTPCookieStorage;

/*
证书存储,如果不使用,可set为nil.
默认和后台session,默认使用的sharedCredentialStorage.
短暂的session使用一个私有存储在内存中。session失效会自动清除。
*/
@property (nullable, retain) NSURLCredentialStorage *URLCredentialStorage;

/*
缓存NSURLRequest的response。
默认的configuration,默认值的是sharedURLCache。
后台的configuration,默认值是nil
短暂的configuration,默认一个私有的cache于内存,session失效,cache自动清除。
*/
@property (nullable, retain) NSURLCache *URLCache;

/* Enable extended background idle mode for any tcp sockets created. Enabling this mode asks the system to keep the socket open
* and delay reclaiming it when the process moves to the background (see https://developer.apple.com/library/ios/technotes/tn2277/_index.html)
*/
@property BOOL shouldUseExtendedBackgroundIdleMode NS_AVAILABLE(10_11, 9_0);

/*
处理NSURLRequest的NSURLProtocol的子类。
重要:对后台Session失效。
*/
@property (nullable, copy) NSArray<Class> *protocolClasses;

@end

URLSessionTask

NSURLSessionTask是一个抽象类,其下有4个实体子类可以直接使用:NSURLSessionDataTask、NSURLSessionUploadTask、NSURLSessionDownloadTask、NSURLSessionStreamTask。这四个子类封装了现代程序四个最基本的网络任务:获取数据,比如JSON或者XML,上传文件和下载文件还有数据流的获取。

NSURLSession比NSURLConnection最方便的地方就是任务可以暂停,继续。在网络请求中,真正去执行下载或者上传任务的就是URLSessionTask,我们来看一下它常用的方法:
- (void)resume; 当使用NSURLSession创建一个NSURLSessionTask任务时,要手动调用此方法,任务才会开启,而NSURLConnection默认开启。
- (void)suspend; 暂停任务方法,手动调用会暂停当前任务,再次开启此任务时,会从紧接上次任务开始,会面会说到如何暂停任务再开启任务。
- (void)cancel; 取消任务。

NSURLSessionTask还有个属性,@property (readonly) NSURLSessionTaskState state; 此属性标识当前任务的状态,枚举类型

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, NSURLSessionTaskState) {
NSURLSessionTaskStateRunning = 0, /* 正在执行 */
NSURLSessionTaskStateSuspended = 1, /* 暂停状态 */
NSURLSessionTaskStateCanceling = 2, /* 取消状态*/
NSURLSessionTaskStateCompleted = 3, /* 任务完成状态 */
}

上面说到的四个类,都直接或间接继承NSURLSessionTask,所有NSURLSessionTask的方法或者属性这四个类都有,那么,简单说一下这四个类都是干什么的。

NSURLSessionDataTask

NSURLSessionDataTask是开发中使用频率最高的,我们平常使用的GET和POST请求都是通过它来实现的,如果请求的数据简单并且不需要对获取的数据进行复杂操作,我们使用 Block 解析返回的数据即可。

另外我们也可以设置session的代理来实时的监听数据,我们可以使用NSURLSession的+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;这两个方法来设置代理,具体的协议为NSURLSessionDelegate,它有四个直接或间接子协议NSURLSessionTaskDelegateNSURLSessionDownloadDelegateNSURLSessionStreamDelegateNSURLSessionDataDelegate

NSURLSessionDownloadTask

NSURLSessionDownloadTask在下载文件的时候,是将数据一点点地写入本地的临时文件。所以在 completionHandler 这个 block 里,我们需要把文件从一个临时地址移动到一个永久的地址保存起来:

断点续传

说一下开发中经常用到的断点续传。在开发中,我们经常由于某种原因,在下载或上传的时候往往不能一次性下载或上传完,有可能下载或上传了一半就终止了,这时候当条件满足继续下载或上传时,我们不希望从头开始,这时候就可以使用断点续传。它的大概思路是:

  • 某种限制,续传暂停
  • 将暂停后数据(当前数据)保存起来–_resumeData = resumeData;
  • 条件允许续传时,使用resumeData创建新的NSURLSessionTask

NSURLSessionUploadTask

在 NSURLSession 中,文件上传主要使用两种方式:

1
2
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

总结

  1. NSURLSession 是iOS7后出来的替代 NSURLConnection 的API;
  2. NSURLSession 通过绑定一个 delegate 在一个网络会话的生命周期里调用某些事件;
  3. NSURLSession 对象是线程安全的;
  4. NSURLSession 默认使用系统提供的delegate,并适当的使用现有的代码使用;
  5. NSURLSession 通过创建 NSURLSessionTask 代表资源被加载的任务,类似 NSURLConnection对象,但相比有更多可控制和定制的 delegate 模型;
  6. NSURLSessionTask 对象创建后是处于挂起状态,只有发-resume消息才会执行;
  7. NSURLSessionTask 的子类 在加载 Data 和 文件下载的使用上有所不同;
  8. NSURLSessionDataTask 接收到资源时会回调 RLSession:dataTask:didReceiveData: 代理method;
  9. NSURLSessionUploadTask 与 NSURLSessionDataTask 的构造方法不同,他需求显示的引用file或data object;
  10. NSURLSessionDownloadTask 会直接把response data写入到临时文件,网络会话结束后,delegate 会发送一个 URLSession:downloadTask:didFinishDownloadingToURL: 消息来处理下载文件,如果中途取消则会生成一个Blob Data以便下次恢复下载;
  11. 从iOS9开始,使用NSURLSessionStream可以通过一个给定的主机和端口直接建立TCP/IP连接;

NSURLSession的优点

  • 后台上传和下载: 只需在创建NSURLSession的时候配置一个选项,就能得到后台网络的所有好处;
  • 能够暂停和恢复网络操作: 使用NSURLSession API能够暂停,停止,恢复所有的网络任务,再也完全不需要子类化NSOperation;
  • 可配置的容器: 对于NSURLSession里面的requests来说,每个NSURLSession都是可配置的容器。举个例来说,假如你需要设置HTTP header选项,你只用做一次,session里面的每个request就会有同样的配置。
  • 提高认证处理:认证是在一个指定的连接基础上完成的。在使用NSURLConnection时,如果发出一个访问,会返回一个任意的request。此时,你就不能确切的知道哪个request收到了访问。而在NSURLSession中,就能用代理处理认证。
  • 丰富的代理模式: 在处理认证的时候,NSURLConnection有一些基于异步的block方法,但是它的代理方法就不能处理认证,不管请求是成功或是失败。在NSURLSession中,可以混合使用代理和block方法处理认证。

多看下 AFNetworking 的实现

组成

CocoaPods是用 Ruby 写的,并由若干个 Ruby 包 (gems) 构成的。在解析整合过程中,最重要的几个 gems 分别是: CocoaPods/CocoaPods, CocoaPods/Core, 和 CocoaPods/Xcodeproj (是的,CocoaPods 是一个依赖管理工具 – 利用依赖管理进行构建的!)。

CocoaPods/CocoaPod

这是是一个面向用户的组件,每当执行一个 pod 命令时,这个组件都将被激活。该组件包括了所有使用 CocoaPods 涉及到的功能,并且还能通过调用所有其它的 gems 来执行任务。

CocoaPods/Core

Core 组件提供支持与 CocoaPods 相关文件的处理,文件主要是 Podfile 和 podspecs。

Podfile

Podfile 是一个文件,用于定义项目所需要使用的第三方库。该文件支持高度定制,你可以根据个人喜好对其做出定制。更多相关信息,请查阅 Podfile 指南。

Podspec

.podspec 也是一个文件,该文件描述了一个库是怎样被添加到工程中的。它支持的功能有:列出源文件、framework、编译选项和某个库所需要的依赖等。

CocoaPods/Xcodeproj

这个 gem 组件负责所有工程文件的整合。它能够对创建并修改 .xcodeproj 和 .xcworkspace 文件。它也可以作为单独的一个 gem 包使用。如果你想要写一个脚本来方便的修改工程文件,那么可以使用这个 gem。

加载源文件

CocoaPods 执行的下一步是加载源码。每个 .podspec 文件都包含一个源代码的索引,这些索引一般包裹一个 git 地址和 git tag。它们以 commit SHAs 的方式存储在 ~/Library/Caches/CocoaPods 中。这个路径中文件的创建是由 Core gem 负责的。

CocoaPods 将依照 Podfile、.podspec 和缓存文件的信息将源文件下载到 Pods 目录中。

生成 Pods.xcodeproj

每次 pod install 执行,如果检测到改动时,CocoaPods 会利用 Xcodeproj gem 组件对 Pods.xcodeproj 进行更新。如果该文件不存在,则用默认配置生成。否则,会将已有的配置项加载至内存中。

安装第三方库

当 CocoaPods 往工程中添加一个第三方库时,不仅仅是添加代码这么简单,还会添加很多内容。由于每个第三方库有不同的 target,因此对于每个库,都会有几个文件需要添加,每个 target 都需要:

  • 一个包含编译选项的 .xcconfig 文件
  • 一个同时包含编译设置和 CocoaPods 默认配置的私有 .xcconfig 文件
  • 一个编译所必须的 prefix.pch 文件
  • 另一个编译必须的文件 dummy.m

一旦每个 pod 的 target 完成了上面的内容,整个 Pods target 就会被创建。这增加了相同文件的同时,还增加了另外几个文件。如果源码中包含有资源 bundle,将这个 bundle 添加至程序 target 的指令将被添加到 Pods-Resources.sh 文件中。还有一个名为 Pods-environment.h 的文件,文件中包含了一些宏,这些宏可以用来检查某个组件是否来自 pod。最后,将生成两个认可文件,一个是 plist,另一个是 markdown,这两个文件用于给最终用户查阅相关许可信息。

写入至磁盘

直到现在,许多工作都是在内存中进行的。为了让这些成果能被重复利用,我们需要将所有的结果保存到一个文件中。所以 Pods.xcodeproj 文件被写入磁盘,另外两个非常重要的文件:Podfile.lock 和 Manifest.lock 都将被写入磁盘。

  • Podfile.lock
    这是 CocoaPods 创建的最重要的文件之一。它记录了需要被安装的 pod 的每个已安装的版本。如果你想知道已安装的 pod 是哪个版本,可以查看这个文件。推荐将 Podfile.lock 文件加入到版本控制中,这有助于整个团队的一致性。

  • Manifest.lock
    这是每次运行 pod install 命令时创建的 Podfile.lock 文件的副本。如果你遇见过这样的错误 沙盒文件与 Podfile.lock 文件不同步 (The sandbox is not in sync with the Podfile.lock),这是因为 Manifest.lock 文件和 Podfile.lock 文件不一致所引起。由于 Pods 所在的目录并不总在版本控制之下,这样可以保证开发者运行 app 之前都能更新他们的 pods,否则 app 可能会 crash,或者在一些不太明显的地方编译失败。

  • xcproj
    如果你已经依照我们的建议在系统上安装了 xcproj,它会对 Pods.xcodeproj 文件执行一下 touch 以将其转换成为旧的 ASCII plist 格式的文件。为什么要这么做呢?虽然在很久以前就不被其它软件支持了,但是 Xcode 仍然依赖于这种格式。如果没有 xcproj,你的 Pods.xcodeproj 文件将会以 XML 格式的 plist 文件存储,当你用 Xcode 打开它时,它会被改写,并造成大量的文件改动。

结语

依赖库有Pod工程管理,最终编译为.a文件或者framework文件,为主项目使用

libPods.a
Pods-resources.sh
Pods.xcconfig

对于MVVM的理解?

MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

Vue的生命周期

  1. beforeCreate(创建前) 在数据观测和初始化事件还未开始
  2. created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
  3. beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
  4. mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
  5. beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
  6. updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  7. beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
    destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

1.什么是vue生命周期?
答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

2.vue生命周期的作用是什么?
答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

3.vue生命周期总共有几个阶段?
答:它可以总共分为8个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

4.第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。

5.DOM 渲染在 哪个周期中就已经完成?
答:DOM 渲染在 mounted 中就已经完成了。

Vue实现数据双向绑定的原理:Object.defineProperty()

vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

js实现简单的双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<input type="text" id="txt">
<p id="show"></p>
</div>
</body>
<script type="text/javascript">
var obj = {}
Object.defineProperty(obj, 'txt', {
get: function () {
return obj
},
set: function (newValue) {
document.getElementById('txt').value = newValue
document.getElementById('show').innerHTML = newValue
}
})
document.addEventListener('keyup', function (e) {
obj.txt = e.target.value
})
</script>

Vue组件间的参数传递

  1. 父组件与子组件传值
    父组件传给子组件:子组件通过props方法接受数据;
    子组件传给父组件:$emit方法传递参数
  2. 非父子组件间的数据传递,兄弟组件传值
    eventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。项目比较小时,用这个比较合适。(虽然也有不少人推荐直接用VUEX,具体来说看需求咯。技术只是手段,目的达到才是王道。)

Vue的路由实现:hash模式 和 history模式

hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com, 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

vue路由的钩子函数

首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

beforeEach主要有3个参数to,from,next:

to:route即将进入的目标路由对象,

from:route当前导航正要离开的路由

next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。

vuex是什么?怎么使用?哪种功能场景使用它?

只用来读取的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事物; 异步逻辑应该封装在action中。
在main.js引入store,注入。新建了一个目录store,….. export 。
场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车

state
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
mutations
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。
getters
类似vue的计算属性,主要用来过滤一些数据。
action
actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({ //store实例
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})

modules
项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
})

vue-cli如何新增自定义指令?

  1. 创建局部指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var app = new Vue({
el: '#app',
data: {
},
// 创建指令(可以多个)
directives: {
// 指令名称
dir1: {
inserted(el) {
// 指令中第一个参数是当前使用指令的DOM
console.log(el);
console.log(arguments);
// 对DOM进行操作
el.style.width = '200px';
el.style.height = '200px';
el.style.background = '#000';
}
}
}
})
  1. 全局指令
1
2
3
4
5
Vue.directive('dir2', {
inserted(el) {
console.log(el);
}
})
  1. 指令的使用
    1
    2
    3
    4
    <div id="app">
    <div v-dir1></div>
    <div v-dir2></div>
    </div>

vue如何自定义一个过滤器?

html代码:

1
2
3
4
<div id="app">
<input type="text" v-model="msg" />
{{msg| capitalize }}
</div>

JS代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var vm=new Vue({
el:"#app",
data:{
msg:''
},
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
})

全局定义过滤器:

1
2
3
4
5
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})

过滤器接收表达式的值 (msg) 作为第一个参数。capitalize 过滤器将会收到 msg的值作为第一个参数。

对keep-alive 的了解?

keep-alive是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
在vue 2.1.0 版本之后,keep-alive新加入了两个属性: include(包含的组件缓存) 与 exclude(排除的组件不缓存,优先级大于include) 。

使用方法

1
2
3
4
5
<keep-alive include='include_components' exclude='exclude_components'>
<component>
<!-- 该组件是否缓存取决于include和exclude属性 -->
</component>
</keep-alive>

参数解释
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式,任何名称匹配的组件都不会被缓存
include 和 exclude 的属性允许组件有条件地缓存。二者都可以用“,”分隔字符串、正则表达式、数组。当使用正则或者是数组时,要记得使用v-bind 。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 逗号分隔字符串,只有组件a与b被缓存。 -->
<keep-alive include="a,b">
<component></component>
</keep-alive>

<!-- 正则表达式 (需要使用 v-bind,符合匹配规则的都会被缓存) -->
<keep-alive :include="/a|b/">
<component></component>
</keep-alive>

<!-- Array (需要使用 v-bind,被包含的都会被缓存) -->
<keep-alive :include="['a', 'b']">
<component></component>
</keep-alive>

一句话就能回答的面试题

  1. css只在当前组件起作用
    答:在style标签中写入scoped即可 例如:<style scoped></style>

  2. v-if 和 v-show 区别
    答:v-if按照条件是否渲染,v-show是display的block或none;

  3. $route和$router的区别
    答:$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。而$router是“路由实例”对象包括了路由的跳转方法,钩子函数等。

  4. vue.js的两个核心是什么?
    答:数据驱动、组件系统

  5. vue几种常用的指令
    答:v-for 、 v-if 、v-bind、v-on、v-show、v-else

  6. vue常用的修饰符?
    答:.prevent: 提交事件不再重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素本身而不是子元素的时候会触发;.capture: 事件侦听,事件发生的时候会调用

  7. v-on 可以绑定多个方法吗?
    答:可以

  8. vue中 key 值的作用?
    答:当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用主要是为了高效的更新虚拟DOM。

  9. 什么是vue的计算属性?
    答:在模板中放入太多的逻辑会让模板过重且难以维护,在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式。好处:①使得数据处理结构清晰;②依赖于数据,数据更新,处理结果自动更新;③计算属性内部this指向vm实例;④在template调用时,直接写计算属性名即可;⑤常用的是getter方法,获取数据,也可以使用set方法改变数据;⑥相较于methods,不管依赖的数据变不变,methods都会重新计算,但是依赖数据不变的时候computed从缓存中获取,不会重新计算。

  10. vue等单页面应用及其优缺点
    答:优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
    缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。

  11. 怎么定义 vue-router 的动态路由? 怎么获取传过来的值
    答:在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 router 对象的 params.id 获取。