【Objective-C环境】Objective-C编译优化

编译过程

一般可以将编程语言分为两种,编译语言和直译式语言。

像C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。

像JavaScript,Python都是直译式语言。直译式语言不需要经过编译的过程,而是在执行的时候通过一个中间的解释器将代码解释为CPU可以执行的代码。所以,较编译语言来说,直译式语言效率低一些,但是编写的更灵活。

Objective和Swift的编译依赖于Clang + LLVM,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。1

Clang-->LLVM Optimizer-->LLVM Code Generator

编译器处理过程中,将 helloworld.c 当做输入文件,并生成一个可执行文件 a.out。这个过程有多个步骤/阶段。我们需要做的就是正确的执行它们。2 3

预处理4
* 符号化 (Tokenization)
* 宏定义的展开
* #include 的展开

语法和语义分析5
* 将符号化后的内容转化为一棵解析树 (parse tree)
* 解析树做语义分析
* 输出一棵抽象语法树(Abstract Syntax Tree* (AST))

生成代码和优化

  • 将 AST 转换为更低级的中间码 (LLVM IR)
  • 对生成的中间码做优化
  • 生成特定目标代码
  • 输出汇编代码

汇编器

  • 将汇编代码转换为目标对象文件。

链接器

  • 将多个目标对象文件合并为一个可执行文件 (或者一个动态库)

解密 Build 日志

为了了解 Xcode build 过程的内部工作原理,我们首先把突破口瞄准完整的 log 文件上。打开Xcode的 Log Navigator ,从列表中选择一个 Build ,Xcode 会将 log 文件很完美的展现出来。

以一测试demo为例,注意观察输出的 log 信息,首先会发现 log 信息被分为不同的几大块,它们与我们工程中的targets相互对应着:

Build target Pods-SSZipArchive
...
Build target Makefile-openssl
...
Build target Pods-AFNetworking
...
Build target crypto
...
Build target Pods
...
Build target ssl
...
Build target objcio

本文涉及到的工程有几个依赖项:其中 AFNetworking 和 SSZipArchive 包含在 Pods 中,而 OpenSSL 则以子工程的形式包含在工程中。

针对工程中的每个 target,Xcode 都会执行一系列的操作,将相关的源码,根据所选定的平台,转换为机器可读的二进制文件。第一个任务是处理一个预编译头文件(为了增强 log 信息的可读性,我省略了许多细节):

(1) ProcessPCH /…/Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c com.apple.compilers.llvm.clang.1_0.compiler
(2) cd /…/Dev/objcio/Pods
setenv LANG en_US.US-ASCII
setenv PATH “…”
(3) /…/Xcode.app/…/clang
(4) -x objective-c-header
(5) -arch armv7
… configuration and warning flags …
(6) -DDEBUG=1 -DCOCOAPODS=1
… include paths and more …
(7) -c
(8) /…/Pods-SSZipArchive-prefix.pch
(9) -o /…/Pods-SSZipArchive-prefix.pch.pch

在 build 处理过程中,每个任务都会出现类似上面的这些 log 信息,我们就通过上面的 log 信息进一步了解详情。
1. 类似上面的每个 log 信息块都会利用一行 log 信息来描述相关的任务作为起点。
2. 接着输出带缩进的3行 log 信息,列出了该任务执行的语句。此处,工作目录发生了改变,并对 LANG 和 PATH 环境变量进行设置。
3. 这里是发生奇迹的地方。为了处理一个.pch文件,调用了 clang,并附带了许多可选项。下面跟着输出的 log 信息显示了完整的调用过程,以及所有的参数。我们看看其中的几个参数…
4. -x 标示符用来指定所使用的语言,此处是 objective-c-header。
5. 目标架构指定为 armv7。
6. 暗示 #defines 的内容已经被添加了。
7. -c 标示符用来告诉 clang 具体该如何做。-c 表示:运行预处理器、词法分析器、类型检查、LLVM 的生成和优化,以及 target 指定汇编代码的生成阶段,最后,运行汇编器以产出一个.o的目标文件。
8. 输入文件。
9. 输出文件。

