iOS开发之Link Map File

概述

iOS App编译之后生成的文件主要包含两部分:资源文件、可执行文件。有时候可能需要分析该可执行文件的组成,这时候可以使用Xcode生成的Link Map File。

Link Map File在Xcode的默认选项中是不生成的,需要手动打开。具体方法如下:
Xcode->Build Setting->Linking->Write Link Map File设置为YES,并在Path to Link Map File中设置存储路径。

编译后,到如下目录找到该文本文件:
~/Library/Developer/Xcode/DerivedData/xxx/Build/Intermediates/xxx.build/Debug-iphoneos/xxx.build/
文件名如下:
项目名-LinkMap-平台名.txt

整个Link Map File文件分为若干section,每个section描述一类信息,具体来说:

  1. 首先是目标文件列表
    Object files:
    [ 1] /Users/xxx/Library/Developer/Xcode/DerivedData/xxx-fsjhasudsqcpnfanlqqibstjbhsb/Build/Intermediates/xxx.build/Debug-iphonesimulator/xxx.build/Objects-normal/i386/AppDelegate+BackgroundLoadService.o

    如果该.o文件是通过链接某个静态库导入的,也能看出来,如:
    [529] /Users/xxx/Library/Developer/Xcode/DerivedData/xxx-fsjhasudsqcpnfanlqqibstjbhsb/Build/Products/Debug-iphonesimulator/xxx.a(ThirdPartyHandler.o)

  2. 然后是一个段表,描述了编译生成的各个段在可执行文件中的偏移及大小,主要是包含指令的代码段(TEXT)和包含数据的数据段(DATA)
    Sections:
    Address Size Segment Section
    0x000038E0 0x01D1AD00 _TEXT text
    0x01D1E5E0 0x00002094 _TEXT symbol_stub
    0x01D20674 0x00002582 _TEXT stub_helper
    0x01D22BF6 0x000E476C _TEXT objc_methname
    0x01E07362 0x00015DCA _TEXT objc_classname
    0x01E1D12C 0x0001E4B1 _TEXT objc_methtype

  3. 最后是符号段,描述了每个符号的偏移地址、所在文件及符号名。符号所在的文件是通过使用目标文件列表中的文件编号表示的。
    Symbols:
    Address Size File Name
    0x000038E0 0x000000A0 [ 1] -[NSDictionary(valueForKeyWithOutNSNull) valueForKeyWithOutNSNull:]
    0x00003980 0x00000070 [ 1] -[NSDictionary(valueForKeyWithOutNSNull) valueForKeyWithReturnEmptyString:]
    0x000039F0 0x0000007E [ 1] -[NSDictionary(valueForKeyWithOutNSNull) intValueWithDefaultVaule:]
    0x00003A70 0x000002C0 [ 2] -[QYFocusScrollView initWithFrame:numberOfPage:]

总结

Link Map File本质上是链接器在链接时输出的一个文本文件,这个文件描述了App可执行文件大致的组成结构。在精简App包大小的时候,有时候需要了解具体的某个库或者某个函数在最终的可执行文件中所占的大小,这个时候Link Map File就派上了用场,它可以给优化提供方向。
其它平台上也有类似的工具,如Windows平台上的dumpbin命令,Linux平台上也有相应的命令。其实无论Windows、Linux还是iOS,其二进制格式都是类似的,无非是先带一个特有的header,随后加上一系列的sections,代码和数据分开存储。详细可以参考《程序员的自我修养》、《Links and Loaders》。
说句题外话,其实有时候了解一些单纯代码之外的东西,能让自己对程序及计算机的理解进一步加深,当时在学校的时候,看了《程序员的自我修养》,现在细节基本上都忘了,但是对各平台可执行文件格式的关系的印象还是很深,Linux下使用的ELF文件和Windows下使用的PE文件都是从Unix系统的COFF文件格式演化而来,无非是增加了一些平台特有的东西,但是大体的结构是相同的,iOS平台没有特别研究,但如果知道iOS平台来历的话,想来应该也差不了太多。
了解一些可执行文件的知识,不仅对优化二进制可执行文件方面有帮助,更重要的是能让你明白了链接器在生成可执行文件的过程中做了什么事情,比如如何收集各静态库的symbol、遇到没有resolve的symbol如何处理;动态链接是怎样实现的;做可执行文件的二进制重排为什么能提高程序的加载效率等等。这些东西的细节不一定要很清楚(实际上99.99%的人终其职业生涯都不会涉及编译器和链接器方面的开发),但是知道概念很重要,这样才能在出现了比如诡异的问题,如奇怪链接错误之后不会无从下手。
最后用引用林语堂先生的一句话自勉:“只用一样东西,不明白它的道理,实在不高明”。当然,人的精力有限,具体深究哪方面,到什么程度,那就是另一个问题了。我的建议是,适当聚焦、最好能和当前工作或兴趣相关,深入到可以用自己的话描述整个过程的大体原理为准,这样才能很自然地将其纳入自己的知识体系。

