扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
本人觉得这个打包framework还是一个比较重要的功能,可以用来做一下事情:(1)封装功能模块,比如有比较成熟的功能模块封装成一个包,然后以后自己或其他同事用起来比较方便。(2)封装项目,有时候会遇到这个情况,就是一家公司找了两个开发公司做两个项目,然后要求他们的项目中的一个嵌套进另一个项目,此时也可以把呗嵌套的项目打包成framework放进去,这样比较方便。我们为什么需要框架(Framework)?要想用一种开发者友好的方式共享库是很麻烦的。你不仅仅需要包含库本身,还要加入所有的头文件,资源等等。苹果解决这个问题的方式是框架(framework)。基本上,这是含有固定结构并包含了引用该库时所必需的所有东西的文件夹。不幸的是,iOS禁止所有的动态库。同时,苹果也从Xcode中移除了创建静态iOS框架的功能。Xcode仍然可以支持创建框架的功能,重启这个功能,我们需要对Xcode做一些小小的改动。把代码封装在静态框架是被app store所允许的。尽管形式不同,本质上它仍然是一种静态库。框架(Framework)的类别大部分框架都是动态链接库的形式。因为只有苹果才能在iOS设备上安装动态库,所以我们无法创建这种类型的框架。静态链接库和动态库一样,只不过它是在编译时链接二进制代码,因此使用静态库不会有动态库那样的问题(即除了苹果谁也不能在iOS上使用动态库)。“伪”框架是通过破解Xcode的目标Bundle(使用某些脚本)来实现的。它在表面上以及使用时跟静态框架并无区别。“伪”框架项目的功能几乎和真实的框架项目没有区别(不是全部)。“嵌入”框架是静态框架的一个包装,以便Xcode能获取框架内的资源(图片、plist、nib等)。本次发布包括了创建静态框架和“伪”框架的模板,以及二者的“嵌入”框架。用哪一种模板?本次发布有两个模板,每个模板都有“强”“弱”两个类别。你可以选择最适合一种(或者两种都安装上)。最大的不同是Xcode不能创建“真”框架,除非你安装静态框架文件xcspec在Xcode中。这真是一个遗憾(这个文件是给项目使用的,而不是框架要用的)。简单说,你可以这样决定用哪一种模板:如果你不想修改Xcode,那么请使用“伪”框架版本如果你只是想共享二进制(不是项目),两种都可以如果你想把框架共享给不想修改Xcode的开发者,使用“伪”框架版本如果你想把框架共享给修改过Xcode的开发者,使用“真”框架版本如果你想把框架项目作为另一个项目的依赖(通过workspace或者子项目的方式),请使用“真”框架(或者“伪”框架,使用-framework——见后)如果你想在你的框架项目中加入其他静态库/框架,并把它们也链接到最终结果以便不需要单独添加到用户项目中,使用“伪”框架“伪”框架“伪”框架是破解的“reloacatable object file”(可重定位格式的目标文件, 保存着代码和数据,适合于和其他的目标文件连接到一起,用来创建一个可执行目标文件或者是一个可共享目标文件),它可以让Xcode编译出类似框架的东西——其实也是一个bundle。“伪框架”模板把整个过程分为几个步骤,用某些脚本去产生一个真正的静态框架(基于静态库而不是reloacatable object file)。而且,框架项目还是把它定义为wrapper.cfbundle类型,一种Xcode中的“二等公民”。因此它跟“真”静态框架一样可以正常工作,但当存在依赖关系时就有麻烦了。依赖问题如果不使用依赖,只是创建普通的项目是没有任何问题的。但是如果使用了项目依赖(比如在workspace中),Xcode就悲剧了。当你点击“Link Binary With Libraries”下方的’+’按钮时,“伪框架”无法显示在列表中。你可以从你的“伪”框架项目的Products下面将它手动拖入,但当你编辑你的主项目时,会出现警告:warning: skipping file '/somewhere/MyFramework.framework' (unexpectedfile type 'wrapper.cfbundle' in Frameworks Libraries build phase)并伴随“伪”框架中的链接错误。幸运的是,有个办法来解决它。你可以在”Other Linker Flags”中用”-framwork”开关手动告诉linker去使用你的框架进行链接:-framework MyFramework警告仍然存在,但起码能正确链接了。添加其他的库/框架如果你加入其他静态(不是动态)库/框架到你的“伪”框架项目中,它们将“链接”进你最终的二进制框架文件中。在“真”框架项目中,它们是纯引用,而不是链接。你可以在项目中仅仅包含头文件而不是静态库/框架本身的方式避免这种情况(以便编译通过)。“真”框架“真”框架各个方面都符合“真”的标准。它是真正的静态框架,正如使用苹果在从Xcode中去除的那个功能所创建的一样。为了能创建真正的静态框架项目,你必需在Xcode中安装一个xcspec文件。如果你发布一个“真”框架项目(而不是编译),希望去编译这个框架的人必需也安装xcspec文件(使用本次发布的安装脚本),以便Xcode能理解目标类型。注意:如果你正在发布完全编译的框架,而不是框架项目,最终用户并不需要安装任何东西。我已经提交一个报告给苹果,希望他们在Xcode中更新这个文件,但那需要一点时间.OpenRadarlink here加其他静态库/框架如果你加入其他静态(不是动态)库/框架到你的“真”框架项目,它们只会被引用,而不会象“伪”框架一样被链接到最终的二进制文件中。从早期版本升级如果你是从Mk6或者更早的版本升级,同时使用“真”静态框架,并且使用Xcode4.2.1以前的版本,请运行uninstall_legacy.sh以卸载早期用于Xcode的所有修正。然后再运行install.sh,重启Xcode。如果你使用Xcode4.3以后,只需要运行install.sh并重启Xcode。安装分别运行Real Framework目录或Fake Framework目录下的install.sh脚本进行安装(或者两个你都运行)。重启Xcode,你将在新项目向导的FrameworkLibrary下看到StaticiOS Framework(或者Fake Static iOS Framework)。卸载请运行unistall.sh脚本并重启Xcode。创建一个iOS框架项目创建新项目。项目类型选择FrameworkLibrary下的Static iOS Framework(或者Fake Static iOS Framework)。选择“包含单元测试”(可选的)。在target中加入类、资源等。凡是其他项目要使用的头文件,必需声明为public。进入target的Build Phases页,Copy Headers项,把需要public的头文件从Project或Private部分拖拽到Public部分。编译你的 iOS 框架选择指定target的scheme修改scheme的Run配置(可选)。Run配置默认使用Debug,但在准备部署的时候你可能想使用Release。编译框架(无论目标为iOS device和Simulator都会编译出相同的二进制,因此选谁都无所谓了)。从Products下选中你的framework,“show in Finder”。在build目录下有两个文件夹:(yourframework).framework and (your framework).embeddedframework.如果你的框架只有代码,没有资源(比如图片、脚本、xib、coredata的momd文件等),你可以把(yourframework).framework 分发给你的用户就行了。如果还包含有资源,你必需分发(your framework).embeddedframework给你的用户。为什么需要embedded framework?因为Xcode不会查找静态框架中的资源,如果你分发(your framework).framework, 则框架中的所有资源都不会显示,也不可用。一个embedded framework只是一个framework之外的附加的包,包括了这个框架的所有资源的符号链接。这样做的目的是让Xcode能够找到这些资源。使用iOS 框架iOS框架和常规的Mac OS动态框架差不多,只是它是静态链接的而已。在你的项目中使用一个框架,只需把它拖仅你的项目中。在包含头文件时,记住使用尖括号而不是双引号括住框架名称。例如,对于框架MyFramework:#import使用问题Headers Not Found如果Xcode找不到框架的头文件,你可能是忘记将它们声明为public了。参考“创建一个iOS框架项目”第5步。No Such Product Type如果你没有安装iOS Universal Framework在Xcode,并企图编译一个universal框架项目(对于“真”框架,不是“假”框架),这会导致下列错误:target specifies product type 'com.apple.product-type.framework.static',but there's no such product type for the 'iphonesimulator' platform为了编译“真”iOS静态框架,Xcode需要做一些改动,因此为了编译“真”静态框架项目,请在所有的开发环境中安装它(对于使用框架的用户不需要,只有要编译框架才需要)。The selected run destination is not valid for this action有时,Xcode出错并加载了错误的active设置。首先,请尝试重启Xcode。如果错误继续存在,Xcode产生了一个坏的项目(因为Xcode4的一个bug,任何类型的项目都会出现这个问题)。如果是这样,你需要创建一个新项目重来一遍。链接警告第一次编译框架target时,Xcdoe会在链接阶段报告找不到文件夹:ld: warning: directory not found for option'-L/Users/myself/Library/Developer/Xcode/DerivedData/MyFramework-ccahfoccjqiognaqraesrxdyqcne/Build/Products/Debug-iphoneos'此时,可以clean并重新编译target,警告会消除。Core Data momd not found对于框架项目和应用程序项目,Xcode会以不同的方式编译momd(托管对象模型文件)。Xcode会简单地在根目录创建.mom文件,而不会创建一个.momd目录(目录中包含VersionInfo.plist和.mom文件)。这意味着,当从一个embedded framework的model中实例化NSManagedObjectModel时,你必需使用.mom扩展名作为model的URL,而不是采用.momd扩展名。NSURL *modelURL = [[NSBundle mainBundle]URLForResource:@"MyModel" withExtension:@"mom"];Unknown class MyClass in Interface Builder file.由于静态框架采用静态链接,linker会剔除所有它认为无用的代码。不幸的是,linker不会检查xib文件,因此如果类是在xib中引用,而没有在O-C代码中引用,linker将从最终的可执行文件中删除类。这是linker的问题,不是框架的问题(当你编译一个静态库时也会发生这个问题)。苹果内置框架不会发生这个问题,因为他们是运行时动态加载的,存在于iOS设备固件中的动态库是不可能被删除的。有两个解决的办法:让框架的最终用户关闭linker的优化选项,通过在他们的项目的Other Linker Flags中添加-ObjC和-all_load。在框架的另一个类中加一个该类的代码引用。例如,假设你有个MyTextField类,被linker剔除了。假设你还有一个MyViewController,它在xib中使用了MyTextField,MyViewController并没有被剔除。你应该这样做:在MyTextField中:+ (void)forceLinkerLoad_ {}在MyViewController中:+(void) initialize { [MyTextField forceLinkerLoad_]; }他们仍然需要添加-ObjC到linker设置,但不需要强制all_load了。第2种方法需要你多做一点工作,但却让最终用户避免在使用你的框架时关闭linker优化(关闭linker优化会导致object文件膨胀)。unexpected file type 'wrapper.cfbundle' in Frameworks Libraries build phase这个问题发生在把“假”框架项目作为workspace的依赖,或者把它当作子项目时(“真”框架项目没有这个问题)。尽管这种框架项目产生了正确的静态框架,但Xcode只能从项目文件中看出这是一个bundle,因此它在检查依赖性时发出一个警告,并在linker阶段跳过它。你可以手动添加一个命令让linker在链接阶段能正确链接。在依赖你的静态框架的项目的OtherLinker Flags中加入:-framework MyFramework警告仍然存在, 但不会导致链接失败。Libraries being linked or not being linked into the finalframework很不幸, “真”框架和“假”框架模板在处理引入的静态库/框架的工作方式不同的。“真”框架模板采用正常的静态库生成步骤,不会链接其他静态库/框架到最终生产物中。“假”框架模板采用“欺骗”Xcode的手段,让它认为是在编译一个可重定位格式的目标文件,在链接阶段就如同编译一个可执行文件,把所有的静态代码文件链接到最终生成物中(尽管不会检查是否确实目标代码)。为了实现象“真”框架一样的效果,你可以只包含库/框架的头文件到你的项目中,而不需要包含库/框架本身。Unrecognized selector in (some class with a category method)如果你的静态库或静态框架包含了一个模块(只在类别代码中声明,没有类实现),linker会搞不清楚,并把代码从二进制文件中剔除。因为在最终生成的文件中没有这个方法,所以当调用这个类别中定义的方法时,会报一个“unrecognizedselector”异常。要解决这个,在包含这个类别的模块代码中加一个“假的”类。linker发现存在完整的O-C类,会将类别代码链接到模块。我写了一个头文件 LoadableCategory.h,以减轻这个工作量:#import "SomeConcreteClass+MyAdditions.h"#import "LoadableCategory.h" MAKE_CATEGORIES_LOADABLE(SomeConcreteClass_MyAdditions); @implementation SomeConcreteClass(MyAdditions)...@end在使用这个框架时,仍然还需要在Build Setting的Other Linker Flags中加入-ObjC。执行任何代码前单元测试崩溃如果你在Xcode4.3中创建静态框架(或库)target时,勾选了“withunit tests”,当你试图运行单元测试时,它会崩溃:Thread 1: EXC_BAD_ACCESS (code=2, address=0x0) 0 0x00000000 --- 15 dyldbootstrap:start(...)这是lldb中的一个bug。你可以用GDB来运行单元测试。编辑scheme,选择Test,在Info标签中将调试器Debugger从LLDB改为GDB。
作为一家“创意+整合+营销”的成都网站建设机构,我们在业内良好的客户口碑。创新互联建站提供从前期的网站品牌分析策划、网站设计、成都网站设计、做网站、创意表现、网页制作、系统开发以及后续网站营销运营等一系列服务,帮助企业打造创新的互联网品牌经营模式与有效的网络营销方法,创造更大的价值。
原文地址:
一、引言
iOS10系统是一个较有突破性的系统,其在Message,Notification等方面都开放了很多实用性的开发接口。本篇博客将主要探讨iOS10中新引入的SpeechFramework框架。有个这个框架,开发者可以十分容易的为自己的App添加语音识别功能,不需要再依赖于其他第三方的语音识别服务,并且,Apple的Siri应用的强大也证明了Apple的语音服务是足够强大的,不通过第三方,也大大增强了用户的安全性。
二、SpeechFramework框架中的重要类
SpeechFramework框架比较轻量级,其中的类并不十分冗杂,在学习SpeechFramework框架前,我们需要对其中类与类与类之间的关系有个大致的熟悉了解。
SFSpeechRecognizer:这个类是语音识别的操作类,用于语音识别用户权限的申请,语言环境的设置,语音模式的设置以及向Apple服务发送语音识别的请求。
SFSpeechRecognitionTask:这个类是语音识别服务请求任务类,每一个语音识别请求都可以抽象为一个SFSpeechRecognitionTask实例,其中SFSpeechRecognitionTaskDelegate协议中约定了许多请求任务过程中的监听方法。
SFSpeechRecognitionRequest:语音识别请求类,需要通过其子类来进行实例化。
SFSpeechURLRecognitionRequest:通过音频URL来创建语音识别请求。
SFSpeechAudioBufferRecognitionRequest:通过音频流来创建语音识别请求。
SFSpeechRecognitionResult:语音识别请求结果类。
SFTranscription:语音转换后的信息类。
SFTranscriptionSegment:语音转换中的音频节点类。
三、申请用户语音识别权限与进行语音识别请求
开发者若要在自己的App中使用语音识别功能,需要获取用户的同意。首先需要在工程的Info.plist文件中添加一个Privacy-Speech Recognition Usage Description键,其实需要对应一个String类型的值,这个值将会在系统获取权限的警告框中显示,Info.plist文件如下图所示:
使用SFSpeechRecognize类的requestAuthorization方法来进行用户权限的申请,用户的反馈结果会在这个方法的回调block中传入,如下:
//申请用户语音识别权限
[SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {
}];
SFSpeechRecognizerAuthorzationStatus枚举中定义了用户的反馈结果,如下:
typedef NS_ENUM(NSInteger, SFSpeechRecognizerAuthorizationStatus) {
//结果未知 用户尚未进行选择
SFSpeechRecognizerAuthorizationStatusNotDetermined,
//用户拒绝授权语音识别
SFSpeechRecognizerAuthorizationStatusDenied,
//设备不支持语音识别功能
SFSpeechRecognizerAuthorizationStatusRestricted,
//用户授权语音识别
SFSpeechRecognizerAuthorizationStatusAuthorized,
};
如果申请用户语音识别权限成功,开发者可以通过SFSpeechRecognizer操作类来进行语音识别请求,示例如下:
//创建语音识别操作类对象
SFSpeechRecognizer * rec = [[SFSpeechRecognizer alloc]init];
//通过一个音频路径创建音频识别请求
SFSpeechRecognitionRequest * request = [[SFSpeechURLRecognitionRequest alloc]initWithURL:[[NSBundle mainBundle] URLForResource:@"7011" withExtension:@"m4a"]];
//进行请求
[rec recognitionTaskWithRequest:request resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) {
//打印语音识别的结果字符串
NSLog(@"%@",result.bestTranscription.formattedString);
}];
四、深入SFSpeechRecognizer类
SFSpeechRecognizer类的主要作用是申请权限,配置参数与进行语音识别请求。其中比较重要的属性与方法如下:
//获取当前用户权限状态
+ (SFSpeechRecognizerAuthorizationStatus)authorizationStatus;
//申请语音识别用户权限
+ (void)requestAuthorization:(void(^)(SFSpeechRecognizerAuthorizationStatus status))handler;
//获取所支持的所有语言环境
+ (NSSetNSLocale * *)supportedLocales;
//初始化方法 需要注意 这个初始化方法将默认以设备当前的语言环境作为语音识别的语言环境
- (nullable instancetype)init;
//初始化方法 设置一个特定的语言环境
- (nullable instancetype)initWithLocale:(NSLocale *)locale NS_DESIGNATED_INITIALIZER;
//语音识别是否可用
@property (nonatomic, readonly, getter=isAvailable) BOOL available;
//语音识别操作类协议代理
@property (nonatomic, weak) idSFSpeechRecognizerDelegate delegate;
//设置语音识别的配置参数 需要注意 在每个语音识别请求中也有这样一个属性 这里设置将作为默认值
//如果SFSpeechRecognitionRequest对象中也进行了设置 则会覆盖这里的值
/*
typedef NS_ENUM(NSInteger, SFSpeechRecognitionTaskHint) {
SFSpeechRecognitionTaskHintUnspecified = 0, // 无定义
SFSpeechRecognitionTaskHintDictation = 1, // 正常的听写风格
SFSpeechRecognitionTaskHintSearch = 2, // 搜索风格
SFSpeechRecognitionTaskHintConfirmation = 3, // 短语风格
};
*/
@property (nonatomic) SFSpeechRecognitionTaskHint defaultTaskHint;
//使用回调Block的方式进行语音识别请求 请求结果会在Block中传入
- (SFSpeechRecognitionTask *)recognitionTaskWithRequest:(SFSpeechRecognitionRequest *)request
resultHandler:(void (^)(SFSpeechRecognitionResult * __nullable result, NSError * __nullable error))resultHandler;
//使用代理回调的方式进行语音识别请求
- (SFSpeechRecognitionTask *)recognitionTaskWithRequest:(SFSpeechRecognitionRequest *)request
delegate:(id SFSpeechRecognitionTaskDelegate)delegate;
//设置请求所占用的任务队列
@property (nonatomic, strong) NSOperationQueue *queue;
SFSpeechRecognizerDelegate协议中只约定了一个方法,如下:
//当语音识别操作可用性发生改变时会被调用
- (void)speechRecognizer:(SFSpeechRecognizer *)speechRecognizer availabilityDidChange:(BOOL)available;
通过Block回调的方式进行语音识别请求十分简单,如果使用代理回调的方式,开发者需要实现SFSpeechRecognitionTaskDelegate协议中的相关方法,如下:
//当开始检测音频源中的语音时首先调用此方法
- (void)speechRecognitionDidDetectSpeech:(SFSpeechRecognitionTask *)task;
//当识别出一条可用的信息后 会调用
/*
需要注意,apple的语音识别服务会根据提供的音频源识别出多个可能的结果 每有一条结果可用 都会调用此方法
*/
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didHypothesizeTranscription:(SFTranscription *)transcription;
//当识别完成所有可用的结果后调用
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishRecognition:(SFSpeechRecognitionResult *)recognitionResult;
//当不再接受音频输入时调用 即开始处理语音识别任务时调用
- (void)speechRecognitionTaskFinishedReadingAudio:(SFSpeechRecognitionTask *)task;
//当语音识别任务被取消时调用
- (void)speechRecognitionTaskWasCancelled:(SFSpeechRecognitionTask *)task;
//语音识别任务完成时被调用
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishSuccessfully:(BOOL)successfully;
SFSpeechRecognitionTask类中封装了属性和方法如下:
//此任务的当前状态
/*
typedef NS_ENUM(NSInteger, SFSpeechRecognitionTaskState) {
SFSpeechRecognitionTaskStateStarting = 0, // 任务开始
SFSpeechRecognitionTaskStateRunning = 1, // 任务正在运行
SFSpeechRecognitionTaskStateFinishing = 2, // 不在进行音频读入 即将返回识别结果
SFSpeechRecognitionTaskStateCanceling = 3, // 任务取消
SFSpeechRecognitionTaskStateCompleted = 4, // 所有结果返回完成
};
*/
@property (nonatomic, readonly) SFSpeechRecognitionTaskState state;
//音频输入是否完成
@property (nonatomic, readonly, getter=isFinishing) BOOL finishing;
//手动完成音频输入 不再接收音频
- (void)finish;
//任务是否被取消
@property (nonatomic, readonly, getter=isCancelled) BOOL cancelled;
//手动取消任务
- (void)cancel;
关于音频识别请求类,除了可以使用SFSpeechURLRecognitionRequest类来进行创建外,还可以使用SFSpeechAudioBufferRecognitionRequest类来进行创建:
@interface SFSpeechAudioBufferRecognitionRequest : SFSpeechRecognitionRequest
@property (nonatomic, readonly) AVAudioFormat *nativeAudioFormat;
//拼接音频流
- (void)appendAudioPCMBuffer:(AVAudioPCMBuffer *)audioPCMBuffer;
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer;
//完成输入
- (void)endAudio;
@end
五、语音识别结果类SFSpeechRecognitionResult
SFSpeechRecognitionResult类是语音识别结果的封装,其中包含了许多套平行的识别信息,其每一份识别信息都有可信度属性来描述其准确程度。SFSpeechRecognitionResult类中属性如下:
//识别到的多套语音转换信息数组 其会按照准确度进行排序
@property (nonatomic, readonly, copy) NSArraySFTranscription * *transcriptions;
//准确性最高的识别实例
@property (nonatomic, readonly, copy) SFTranscription *bestTranscription;
//是否已经完成 如果YES 则所有所有识别信息都已经获取完成
@property (nonatomic, readonly, getter=isFinal) BOOL final;
SFSpeechRecognitionResult类只是语音识别结果的一个封装,真正的识别信息定义在SFTranscription类中,SFTranscription类中属性如下:
//完整的语音识别准换后的文本信息字符串
@property (nonatomic, readonly, copy) NSString *formattedString;
//语音识别节点数组
@property (nonatomic, readonly, copy) NSArraySFTranscriptionSegment * *segments;
当对一句完整的话进行识别时,Apple的语音识别服务实际上会把这句语音拆分成若干个音频节点,每个节点可能为一个单词,SFTranscription类中的segments属性就存放这些节点。SFTranscriptionSegment类中定义的属性如下:
//当前节点识别后的文本信息
@property (nonatomic, readonly, copy) NSString *substring;
//当前节点识别后的文本信息在整体识别语句中的位置
@property (nonatomic, readonly) NSRange substringRange;
//当前节点的音频时间戳
@property (nonatomic, readonly) NSTimeInterval timestamp;
//当前节点音频的持续时间
@property (nonatomic, readonly) NSTimeInterval duration;
//可信度/准确度 0-1之间
@property (nonatomic, readonly) float confidence;
//关于此节点的其他可能的识别结果
@property (nonatomic, readonly) NSArrayNSString * *alternativeSubstrings;
温馨提示:SpeechFramework框架在模拟器上运行会出现异常情况,无法进行语音识别请求。会报出kAFAssistantErrorDomain的错误,还望有知道解决方案的朋友,给些建议,Thanks。
1 建立一个single view application工程,然后打开工程中的Main.storyboard,选中里面的唯一一个ViewController,点击菜单栏的Editor-embed in-navigation Controller(嵌入这个navigation controller只是为了测试需要,并不是必须的)。
文/OneV's Den这是我在今年 1 月 10 日@Swift 开发者大会上演讲的文字稿。相关的视频还在制作中,没有到现场的朋友可以通过这个文字稿了解到这个 session 的内容。虽然我的工作是程序员,但是最近半年其实我的主要干的事儿是养了一个小孩。 所以这半年来可以说没有积累到什么技术,反而是积累了不少养小孩的心得。 当知道了有这么次会议可以分享这半年来的心得的时候,我毫不犹豫地选定了主题。那就是如何打造一个让人愉快的小孩但考虑到这是一次开发者会议...当我把这个想法和题目提交给大会的时候,被残酷地拒绝了。考虑到我们是一次开发者大会,所以我需要找一些更合适的主题。其实如果你对自己的代码有感情的话,我们开发和维护的项目或者框架就如同自己的孩子一般这也是我所能找到的两者的共同点。所以,我将原来拟定的主题换了两个字:如何打造一个让人愉快的框架在正式开始前,我想先给大家分享一个故事。我们那儿的 iOS 开发小组里有一个叫做武田君的人,他的代码写得不错,做事也非常严谨,可以说是楷模般的员工。但是他有一个致命的弱点 -- 喜欢自己发明轮子。他出于本能地抗拒在代码中使用第三方框架,所以接到开发任务以后他一般都要花比其他小伙伴更多的时间才能完成。武田君其实在各个方面都有建树...比如网络请求 模型解析 导航效果 视图动画 ... 不过虽然造了很多轮子,但是代码的重用比较糟糕,耦合严重。在新项目中使用的话,只能复制粘贴,然后针对项目修修补补。因为承担的任务总是没有办法完成,他一直是项目 deadline 的决定者,在日本这种社会,压力可想而知。就在我这次回国之前,武田君来向我借了一本我本科时候最喜欢的书。就是这本:我有时候就想,到底是什么让一个开发者面临如此大的精神压力,我们有什么办法来缓解这种压力。在我们有限的开发生涯中,应该如何有效利用时间来做一些更有价值的事情。以上故事纯属虚构,如有雷同实属巧合使用框架在了解如何制作框架之前,先让我们看看如何使用框架。可以说,如果你想成为一个框架的提供者,首先你必须是一个优秀的使用者。在 iOS 开发的早期,使用框架其实并不是一件让人愉悦的事情。可能有几年经验的开发者都有这样的体会,那就是:忘不了那些年,被手动引用和.a文件所支配的恐惧其实恐惧源于未知,回想一下,当我们刚接触软件开发的时候,懵懵懂懂地引用了一个静态库,然后面对一排排编译器报错时候手足无措的绝望。但是当我们了解了静态库的话,我们就能克服这种恐惧了。什么是静态库 (Static Library)所谓静态库,或者说 .a 文件,就是一系列从源码编译的目标文件的集合。它是你的源码的实现所对应的二进制。配合上公共的 .h 文件,我们可以获取到 .a 中暴露的方法或者成员等。在最后编译 app 的时候 .a 将被链接到最终的可执行文件中,之后每次都随着 app 的可执行二进制文件一同加载,你不能控制加载的方式和时机,所以称为静态库。在 iOS 8 之前,iOS 只支持以静态库的方式来使用第三方的代码。什么是动态框架 (Dynamic Framework)与静态相对应的当然是动态。我们每天使用的 iOS 系统的框架是以 .framework 结尾的,它们就是动态框架。Framework 其实是一个 bundle,或者说是一个特殊的文件夹。系统的 framework 是存在于系统内部,而不会打包进 app 中。app 的启动的时候会检查所需要的动态框架是否已经加载。像 UIKit 之类的常用系统框架一般已经在内存中,就不需要再次加载,这可以保证 app 启动速度。相比静态库,framework 是自包含的,你不需要关心头文件位置等,使用起来很方便。Universal FrameworkiOS 8 之前也有一些第三方库提供 .framework 文件,但是它们实质上都是静态库,只不过通过一些方法进行了包装,相比传统的 .a 要好用一些。像是原来的 Dropbox 和 Facebook 等都使用这种方法来提供 SDK。不过因为已经脱离时代,所以在此略过不说。有兴趣和需要的朋友可以参看一下这里和这里。Library v.s. Framework对比静态库和动态框架,后者是有不少优势的。首先,静态库不能包含像 xib 文件,图片这样的资源文件,其他开发者必须将它们复制到 app 的 main bundle 中才能使用,维护和更新非常困难;而 framework 则可以将资源文件包含在自己的 bundle 中。 其次,静态库必须打包到二进制文件中,这在以前的 iOS 开发中不是很大的问题。但是随着 iOS 扩展(比如通知中心扩展或者 Action 扩展)开发的出现,你现在可能需要将同一个 .a 包含在 app 本体以及扩展的二进制文件中,这是不必要的重复。最后,静态库只能随应用 binary 一起加载,而动态框架加载到内存后就不需要再次加载,二次启动速度加快。另外,使用时也可以控制加载时机。动态框架有非常多的优点,但是遗憾的是以前 Apple 不允许第三方框架使用动态方式,而只有系统框架可以通过动态方式加载。
这个看你需求了,navigation 和 tabbar controller 是可以相互交叉的,你可以参考写你想做的app的类似线上app。
阿里妹导读:刚刚,阿里巴巴正式对外开源了基于 Apache 2.0 协议的协程开发框架 coobjc,开发者们可以在 Github 上自主下载。
coobjc是为iOS平台打造的开源协程开发框架,支持Objective-C和Swift,同时提供了cokit库为Foundation和UIKit中的部分API提供了 协程 化支持,本文将为大家详细介绍coobjc的设计理念及核心优势。
从2008年第一个iOS版本发布至今的11年时间里,iOS的异步编程方式发展缓慢。
基于 Block 的异步编程回调是目前 iOS 使用最广泛的异步编程方式,iOS 系统提供的 GCD 库让异步开发变得很简单方便,但是基于这种编程方式的缺点也有很多,主要有以下几点:
针对多线程以及尤其引发的各种崩溃和性能问题,我们制定了很多编程规范、进行了各种新人培训,尝试降低问题发生的概率,但是问题依然很严峻,多线程引发的问题占比并没有明显的下降,异步编程本来就是很复杂的事情,单靠规范和培训是难以从根本上解决问题的,需要有更加好的编程方式来解决。
上述问题在很多系统和语言开发中都可能会碰到,解决问题的标准方式就是使用协程,C#、Kotlin、Python、Javascript 等热门语言均支持协程极其相关语法,使用这些语言的开发者可以很方便的使用协程及相关功能进行异步编程。
2017 年的 C++ 标准开始支持协程,Swift5 中也包含了协程相关的标准,从现在的发展趋势看基于协程的全新的异步编程方式,是我们解决现有异步编程问题的有效的方式,但是苹果基本已经不会升级 Objective-C 了,因此使用Objective-C的开发者是无法使用官方的协程能力的,而最新 Swift 的发布和推广也还需要时日,为了让广大iOS开发者能快速享受到协程带来的编程方式上的改变,手机淘宝架构团队基于长期对系统底层库和汇编的研究,通过汇编和C语言实现了支持 Objective-C 和 Swift 协程的完美解决方案 —— coobjc。
核心能力
内置系统扩展库
coobjc设计
最底层是协程内核,包含了栈切换的管理、协程调度器的实现、协程间通信channel的实现等。
中间层是基于协程的操作符的包装,目前支持async/await、Generator、Actor等编程模型。
最上层是对系统库的协程化扩展,目前基本上覆盖了Foundation和UIKit的所有IO和耗时方法。
核心实现原理
协程的核心思想是控制调用栈的主动让出和恢复。一般的协程实现都会提供两个重要的操作:
我们基于线程的代码执行时候,是没法做出暂停操作的,我们现在要做的事情就是要代码执行能够暂停,还能够再恢复。 基本上代码执行都是一种基于调用栈的模型,所以如果我们能把当前调用栈上的状态都保存下来,然后再能从缓存中恢复,那我们就能够实现yield和 resume。
实现这样操作有几种方法呢?
上述第三种和第四种只是能过做到跳转,但是没法保存调用栈上的状态,看起来基本上不能算是实现了协程,只能算做做demo,第五种除非官方支持,否则自行改写编译器通用性很差。而第一种方案的 ucontext 在iOS上是废弃了的,不能使用。那么我们使用的是第二种方案,自己用汇编模拟一下 ucontext。
模拟ucontext的核心是通过getContext和setContext实现保存和恢复调用栈。需要熟悉不同CPU架构下的调用约定(Calling Convention). 汇编实现就是要针对不同cpu实现一套,我们目前实现了 armv7、arm64、i386、x86_64,支持iPhone真机和模拟器。
说了这么多,还是看看代码吧,我们从一个简单的网络请求加载图片功能来看看coobjc到底是如何使用的。
下面是最普通的网络请求的写法:
下面是使用coobjc库协程化改造后的代码:
原本需要20行的代码,通过coobjc协程化改造后,减少了一半,整个代码逻辑和可读性都更加好,这就是coobjc强大的能力,能把原本很复杂的异步代码,通过协程化改造,转变成逻辑简洁的顺序调用。
coobjc还有很多其他强大的能力,本文对于coobjc的实际使用就不过多介绍了,感兴趣的朋友可以去官方github仓库自行下载查看。
我们在iPhone7 iOS11.4.1的设备上使用协程和传统多线程方式分别模拟高并发读取数据的场景,下面是两种方式得到的压测数据。
从上面的表格我们可以看到使用在并发量很小的场景,由于多线程可以完全使用设备的计算核心,因此coobjc总耗时要比传统多线程略高,但是由于整体耗时都很小,因此差异并不明显,但是随着并发量的增大,coobjc的优势开始逐渐体现出来,当并发量超过1000以后,传统多线程开始出现线程分配异常,而导致很多并发任务并没有执行,因此在上表中显示的是大于20秒,实际是任务已经无法正常执行了,但是coobjc仍然可以正常运行。
我们在手机淘宝这种超级App中尝试了协程化改造,针对部分性能差的页面,我们发现在滑动过程中存在很多主线程IO调用、数据解析,导致帧率下降严重,通过引入coobjc,在不改变原有业务代码的基础上,通过全局hook部分IO、数据解析方法,即可让原来在主线程中同步执行的IO方法异步执行,并且不影响原有的业务逻辑,通过测试验证,这样的改造在低端机(iPhone6及以下的机器)上的帧率有20%左右的提升。
简明
易用
清晰
性能
程序是写来给人读的,只会偶尔让机器执行一下。——Abelson and Sussman
基于协程实现的编程范式能够帮助开发者编写出更加优美、健壮、可读性更强的代码。
协程可以帮助我们在编写并发代码的过程中减少线程和锁的使用,提升应用的性能和稳定性。
本文作者:淘宝技术
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流