针对这个 target ,虽然只有一个 .pch 文件,但实际上这里对 objective-c-header 文件的处理有两个任务。通过观察具体输出的 log 信息,我们可以知道详情:

ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c ...
ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7s objective-c ...

从上面的 log 信息中,可以明显的看出 target 针对两种架构做了 build – armv7 和 armv7s – 因此 clang 对文件做了两次处理,每次针对一种架构。

在处理预编译头文件之后,可以看到针对 SSZipArchive target 有另外的几个任务类型。

CompileC ... 
Libtool ... 
CreateUniversalBinary ...

顾名思义:CompileC 用来编译 .m 和 .c 文件,Libtool 用来从目标文件中构建 library,而 CreateUniversalBinary 则将上一阶段产生的两个 .a 文件(每个文件对应一种架构)合并为一个通用的二进制文件,这样就能同时在 armv7 和 armv7s 上面运行。

接着,在工程中其它一些依赖项也会发生于此类似的步骤。AFNetworking 被编译之后,会与 SSZipArchive 进行链接,以当做 pod library。OpenSSL 编译之后,会接着处理 crypto 和 ssl target。

当所有的依赖项都 build 完成之后,就轮到我们程序的 target 了。Build 该 target 时,输出的 log 信息会包含一些非常有价值,并且之前没有出现过的内容:

PhaseScriptExecution ...
DataModelVersionCompile ...
Ld ...
GenerateDSYMFile ...
CopyStringsFile ...
CpResource ...
CopyPNGFile ...
CompileAssetCatalog ...
ProcessInfoPlistFile ...
ProcessProductPackaging /.../some-hash.mobileprovision ...
ProcessProductPackaging objcio/objcio.entitlements ...
CodeSign ...

在上面的任务列表中,根据名称不能区分的唯一任务可能就是 Ld,Ld 是一个 linker 工具的名称,与 libtool 非常相似。实际上,libtool也是简单的调用 ld 和 lipo。’ld’被用来构建可执行文件,而libtool则用来构建 library 文件。

静态库架构

可以通过如下命令来查看这个静态库所支持的所有架构。i386 是供模拟器使用的。
lipo -info ../…./XXXX.a
如果通过上述命令,查看得到这个静态库确实不支持i386架构的话,只能是重新编译这个库

合并iPhone模拟器和真机通用的静态类库:
lipo -create XXXX_V7.a XXXX_V7s.a -output XXXX_all.a 合成他们成为新的.a文件。

插入脚本

通常,如果使用CocoaPod来管理三方库,那么Build Phase里会执行以下脚本([CP]开头的,就是CocoaPod插入的脚本):

- Check Pods Manifest.lock,用来检查cocoapod管理的三方库是否需要更新

- Embed Pods Framework,运行脚本来链接三方库的静态/动态库

- Copy Pods Resources,运行脚本来拷贝三方库的资源文件

提高项目编译速度

编译速度衡量方法:

对于Xcode 8:

  1. 关闭Xcode,在终端执行以下命令:

defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

  1. 重启XCode,编译项目成功后可以在Xcode顶部看到编译总时间。

代码层面的优化

  1. forward declaration
    所谓forward declaration,就是@class CLASSNAME,而不是#import CLASSNAME.h。这样,编译器能大大提高#import的替换速度。

  2. 打包常用工具类(*.a静态库/framework)
    打包后,这部分代码在编译时不需要重新编译。

  3. 常用头文件放到预编译文件(*.pch)里
    Xcode里的.pch是预编译文件,它的内容在执行编译之前就已经被预先编译,并且引入到每一个.m文件里了。