iOS 9新特性之应用瘦身(App thinning)

概述

针对app体积越来越大的问题,iOS9引入了3种新技术:

  • 应用切片(App Slicing)
  • 资源按需加载(On Demand Resources)
  • Bitcode

    应用切片(App Slicing):

    现在的app为了兼容不同设备,同时包含了32 bit 和 64 bit 两个 slice;图片资源方面,1x,2x、3x的资源一应俱全,但是对具体用户来讲,其设备是特定的,为什么要让只需要3x分辨率的设备去下载2x和1x资源?为什么要让只能运行32bit的设备下载64bit的Architecture?
    App Slicing的直观理解,就是在开发者上传的完整包中,根据设备情况切取最小可用的单元,并部署到实际设备,如下图所示:

iOS9之前的app的完整包的组成如下图所示,对具体设备而言,存在冗余内容:

以iPhone 6 Plus为例,下图说明了它实际需要的内容,除此之外的都是冗余的内容:

有了App Slices,开发者就可以根据设备添加资产标签,当用户从iTunes下载应用时,它将仅下载你的设备需要的资产。因为苹果已经将整个过程设计得非常简单,所以相信很多应用很快就会开始支持这项特性。

以前做资源的延迟下载,一般都是自己搭建server放置那些app以后用到的资源,然后在某个时机触发多线程下载,之后进行后续的处理。现在Apple用自己的server来实现这个功能, 不需要自己搭建server等繁琐的一套,只需要给资源打上tag,剩下的工作apple帮你完成。(XCode7有相应的支持,个人理解就是通过tag,进行分组下载)

应用场景:
• 初始资源的延迟加载。app有一些资源是主要功能要用到的,但在启动时并不需要。将这些资源标记为“初始需要”。操作系统在app启动时会自动下载这些资源。例如,图片编辑app有许多不常用的滤镜。
• app资源的延迟加载。app有一些只在特定情景下使用的资源,当应用可能要进入这些场景时,会请求这些资源。例如,在一个有很多关卡的游戏中,用户只需要当前关卡和下一关卡的资源。
• 不常用资源的远程存储。app有一些很少使用的资源,当需要这些资源时会去请求它们。例如,当app第一次打开时会展示一个教程,而这个教程之后就可能不会在用到。app在第一次启动时请求教程的资源,这之后只在需要展示教程或者添加了新功能才去请求该资源。
• 应用内购买资源的远程存储。app提供包含额外资源的应用内购买。app会在启动完成后请求已购买模块的资源。例如,用户在一个键盘app内购买的表情包。应用程序会在启动完成后请求表情包的资源。
• 第一次启动时必需资源的加载。app有一些资源只在第一次启动时需要,之后的启动不再需要。例如,app有一个只在第一次启动时展示的教程。

App包含一系列的资源,并且根据程序的执行流程,可以分级,如下图:

资源按需加载的思想是,最初安装到设备上的程序只包含必要的资源,非必要的资源放在云端,如下图:

按需加载资源的流程:

如何使用该特性:



tag的分类:
Initial install tags:该tag的资源与app同时下载
Prefetch tag order:app安装后开始下载
Dowloaded only on demand:需要显式请求、并且无cache时才开始下载
涉及到的类:
NSBundleResourceRequest
NSBundle

使用流程:
• 分配NSBundleResourceRequest,根据tag发出资源请求;
• 开始下载,监测进度与下载过程中可能出现的错误;
• 设置资源在本地保存的优先级;
• 使用资源;
• 告知系统不再需要资源,以便系统在必要的时候释放空间。

何时开始发起下载:
不幸的是,在这方面系统能做的不多,大体上需要开发者根据程序逻辑自己决定,系统提供了以下有限的几个影响资源下载的方式:
• 在下载开始前设置下载优先级;
• 监控下载过程。其一,开发者可以根据下载情况动态调整优先级,其二,可以通过UI将进度反馈给用户

几个要注意的点:

处理空间不足的警告
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(lowDiskSpace:) name:NSBundleResourceRequestLowDiskSpaceNotification object:nil];

注意应对tag所标示的资源的各种可能的状态

好处:
• 提高存储空间的应用效率:比如多级游戏,用户需要的通常都是他们当前的级数以及下一级。ODR意味着用户可以下载他们需要的几级游戏。随着你的级数不断增加,应用再下载其他级数,并将用户成功过关的级数给删掉;
• 对于延迟下载的资料,不用搭建自己的服务器,这点对于个人开发者更重要。

不足:
• 需要自己控制下载时机,处理下载过程中出现的各种情况;
• 消耗更多移动流量;
• 在使用app的时候,如果触发了较多的资源下载,用户需要等待,甚至block住。比如你坐十几个小时飞机,期间一直在玩游戏,不小心你就一路过关斩将,但是因为没有下载所以不能继续玩下去,这种时候会非常无奈;
• 网络的不可控性,特别是app store的访问速度。

Bitcode
• 开发者上传应用程序时提交的是中间码,App Slicing可以根据用户需求,来判断是需要32位还是64位。
• 也就是说,在用户下载应用之前,App Store在自动编译应用程序。这样,即使开发者没有给代码添加标签,应用也能够执行App Slicing的部分功能,仅下载设备需要的32或64位代码。
• 也意味着如果苹果完善编译器提高代码效率,用户下载应用时苹果进行的完善会自动整合进去。

iOS 9新特性之分屏多任务

概述

分屏多任务也许是iOS9最大的变化之一(目前只支持iPad Air 2),从具体形态上来说,其有3种表现形式:

  • 滑动覆盖 (Slide Over):临时调出另一个程序,如写文档的过程中,临时弹出聊天窗口,短暂划出后关闭
  • 分割视图 (Split View):真正的多任务,两个程序同时激活,两边的任务都可以进行实时操作,并且分屏比例可以调节
  • 画中画模式 (Picture in Picture):视频在一个悬浮小窗口中播放,类似于以前的弹窗播放

    背景知识

    屏幕适配经历了代码计算frame -> auto resizing(父控件和子控件的关系ios6) -> auto layout(任何控件都可以产生关系ios7) 的演变过程,为了解决适配更丰富屏幕的问题,在iOS8中,引入了一个全新的概念,这就是Size Class,不再根据设备屏幕的具体尺寸来进行区分,也淡化了横屏和竖屏的概念,而是通过它们的感官表现,在每个方向将其分为普通 (Regular) 和紧密(Compact) 两个类别,主流设备在不同方向下的分类如下:
    iPhone4S,iPhone5/5s,iPhone6
    竖屏:(w:Compact h:Regular)
    横屏:(w:Compact h:Compact)
    iPhone6 Plus
    竖屏:(w:Compact h:Regular)
    横屏:(w:Regular h:Compact)
    iPad
    竖屏:(w:Regular h:Regular)
    横屏:(w:Regular h:Regular)
    上述可以总结为下图:

    在iPad上,iOS 9引入分割视图以后,可以通过调节分割视图的分割条来调节各子区域的大小。设备在横屏或竖屏的情况下,分割条拖到不同的位置,各区域的Size Class定义如下图所示:

    引入Size Class最大的好处:
    1、所有不同设备屏幕统一;
    2、所有改变Window大小的行为统一(滑动分割窗口、旋转设备方向都统一为Size Class的变化);
    在上述思想下,
  • willRotateToInterfaceOrientation:duration:
  • willAnimateRotationToInterfaceOrientation:duration:
  • didRotateFromInterfaceOrientation:
  • shouldAutomaticallyForwardRotationMethods
    等与旋转有关的方法都将在8.0之后废弃,全部统一到-willTransitionToTraitCollection:withTransitionCoordinator: 和 -viewWillTransitionToSize:withTransitionCoordinator:,今后在程序代码层面将不再有旋转的概念,旋转与设备尺寸变化及分割窗口尺寸变化统一归结为Size Class的变化,这也是Size Class的设计哲学。

    滑动覆盖 (Slide Over)、分割视图 (Split View)的适配方法

    官方文档:“Most apps should adopt Slide Over and Split View”
    即在iOS9时代,滑动覆盖和分割视图是很基本的特性,多数app需要支持。
    下图显示了旋转屏幕的过程中, ViewController收到的一系列的消息

    注意其中的Size Class变化对应的消息。