编译器选项优化

  1. Debug模式下,不生成*.dysm文件
    dysm存储了调试信息,在Debug模式下,我们可以借助XCode和LLDB进行调试。所以,不需要生成额外的dsym文件来降低编译速度。
    Build Options section - Debug Information Format: Debug > Dwarf with dSYM File

  2. Debug开启Build Active Architecture Only
    在XCode -> Build Settings -> Build Active Architecture Only 改为YES。这样做,可以只编译当前的版本,比如arm7/arm64等等,记得只开启Debug模式。

  3. Debug模式下,关闭编译器优化
    Optimization Level: Debug -> None[-O0]

编译器指令

编译器变量

_cmd:代表当前方法的selector。例如
NSStringFromSelector(_cmd);

id target = [[targetClass alloc] init];
SEL action = NSSelectorFromString(actionString);
[target performSelector:action withObject:params];

初始化方法声明

#import <Foundation/Foundation.h>
#import <UIKit/UIViewController.h>
#import <UIKit/UIKitDefines.h>
#import <UIKit/UIActivity.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^UIActivityViewControllerCompletionHandler)(UIActivityType __nullable activityType, BOOL completed);
typedef void (^UIActivityViewControllerCompletionWithItemsHandler)(UIActivityType __nullable activityType, BOOL completed, NSArray * __nullable returnedItems, NSError * __nullable activityError);

NS_CLASS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED @interface UIActivityViewController : UIViewController

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
- (instancetype)initWithActivityItems:(NSArray *)activityItems applicationActivities:(nullable NSArray<__kindof UIActivity *> *)applicationActivities NS_DESIGNATED_INITIALIZER;

@property(nullable, nonatomic, copy) UIActivityViewControllerCompletionHandler completionHandler NS_DEPRECATED_IOS(6_0, 8_0, "Use completionWithItemsHandler instead.");  // set to nil after call
@property(nullable, nonatomic, copy) UIActivityViewControllerCompletionWithItemsHandler completionWithItemsHandler NS_AVAILABLE_IOS(8_0); // set to nil after call

@property(nullable, nonatomic, copy) NSArray<UIActivityType> *excludedActivityTypes; // default is nil. activity types listed will not be displayed

@end

NS_ASSUME_NONNULL_END

编译器指令attribute 6

attribtue 是一个高级的的编译器指令,它允许开发者指定更更多的编译检查和一些高级的编译期优化。
分为三种:
函数属性 (Function Attribute)
类型属性 (Variable Attribute)
变量属性 (Type Attribute)

语法结构:

attribute 语法格式为:attribute ((attribute-list))
放在声明分号“;”前面。

比如,在三方库中最常见的,声明一个属性或者方法在当前版本弃用了

@property (strong,nonatomic)CLASSNAME * property __deprecated;

使用场景很多,例如:

//弃用API
#define __deprecated    __attribute__((deprecated))

//带描述信息的弃用
#define __deprecated_msg(_msg) __attribute__((deprecated(_msg)))

//遇到__unavailable的变量/方法,编译器直接抛出Error
#define __unavailable   __attribute__((unavailable))

//告诉编译器,即使这个变量/方法 没被使用,也不要抛出警告
#define __unused    __attribute__((unused))

//和__unused相反
#define __used      __attribute__((used))

//如果不使用方法的返回值,进行警告
#define __result_use_check __attribute__((__warn_unused_result__))

//OC方法在Swift中不可用
#define __swift_unavailable(_msg)   __attribute__((__availability__(swift, unavailable, message=_msg)))

Clang警告处理

  • 明确编译器错误
#error Whoa, buddy, you need to check for zero here!
return (dividend / divisor);
  • 添加warning
#warning todo ...
  • 消除warning
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
...
#pragma clang diagnostic pop

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations”
            ...
#pragma clang diagnostic pop
  • 提示deprecated
@property (strong, nonatomic) UIColor *color __attribute__((deprecated("Customize the bezelView color instead.")));
- (void)show:(BOOL)animated __attribute__((deprecated("Use showAnimated: instead.")));
  • 忽略没用使用变量的编译警告