开发者根据当前工作的 UIViewController 的 traitCollection 属性来设置合适的布局,并且在 -willTransitionToTraitCollection:withTransitionCoordinator: 和 -viewWillTransitionToSize:withTransitionCoordinator: 被调用时对 UI 布局做出正确的响应。如下图所示:

需要注意的问题

  • 在分屏的情况下UIScreen.bounds和UIWindow.bounds可能不同,后者可能是前者的一半或者三分之一。
  • iPad的Size Class可能会发生变化。在iOS 8时代,iPad不论横屏还是竖屏,长宽都是Regular的,但是在iOS 9引入分屏之后,就会有多种情况(见上图)。总之,iOS 9时代不要对Size Class和Window Size做任何假定,因为你的app可能被作为从属app打开,你的app也可以作为主app打开其它从属app,并且在上述过程中用户还可以进行拖动,导致Size Class有各种组合,解决的办法是使你的app支持所有方向和相应方向对应的Size Class的组合。
  • 分屏情况下,用户移动分割条,系统回调delegate的applicationWillResignActive:方法,注意在这个过程中不要丢失相关信息,保证用户释放分隔条时,如果app窗口大小和原来一致,程序的状态(文本选择范围、滚动条位置等)和最初一致。
  • 分屏情况下,如果用户将分隔条从左侧滑动到最右侧,系统回调被完全覆盖的app的delegate的applicationDidEnterBackground:方法。

    画中画模式 (Picture in Picture)

    官方文档:“Picture in Picture (PiP) is for apps whose primary role is video playback. And PiP is best for playback of medium-to-long duration content”
    我的理解是PiP特性需要根据app所在的行业特点来决定是否应该支持,对于播放类的应用,最好都能支持。

在iOS9中,下面3个框架支持画中画模式:

具体来说,根据下面的步骤来适配:
1、使用iOS 9 SDK;
2、在工程配置中,打开后台模式;

3、将AudioSession的Catogory 设置为合适的选项,比如 AVAudioSessionCategoryPlayback;
4、如果使用的是AVPlayerViewController来播放视频,上述配置后进入全屏播放就支持画中画模式;
5、如果使用AVPlayerLayer来构造自定义播放界面,需要使用AVPlayerLayer 来创建一个 AVPictureInPictureController:


AVPictureInPictureController提供了判断是否支持画中画模式、进入画中画模式、退出画中画模式的API:

此外,AVPictureInPictureControllerDelegate
还提供了一些回调方法来获取画中画的执行情况,如:
pictureInPictureControllerDidStartPictureInPicture
pictureInPictureControllerFailedToStartPictureInPicture
pictureInPictureControllerWillStopPictureInPicture
可以根据这些回调通知做一些业务性的功能。

EGOTableViewPullRefresh解析

概述

列表视图的下拉刷新是个很常见的功能,github上有很多开源的解决方案,EGOTableViewPullRefresh是其中使用很多的一个,下面来分析它的实现。

UI组成

整体效果图如下图所示:
整体效果图
可以分为两个部分,列表视图部分和刷新指示视图部分。
从UI结构上讲,没什么特别的,就是普通的subView关系:

if (_refreshHeaderView == ni/Users/klarm/Documents/kkm/经典禁书/-8964日天安门事件.txtl) {    
        EGORefreshTableHeaderView *view = [[EGORefreshTableHeaderView alloc] initWithFrame:CGRectMake(0.0f, 0.0f - self.tableView.bounds.size.height, self.view.frame.size.width, self.tableView.bounds.size.height)];
        view.delegate = self;
        [self.tableView addSubview:view];
        _refreshHeaderView = view;
        [view release];
    }

但怎么样实现在正常情况下,刷新指示视图不显示,在列表顶部下拉才显示的效果呢?关键在于坐标为负值的frame。从上面的代码来看,整个刷新指示视图在列表视图的正上方,大小和列表视图一样。继续看代码:

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, frame.size.height - 30.0f, self.frame.size.width, 20.0f)];

label = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, frame.size.height - 48.0f, self.frame.size.width, 20.0f)];

layer.frame = CGRectMake(25.0f, frame.size.height - 65.0f, 30.0f, 55.0f);

发现刷新指示视图中,真正的内容部分,即显示提示文字和上次更新时间的lable、显示箭头的layer在刷新指示视图的下方,如下图所示:
整体示意图
那下拉的过程中,可视区域外的刷新指示视图是怎么显示出来的呢,加一条log:NSLog(@”scrollView.contentOffset.y=%f”, scrollView.contentOffset.y);
发现在列表下拉的过程中,刚好拉倒列表顶部的时候,scrollView.contentOffset.y为0,继续往下拉,scrollView.contentOffset.y变为负值,相对于scroolView的frame在y轴方向上为负的子视图就显示出来了。

状态转换

代码里定义了几种状态

typedef enum{
    EGOOPullRefreshPulling = 0,
    EGOOPullRefreshNormal,
    EGOOPullRefreshLoading,    
} EGOPullRefreshState;

跟了一下,一个完整的刷新过程中的状态变化为normal-》pulling-》loading-》normal,如下图:
状态转换示意图
这个也好理解,正常状态是normal,下来满足一定条件(下拉偏移量大于65),进入pulling状态,触发刷新,进入loading状态,loading结束后,恢复normal状态。这里的关键是触发这些状态转换过程的时机:

  • normal状态-》pulling状态 开始滚动并且偏移量大于一定的负值,具体来讲用的是scrollViewDidScroll这个时机
  • pulling状态-》loading状态 结束滚动并且偏移量大于一定的负值,具体来讲用的是scrollViewDidEndDragging这个时机
  • loading状态-》normal状态 业务代码中刷新结束后,重置状态

每种状态下,需要改变刷新指示视图中文本及动画的状态,这个不赘述。

总结

总体来讲EGOTableViewPullRefresh的代码还是挺简单的,关键在于上述两点:滚动视图中,frame坐标为负值的运用;利用滚动事件的回调,处理好状态转换。

SDWebImage解析

概述

简单来讲,SDWebImage是一个用来异步加载网络图片的库,在使用者的角度,设置图片的url以及占位图片后,其他的就不用做了,SDWebImage会自动开始下载图片,在图片下载完成前在相应的位置显示占位图片,在图片下载完成之后将占位图片替换为实际的图片。

实现形式

通过为系统预定义的控件添加类别的方式来实现,具体来说,为以下控件做了扩展:

  • UIImage
  • UIButton
  • UIImageView
  • MKAnnotationView

代码结构

主要由下面几个类组成:

  • SDWebImageDownloader 图片下载器
  • SDImageCache 实现缓存的地方
  • SDWebImageManager 大内总管,扮演协调downloader和cache之间关系的角色

主要类的设计

SDWebImageManager主要成员:

  • SDWebImageDownloader *imageDownloader管理下载
  • SDImageCache *imageCache管理缓存
  • id delegate回调通知

SDWebImageDownloader主要成员:

  • NSOperationQueue *downloadQueue存放下载任务队列
    (SDWebImageDownloaderOperation派生自NSOperation)

SDImageCache主要成员:

  • NSCache *memCache存放内存cache
  • NSString *diskCachePath存放磁盘cache的路径
  • dispatch_queue_t ioQueue表示io队列

几个关键点

  • 单例的实现
    SDWebImage中SDWebImageManager、SDWebImageManager和SDImageCache都是以单例的形式存在,实现上用了类似的方法:
    1
    2
    3
    4
    5
    6
    7
    8
    + (id)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
    instance = [self new];
    });
    return instance;
    }

简单来说,就是通过dispatch_once来实现单例。这种方法利用了dispatch_once可以保证block里的代码只执行一次,并且是线程安全的特性。可以参考这里。

  • 磁盘cache的实现
    这里使用了UIImagePNGRepresentation和UIImageJPEGRepresentation读取图片的data,保存在NSData中,然后通过createFileAtPath保存在磁盘中。

相关链接

EGOImageLoading实现的思路大同小异,不同的是EGOImageLoading不是通过类别扩展标准控件,而是通过直接派生自UIButton、UIImageView实现的。