NSString *foo;
#pragma unused (foo)

@compatibility_alias

@compatibility_alias: Allows existing classes to be aliased by a different name. KVO works perfectly with it.

typedef UserModel FRWUserModel;
/** KVO doesn’t work perfectly with it. for example:
[obj valueForKey:@“userModel"] // throws NSUnknownKeyException ??
 */
// Allows code to just use UICollectionView as if it would be available on iOS SDK 5.
// http://developer.apple.    com/legacy/mac/library/#documentation/DeveloperTools/gcc-3.   3/gcc/compatibility_005falias.html
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 60000
@compatibility_alias UICollectionViewController PSTCollectionViewController;
@compatibility_alias UICollectionView PSTCollectionView;
@compatibility_alias UICollectionReusableView PSTCollectionReusableView;
@compatibility_alias UICollectionViewCell PSTCollectionViewCell;
@compatibility_alias UICollectionViewLayout PSTCollectionViewLayout;
@compatibility_alias UICollectionViewFlowLayout PSTCollectionViewFlowLayout;
@compatibility_alias UICollectionViewLayoutAttributes     PSTCollectionViewLayoutAttributes;
@protocol UICollectionViewDataSource <PSTCollectionViewDataSource>]]> @end
@protocol UICollectionViewDelegate <PSTCollectionViewDelegate>]]> @end
#endif

attribute((cleanup(…))) 作用域结束后调用 @onExit

/**
 __attribute__((cleanup(...))),用于修饰一个变量,在它的作用域结束时可以自动执行一个指定的方法
 */

// 指定一个cleanup方法,注意入参是所修饰变量的地址,类型要一样
// 对于指向objc对象的指针(id *),如果不强制声明__strong默认是__autoreleasing,造成类型不匹配
/** 万能的Reactive Cocoa中神奇的@onExit方法
 */
// void(^block)(void)的指针是void(^*block)(void)
static void blockCleanUp(__strong void(^*block)(void)) {
    (*block)();
}

// Details about the choice of backing keyword:
//
// The use of @try/@catch/@finally can cause the compiler to suppress
// return-type warnings.
// The use of @autoreleasepool {} is not optimized away by the compiler,
// resulting in superfluous creation of autorelease pools.
//
// Since neither option is perfect, and with no other alternatives, the
// compromise is to use @autorelease in DEBUG builds to maintain compiler
// analysis, and to use @try/@catch otherwise to avoid insertion of unnecessary
// autorelease pools.
#if DEBUG
#define ext_keywordify autoreleasepool {}
#else
#define ext_keywordify try {} @catch (...) {}
#endif

#define onExit \
ext_keywordify __strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^

NSRecursiveLock *aLock = [[NSRecursiveLock alloc] init];
[aLock lock];
@onExit {
    NSLog(@"unlocked");
    [aLock unlock]; // 妈妈再也不用担心我忘写后半段了
};
// 这里
//    爱多少行
//           就多少行

自动更新版本号

version & build number keys分别代表app的用于市场推广和内部的版本号。

avgtool是自动递增版本号的命令行。

Xcode工程中的Info.plist代表版本号和build号的键值:

// Info.plist

CFBundleShortVersionString (Bundle versions string, short): version number
CFBundleVersion (Bundle version): build number

自动设置编译号步骤:7

  1. 在工程target的Build Settings中,设置Current Project Version 为选定的编译号,若是新建工程则设置为1。
  2. 在工程target的Build Settings中,设置 Versioning System 为 Apple Generic
  3. 确保Info.plist中存在CFBundleVersion (Bundle version)和CFBundleShortVersionString (Bundle versions string, short) keys。
  4. 在Buid Phases中新建一个Run Script,确保执行顺序放在Copy Bundle Resources之前,Shell命令文本框中输入xcrun agvtool next-version -all
  5. 下次编译/运行/打包时即可看到编译号的递增。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值