视频录制
最终诉求?
拍摄、保存、播放、上传。就这四个步骤,当然首先拍摄就有许许多多的优化小功能,切换摄像头、单击跳帧焦距、双击远近距离切换、上移取消拍摄。
保存功能就只有一个保存到本地沙盒的功能。
播放自然就涉及到原生或是三方库来播放的问题,用什么第三方库来播放。
上传就有几种情况了,第一种上传到聊天服务器,第二种上传到七牛云。
持续录制?
- 开始动画(
录制状态属性已开始、按钮放大动画、进度条Alpha变1、添加CADisplayLin
) - 提示上移取消
- 隐藏摄像头按钮和闪光灯按钮
- 开始录制(
设置视频输出路径、视频正式输出
)
进度条的逻辑?
- 实例CADisplayLink添加到主线程
- 每次宽度缩小360分之一
- 宽度变成0或是负数
- 结束视频录制
- 修改canSave属性
- 按钮复原
- 移除CADisplayLink
- 进度条恢复屏幕宽度
上移取消?
- tip提示上移取消
- 显示摄像头按钮和闪光灯按钮
- 结束动画(
录制状态属性已结束、结束视频录制、按钮复原、移除CADisplayLink、进度条恢复屏幕宽度
)
结束录制?
- tip提示松手结束视频录制
- 显示摄像头按钮和闪光灯按钮
- 结束动画(
录制状态属性已结束、结束视频录制、按钮复原、移除CADisplayLink、进度条恢复屏幕宽度
) - 修改canSave属性(跳转控制器)
微信小视频逻辑?
- 分析为什么不用UIImagePickerController
- 自定义相机UI
- 执行视频捕获逻辑
- 写入数据
- 完成录制并写入相册
UIImagePickerController?
这是一个封装了相机UI同时处理视频捕获逻辑的苹果原生控制器。除非自定义相机UI,没有比通过UIImagePickerController集成视频捕获功能到应用更简单的方法。
每次实例化UIImagePickerController控制器开启视频录制功能以前,首先就需要通过UIImagePickerController的类方法isSourceTypeAvailable判断应用运行的设备是否有摄像头,防止App运行在摄像头毁坏的设备上或者模拟器上。
当然拥有摄像头也不一定就代表着这个摄像头有视频录制功能,就像不是每一个安卓机都有全景功能功能,一样一样的道理。
同样是通过UIImagePickerController的类方法availableMediaTypesForSourceType来获取摄像头可支持的功能的数组列表。
然后判断这个摄像头功能数组是否包括(NSString )kUTTypeMovie这个元素,如果包括,自然可以开始实例化UIImagePickerController控制器对象,设置控制器对象的sourceType属性为摄像头UIImagePickerControllerSourceTypeCamera、设置控制器对象的mediaTypes摄像头功能数组为@[(NSString )kUTTypeMovie],同时设置控制器对象的delegate*委托属性为self便于响应摄像头的不同状态。
设置控制器对象的cameraDevice即前置还是后置尤其注意,很有可能会出现前置摄像头可用而后置摄像头不可用的情况。
虽然同样是用UIImagePickerController的类方法来判断前置摄像头或是后置摄像头可用,但是判断前置还是后置可用isCameraDeviceAvailable跟前面判断是否有摄像头isSourceTypeAvailable还是有区别的,不然就直接写在一起了。
还有就是可以设置摄像头视频拍摄的画面质量videoQuality属性,按照常理来说,不都是有多清晰就来多清晰么,还真不是,典型的微信小视频就是为了更即时,明显降低了视频清晰度。
当然了,默认是UIImagePickerControllerQualityTypeHigh,意味着摄像头会将自动设置成所能支持的最高编码配置实现最高画质。
自定义相机UI?
自定义相机UI又有两种方法:方法一是通过UIImagePickerController来实现,是通过隐藏UIImagePickerController的默认控件和覆盖自定义视图到相机预览图层来实现UI的部分自定义。
当然需要重新设置UIImagePickerController控制器对象的cameraOverlayView属性为我们自定义的视图View。还有如果自定义了startVideoCapture按钮,则需要将控制器对象的showsCameraControls置为NO,需要重新将自定义的按钮与startVideoCapture事件关联起来。
方法二就是完全自己搭建相机UI界面并重写视频捕获逻辑,同样通过UIImagePickerController的视频处理底层AVFoundation类来实现。
AVFoundation可就复杂了,说三天三夜也说不完。
自定义相机UI界面设置十分简单,就是一个半屏幕大小的VideoView、摄像头切换按钮changeBtn、闪光灯开关按钮flashModelBtn、倒计时条progressView、上移取消的cancelTipLable、开始录制的按钮TapBtn、单击调整焦距的TipLable、双击切换近远镜头的TipLable。
当然单击调整焦距、双击切换近远镜头的手势是以代码的形式添加在半屏幕大小的VideoView上。
用代码而不用XIB是因为这里面有一个特别容易让人搞混的问题,就是点击事件和双击事件存在于同一个父视图上,或者说pan手势和swipe手势同时存在于一个父视图上,都是只会响应单击手势和pan手势。
永远也不会响应双击手势和swipe手势,解决的办法就是在响应单击手势或是swipe手势之前先提前确认一下用户确实不是想要触发双击手势和swipe手势,通过requireGestureRecognizerToFail确认用户确实不是想要触发双击手势和swipe手势之后再响应单击手势和pan手势。
视频捕获逻辑?
- 摄像头和麦克风授权
- 建立Session
- 添加Input(
摄像头---对焦、曝光、白平衡。麦克风---修改默认麦克风配置
) - 添加Output(
样本缓存滤镜处理
) - 正式开始视频捕捉、修改当前的摄像头状态为录制状态、持续捕捉、结束捕捉
摄像头和麦克风授权?
访问相机和麦克风需要先获得用户授权,授权的状态包括用户未选择、用户想授权却没资格、用户拒绝授权、用户同意授权。
1、用户未选择:只要创建AVCaptureDeviceInput对象时,iOS就会自动弹出原生对话框请求用户授权(仅第一次);
2、用户同意授权:直接进入下一个逻辑;
3、用户拒绝授权Or用户想授权没资格:还是通过AVCaptureDevice调用requestAccessForMediaType类方法重新弹出原生对话框请求用户授权,Block回调授权成功则进入下一个逻辑,Block回调失败则pop销毁当前控制器,同时来点提示“设置-->通用-->隐私-->更改”。如果用户未授权就进入下一步逻辑,得到的将是令人恐慌的黑屏和无声。
建立AVCaptureSession对象Session?
AVCaptureSession视频捕获会话类,AVFoundation框架的中心枢纽,负责调配数据流,这个数据流不仅包括视频数据流、自然还包括音频数据流。本质上AVCaptureSession视频捕获会话类就像个大卡车似的,不断把视频数据和音频数据从摄像头和麦克风输入端AVCaptureDeviceInput那里拉到视频捕捉输出端AVCaptureMovieFileOutput。通过[AVCaptureSession startRunning]开始数据流从输入到输出,[AVCaptureSession stopRunning]停止输出输入的流动。设置sessionPreset属性设置视频捕捉的质量水平或视频捕捉输出的比特率。
添加AVCaptureDeviceInput对象到AVCaptureSession对象?
AVCaptureSession的输入其实就是一个或多个的AVCaptureDeviceInput对象。可以使用[AVCaptureDevice devices]来搜寻所有具有数据输入资格的捕获设备(前摄、后摄、麦克风)。创建两个AVCaptureDeviceInput数据采集类对象作为AVCaptureSession的数据输入源,分别以摄像头和麦克风为捕获设备,捕获设备就是创建的AVCaptureDevice对象。AVCaptureDevice实例创建捕获设备,捕获设备作为参数传入创建AVCaptureDeviceInput对象的初始化方法里。异步添加AVCaptureDeviceInput数据输入对象到AVCaptureSession视频捕获会话中就可以了。如果摄像头默认的捕获配置参数不符合我们的要求,不是我们想要的视频帧频率,那么需要调用lockForConfiguration:来获取设备的配置属性的独占访问权限,获得权限后调用setActiveFormat:方法来重新配置捕获设备的捕获格式。
摄像头的对焦、曝光和白平衡?
通过设置捕获设备的activeVideoMinFrameDuration属性和activeVideoMaxFrameDuration属性来设置捕获设备的帧速率,每一帧的时长就是帧速率的倒数,帧速率干脆叫帧频率更准确了,表示摄像头在一秒时间内总共采集多少视频图片帧。但尤其需要注意的是帧速率的设置是有限制的,设置的帧速率必须是在设备格式所能支持的范围内。只是这个地方有点不解,既然设置帧速率就间接设置了帧时长,那为什么还要单独来设置帧时长呢,除非不能设置帧速率,间接通过设置帧时长来设置帧速率,但是回归当初的诉求,不就是想要控制器摄像头采集视频图片帧的频率么?为了确保帧速率恒定,可以将最小与最大的帧时长设置成一样的值。这是个什么鬼,但是确定了一点,帧速率是不可以直接设置的,只能通过设置最小帧时长和最大帧时长来间接设置帧速率。那么如何设置最小帧时长和最大帧时长呢?通过CMTime实例化一个frameDuration帧时长变量。通常是1秒采集60次,也就是说最小和最大帧时长都是CMTimeMake(1, 60)。可是在设置捕获设备的最小帧时长和最大帧时长之前,要确认两个判断,第一个判断很简单就是[device lockForConfiguration:&error]解锁捕获设备的配置,如此才有资格去设置捕获设备的帧时长。第二个判断就是确认我们设置的帧时长介于捕获设备允许设置的帧时长范围之间。那么如何判断设置的帧时长是介于捕获设备允许设置的最小帧时长和最大帧时长之间呢?首先就是用数组接收[device.activeFormat videoSupportedFrameRateRanges]返回捕获设备所有可设置的格式选项,然后遍历这个格式数组的每一种格式,只要我们设置的帧时长在任意一种格式的最小帧时长和最大帧时长之间就可以了。现在设置捕获设备帧时长的两个前提条件都已存在,接下来就是捕获设备对象直接调用方法setActiveVideoMaxFrameDuration设置帧时长为我们自定义的那个帧时长,即1/60秒。当然最后尤其要注意一点就是设置完捕获设备的帧试产过之后,需要捕获设备对象及时调用unlockForConfiguration方法给这个捕获设备的配置上锁,避免重复修改和其它未知错误。
摄像头的视频防抖?
iphone6笑称影院级的视频防抖。说的好像在电影院建在过山车上似得!不过苹果的工程师也蛮拼的,为了增加防抖功能,硬是新建了一个AVCaptureConnection类,很奇怪吧,按理说这应该通过捕获设备的对象来设置呀!可能是因为苹果工程师考虑到不是每一个捕获设备都支持视频防抖功能的考虑吧。其实啦,就算通过AVCaptureConnection类来开启视频防抖功能也是需要先确认捕获设备支持的防抖到底属于哪一个级别。然后搞笑的一面出来了,判断设备是否支持特定级别的防抖功能是通过捕获设备对象的activeFormat属性调用isVideoStabilizationModeSupported方法判断,可是真正开启视频防抖功能,又是AVCaptureConnection对象调用setPreferredVideoStabilizationMode方法来实现。不得不说,苹果你真会玩儿!当然了,言归正传,每一个设备所能支持的视频防抖级别还都不一样呢,那到底应该如何找到最适合的,成了摆在我面前的疑问?AVCaptureVideoStabilizationMode stabilizationMode = AVCaptureVideoStabilizationModeCinematic;这句代码应该就是默认获取当前设备能提供的最高视频防抖级别吧,除非苹果想要有所保留。哈哈,管他的呢,爱留不留!最后提一点黑科技,从ipone6以后,能够拍摄高动态的HDR视频,虽然我也不知道这是什么鬼,但是知道他很厉害就对了,如果开启,方法一:设置捕获设备的videoHDREnabled属性。方法二:设置捕获设备的automaticallyAdjustsVideoHDREnabled属性。这个更高级,因为会自动判断当前捕获设备是否支持HDR拍摄。
捕获设备之麦克风音频输入?
首先普及常识,手机共有3个麦克风,但是前面通过获取到的捕获设备数组来看,列表里面有前置摄像头、后置摄像头、一个麦克风。一查才知道,是因为总是把3个麦克风放在一起使用,便于优化性能,例如iphone5及以上的手机录制视频时,都是同时使用前置和后置麦克风,用于定向降噪。大多数情况下,设置成默认的麦克风配置即可。后置麦克风会自动搭配后置摄像头使用 (前置麦克风则用于降噪),前置麦克风和前置摄像头也是一样。当然啦,很有可能有这么一种需求,使用后置摄像头捕获场景,使用前置麦克风来录制解说。那么这时候就需要访问和配置单独的麦克风。这就是我们需要弄明白的AVAudioSession音频会话类。
AVAudioSession?
首先AVAudioSession是一个单例类,直接通过sharedInstance获取实例化对象。首先就遇到一个问题。为什么们必须给音频会话对象setCategory设置一个音频类型?设置不同的音频类型各有什么不同的特点?音频类型有很多,选择音频类型时考虑的主要因素包括:应用是否会随着静音键和屏幕关闭而静音(直播的扬声器就不听静音键控制)、应用会不会中止其它应用(如Safair)正在播放的声音、应用是否能在后台播放声音(默认音频类型不能,如果后台播放则需要修改info.plist的UIBackgroundModes属性和默认的音频类型)、录音时是否自动屏蔽除来电铃声的其它系统声音(如闹钟、日历)、是否同时支持播音和录音(微信就用这个音频类型,这个音频类型的默认输出口就是听筒)。选择音频类型的考虑因素如此之多,不费点脑子是不行的,就拿我们的需求来说,我想要实现后置摄像头拍摄,而前置麦克风录音,后置麦克风降噪,本质上就是访问和变更音频会话,技巧就在于此,并不是音频会话的每一个音频类型都可以被访问和变更,能够访问和变更的音频类型只有一个,这个音频类型就是AVAudioSessionCategoryPlayAndRecord。同时这个音频类型也是所有音频类型中最特别最个性的那个,倒不是因为微信也是用这一个音频类型,怪只怪这个音频类型实在是太独特。1、一般来说,没有外接音频设备,声音默认都是从扬声器出来,但是在音频类型为AVAudioSessionCategoryPlayAndRecord的情况下,就很特别,听筒就一跃成为默认的输出设备,这也是唯一能够使用到手机听筒设备的音频类型。那么问题来了,刚才说微信也是使用这个音频类型,按此逻辑,当我们点击播放别人发来的语音时,应该默认从听筒放出来才对,但是事实上除非把耳朵贴近听筒,声音都是从扬声器出来,这如何解释?方法1、setCategory: AVAudioSessionCategoryPlayAndRecord withOptions: AVAudioSessionCategoryOptionDefaultToSpeaker方法完美地做到了这一点,尽管你AVAudioSessionCategoryPlayAndRecord音频类型默认听筒输出声音,但是依然可以修改你的默认配置嘛!真是上有政策下有对策,斗天斗地都人类其乐无穷呀!方法2:调用overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error方法代码的形式切换音频输出为扬声器。好了,现在弄明白了音频类型,咱接着往下走,就是音频会话类对象调用setActive方法正式开启手机的音频。如果不需要考虑特别的麦克风配置需求,做到这一步就可以了,但是这没有难度呀,干脆再走一步,如果我在开启后置摄像头情况下,不想使用默认的后置麦克风录音而前置麦克风降噪这种配置。想要开启前置麦克风录音而后置麦克风降噪,那么我们需要做的还要很多。首先就是数组接收[audioSession availableInputs]获取到的输入端口列表。这个列表里面的输入端口不仅包括AVAudioSessionPortBuiltInMic(内置麦克风)通知也包括AVAudioSessionPortHeadsetMic(耳机麦克风)。我想修改的是内置麦克风,因此遍历数组的每一个输入端口,将内置麦克风AVAudioSessionPortBuiltInMic这个输入端口通过AVAudioSessionPortDescription实例化的对象保存起来,然后终止遍历。那么问题来了?干嘛不直接就把AVAudioSessionPortBuiltInMic内置麦克风输入端口赋值给AVAudioSessionPortDescription实例化的对象,非要遍历一下,唯一能想到的理由就是担心设备无法扫描到可用的输入端口,所以在最终确定内置麦克风AVAudioSessionPortBuiltInMic为输入端口的时候需要再用代码确认一下,增强程序鲁棒性。现在已经确定了AVAudioSessionPortBuiltInMic内置麦克风作为音频输入端口,可是内置麦克风可有3个呢,如何找到我们需要的那个前置麦克风呢?很简单,遍历AVAudioSessionPortBuiltInMic内置麦克风音频输入端口的dataSources数组属性,取出数组的每一个AVAudioSessionDataSourceDescription对象。然后判断取出对象的orientation属性的值是否就是isEqual:AVAudioSessionOrientationFront前置麦克风。一但判断输入端口的麦克风就是我们想要更改的前置麦克风,那么内置麦克风音频输入端口对象就调用setPreferredDataSource:soure方法设置前置麦克风为首选麦克风。同时音频会话对象调用setPreferredInput:builtInMic方法设置内置麦克风为音频会话的首选输入端口。最后在说一点,除了通过AVAudioSession音频会话单例对象修改默认的麦克风配置以外,还可以来配置音频增益和采样率等其他高大上的东西。
视频捕捉数据的输出?
视频捕捉会话的输入包括摄像头和麦克风都已经配置好了,现在还需搞定视频捕捉会话的输出。方法1:最简单的就是AVCaptureMovieFileOutput类是将采集到的音视频数据写入QuickTime文件,通过AVCaptureMovieFileOutput能够实时判断内存的剩余空间是否达到指定的阀值。方法2:如果想要实现对采集到的音视频数据进行一些处理甚至更复杂一点的操作,AVCaptureMovieFileOutput就有心无力了,必须得靠AVCaptureVideoDataOutput和AVCaptureAudioDataOutput这两个类来实现。
AVCaptureMovieFileOutput?
AVCaptureMovieFileOutput类主要负责将摄像头采集的图片帧数据和麦克风采集到的音频数据打包成音视频QuickTime文件后写入指定路径之中。这个逻辑就十分简单啦,直接就是AVCaptureMovieFileOutput实例化一个视频捕捉输出对象,然后在AVCaptureSession视频捕捉会话对象能够添加AVCaptureMovieFileOutput视频捕捉输出对象的前提下将输出对象添加到会话之中。视频捕捉输出类对象唯一需要设置的参数也就是调用startRecordingToOutputFileURL:outputURL方法传入视频文件输出路径这个参数。同时设置一个委托代理方便处理视频输出的各种中间状态。但是AVCaptureMovieFileOutput类在iphone上不能暂停录制,也不能修改视频文件的类型,更不能对录制的视频预处理,所以采用更复杂但是灵活性更强的AVCaptureVideoDataOutput和AVCaptureAudioDataOutput来实现将数据写入文件的任务。
AVCaptureVideoDataOutput和AVCaptureAudioDataOutput?
AVCaptureVideoDataOutput和AVCaptureAudioDataOutput都是AVCaptureOutput这个抽象类的子类,AVCaptureVideoDataOutput主要输出捕获的未压缩或压缩的视频捕获的帧,AVCaptureAudioDataOutput主要输出捕获的非压缩或压缩的音频样本。captureOutput:didOutputSampleBuffer:fromConnection:这个代理方法将接收到发送过来的样本缓存,通常下一步处理就是给视频图片帧添加滤镜。接着使用AVAssetWriter对象将处理过的样本缓存写入到指定的文件路劲之中。那么说到这里,疑问是排山倒海般涌过来。
AVAssetWriter?
AVAssetWriter负责将媒体数据写入到文件。AVAssetWriter对象可以规定写入媒体文件的格式(如QuickTime电影文件格式或MPEG-4文件格式)。AVAssetWriter有多个并行的轨道媒体数据,视频轨道和音频轨道是最基础的轨道。面向对象编程,无论是啥,首先就是创建对象,没有对象的光棍毫无用处呀!言归正传,创建AVAssetWriter对象需要传入的参数包括文件的输出路径URL和文件格式。文件格式选择AVFileTypeMPEG4即是MP4格式。iphone摄像头录制视频的默认格式是mov,虽然move兼容MP4,但是MP4格式还是存在很多需求和便利。好了,创建好了AVAssetWriter对象之后,就需要创建两个AVAssetWriterInput对象(一个传输音频缓存数据AVMediaTypeAudio,一个传输视频缓存数据AVMediaTypeVideo),接着把这两个对象添加到AVAssetWriter对象之中,经整合成音视频数据后再一同输出写入到QuickTime文件。不得不说,AVAssetWriterInput真是厉害呀!居然既可以传输音频缓存数据又可以传输视频缓存数据。当然只要是创建AVAssetWriterInput对象,都必须设置expectsMediaDataInRealTime属性为YES,表示实时获取摄像头和麦克风采集到的视频数据和音频数据。就是有一点不解,为什么添加AVAssetWriterInput对象到AVAssetWriter对象前必须先调用canAddInput判断是否可以添加呢?感觉根本没必要呀,讲真,如果结果是NO,就不会添加音频输入跟添加了音频输入而不生效是一样一样的呀!当然啦,逻辑的本质就是写if判断嘛!哈哈。最后再说明一点,创建AVAssetWriterInput对象的初始化方法里(这里不论视频Or音频),有一个参数叫配置字典,主要负责对传输的音视频数据进行处理重新编码,通常传nil使用默认配置。而且如果实在要自己配置字典,还有写小技巧就是调用recommendedVideoSettingsForAssetWriterWithOutputFileType:方法或者recommendedAudioSettingsForAssetWriterWithOutputFileType:方法来返回手机设备推荐的配置字典,然后再稍微调整一下配置字典的特别参数就可以获得想要的输出配置字典。例如,增加视频的比特率来提高视频质量。科普一下:视频比特率就是1秒传送多少比特的视频二进制数据。音频比特率就是1秒传送多少音频二进制数据量,间接控制音频质量。当然了,这个配置字典也完全通过AVOutputSettingsAssistant来配置完全自定义的输出配置字典。但是不怎么好用,毕竟涉及到的因素太多,还是在苹果推荐数据输出配置字典上稍加改动靠谱更多。
AVAssetWriterInput?
AVAssetWriterInput主要负责将多媒体样本缓存文件CMSampleBuffer实例连接到AVAssetWriter对象的一个文件输出轨道;当有多个输入时,AVAssetWriter试图在用于存储和播放效率的理想模式写媒体数据。它的每一个输入信号,是否能接受媒体的数据根据通过readyForMoreMediaData的值来判断。如果readyForMoreMediaData是YES,说明输入可以接受媒体数据。并且你只能媒体数据追加到输入端。上面是录制之前的一些需要的类和配置,下面介绍的是如何将获取到的数据呈现出来和怎样进行文件写入
AVCaptureConnection?
AVCaptureConnection代表AVCaptureInputPort或端口之间的连接,和一个AVCaptureOutput或AVCaptureVideoPreviewLayer在AVCaptureSession中的呈现。代表着输出端口的连接,这个输出端口连接着视频数据输出或音频数据输出。
遗留问题:当手机的剩余内存达到某个阈值停止录制?自定义音频视频的压缩率?
正式开始视频捕捉?
现在数据输入类的两个对象(一个摄像头另一个麦克风)和数据输出类的对象都已添加到视频采集会话对象中了,既然视频捕捉的输入端和输出端都处理好了,自然调用startRunning方法正式开启视频捕捉。特别需要提醒的是,视频捕捉会话对象调用的方法都是耗时操作,所有尤其有必要使用多线程,操作队列是首选,毕竟通过队列来实现操作与操作之间的依赖无比方便呀!视频捕捉输出过程中,可能最想要的需求就是能够控制视频输出的清晰度。太高清的视频存本地还行,如果要往云端传可就费劲了。还有如果能够在程序之外改变视频的清晰度就更好了,唯一的方法就是设置视频捕捉会话对象的sessionPreset属性为AVCaptureSessionPresetInputPriority,表示视频捕捉会话对象不再通过代码的形式去控制音频与视频的输出画面的质量,而是通过已连接的捕获设备(摄像头)改变activeFormat属性(设备捕获格式)控制采集频率进而控制数据输出的质量。
视频捕捉实时预览?
实时预览很简单,本质就是把AVCaptureVideoPreviewLayer对象作为一个sublayer添加到相机UI的图层上。当然初始化AVCaptureVideoPreviewLayer对象这个子图层的时候需要传入一个参数,就是AVCaptureSession视频捕捉会话对象,毕竟输入端和输出端的通道全靠AVCaptureSession视频捕捉会话对象来调用。预览就相当于在正式输出之前,在输入端和输出端之间提添加一个滤网,自然初始化滤网的时候必须表明这个滤网到底添加在哪一个通道里。设置好预览图层对象的frame就可以轻松添加到相机UI的图层上了,为什么是图层,因为不需要响应任何交互事件嘛!通常来说,预览图层的frame是等于我们自定义的相机UI的bounds属性的。当然这不难,真正有挑战的是给实时预览画面添加滤镜,这就需要用到一个全新类,即AVCaptureVideoDataOutput视频捕捉数据输出类。将AVCaptureVideoDataOutput对象添加到AVCaptureSession视频捕捉会话对象之后,使用OpenGL来渲染融合一体。这就是相机带来的挑战,哈哈,有点意思!这就属于图片捕捉的范畴了,下面就跟微信小视频无关了,纯属相机照片捕捉的扩展!
UIImagePickerController?
集成拍照功能的最简方法。前后摄像头切换、闪光灯开关、点击屏幕对焦和曝光功能通通都有!
AVFoundation 框架?
代码更改摄像头硬件参数、操纵实时预览图
AVCaptureDevice?
控制相机硬件特性(镜头位置、曝光、闪光灯)
AVCaptureDeviceInput?
负责摄像头数据的采集
AVCaptureOutput?
作为一个抽象类,主要负责摄像头数据的输出,描述捕捉图片的结果。AVCaptureStillImageOutput 用于捕捉静态图片。AVCaptureMetadataOutput 主要用于检测人脸和扫描二维码。AVCaptureVideoOutput 为实时预览图提供原始帧。
AVCaptureVideoPreviewLayer?
CALayer 的子类,自动显示相机产生的实时图像。
图片捕捉逻辑?
遍历所有能提供视频数据的设备、检查 position 属性、设置为 AVCaptureDeviceInput 对象的捕获设备
图片捕捉预览?
最简单的方法一:创建一个 AVCaptureVideoPreviewLayer预览图层添加到 的view的subLayer。AVCaptureVideoPreviewLayer会自动显示来自相机的输出。方法二:创建AVCaptureVideoDataOutput对象从输出的视频数据流捕捉单一图像视频帧,使用OpenGL手动显示到View上。肃然很复杂,但是这是实现预览操作(毕竟方法一只是一个预览图层无法响应事件)和滤镜处理的唯一方法。最后需要说明的是,如果旋转相机90度,方法一的预览图层AVCaptureVideoPreviewLayer 会自动根据传感器的朝向处理这种情况。但是方法二则需要代码来进行判断。
图片捕捉最佳参数配置?
sessionPreset属性设置为AVCaptureSessionPresetPhoto最简单,系统根据手机设备自动选择照片捕捉最合适的配置。配置考虑的因素包括:感光度(ISO)、曝光时间、基于相位检测的自动对焦、输出全分辨率的JPEG格式压缩的静态图片。当然了,如果还是不满意手机设备智能推荐的图片捕捉配置,可以创建AVCaptureDeviceFormat对象存储摄像头捕捉设备的更多参数,比如静态图片的分辨率、视频预览分辨率、自动对焦类型、感光度、曝光时间等。然后把这个图片捕捉参数配置模型赋值给AVCaptureDevice对象的activeFormat属性就可以了。当然了,我们自定义的图片捕捉参数配置模型的数据必须介于AVCaptureDevice.formats所有可支持的格式的列表之中。
图片捕捉对焦?
通过移动摄像头的镜片与光线传感器之间的距离实现的。自动对焦是通过相位检测和反差检测实现的。然而,反差检测只适用于低分辨率和高 FPS 视频捕捉 (慢镜头)。
图片捕捉曝光?
在 iOS 设备上,镜头上的光圈是固定的 (在 iPhone 5s 以及其之后的光圈值是 f/2.2,之前的是 f/2.4),因此只有改变曝光时间和传感器的灵敏度才能对图片的亮度进行调整,从而达到合适的效果。至于对焦,我们可以选择连续自动曝光,在“感兴趣的点”一次性自动曝光,或者手动曝光。除了指定“感兴趣的点”,我们可以通过设置曝光补偿 (compensation) 修改自动曝光,也就是曝光档位的目标偏移。目标偏移在曝光档数里有讲到,它的范围在 minExposureTargetBias 与 maxExposureTargetBias 之间,0为默认值 (即没有“补偿”)。使用手动曝光,我们可以设置 ISO 和曝光时间,两者的值都必须在设备当前格式所指定的范围内。
图片捕捉白平衡?
数码相机为了适应不同类型的光照条件需要补偿。这意味着在冷光线的条件下,传感器应该增强红色部分,而在暖光线下增强蓝色部分。在 iPhone 相机中,设备会自动决定合适的补光,但有时也会被场景的颜色所混淆失效。幸运地是,iOS 8 可以里手动控制白平衡。自动模式工作方式和对焦、曝光的方式一样,但是没有“感兴趣的点”,整张图像都会被纳入考虑范围。在手动模式,我们可以通过开尔文所表示的温度来调节色温和色彩。典型的色温值在 2000-3000K (类似蜡烛或灯泡的暖光源) 到 8000K (纯净的蓝色天空) 之间。色彩范围从最小的 -150 (偏绿) 到 150 (偏品红)。温度和色彩可以被用于计算来自相机传感器的恰当的 RGB 值,因此仅当它们做了基于设备的校正后才能被设置。
微信小视频
现在摆在我面前的视频图片帧处理逻辑,貌似还是有点蒙圈,就是很多都不知道是什么意思,这里面到底是一个什么逻辑,换句话说,只要把这个逻辑想明白了,其它也就没有什么问题了,可是我改怎么去想明白这个问题呢?为什么会觉得难,主要是看到了我不熟悉的东西,什么不熟悉,然后习惯性地去逃避他,那么现在我需要做的就是研究弄明白这个逻辑,共需要处理哪些不熟悉,一旦弄懂了这些不熟悉,我将能够找到我想要的进步和成长,好了,现在开始来分析这个逻辑吧,到底这连是什么意思,想整理一下我可能会遇到的所有问题,
第一:@synchronized用在此处的意义?
第二:判断视频生在录制或是正处于暂停状态就立马返回这是什么意思?
第三:判断回调方法传递过来的AVCaptureOutput对象是视频图片帧还是音频帧?
第四:判断两个并发条件,条件一录制编码对象为空,条件二有音频参数出入。连个条件同时满足时,实例化CMFormatDescriptionRef音频格式转换器对象、拿着这个对象去设置音频的格式、根据时间戳构建视频名字、拿着视频名字和文件夹路径拼接文件路径、根据文件路径+视频分辨率高+视频分辨率宽+音频通道+音频采样率构建音频编码器?
第五:判断是否中断录制,这葫芦里卖的是什么药?假设中断过,如果是视频中断,直接返回,如果是音频中断、计算暂停时间、这个属性用来判断什么录制状态,貌似特别重要,首先就是考虑,妈的,启动录制功能和开始录制还不一样,这就就有些坑爹呀,根本不知道这是要闹哪样,关键的问题是为了分析视频流,我们需要为output设置delegate,并且指定delegate方法在哪个线程被调用。需要主要的是,线程必须是串行的,确保视频帧按序到达。开始录像之后你可以从这个回调的method 里面拿到每一个录下的frame,然后可以用AVAssetWriter来写入local的文件
属性:是否正在录制、是否暂停录制、当前录制时间、录制最长时间、代理回调引擎处理结果、视频保存路径、预览图层
方法?
- 开始录制
- 停止录制
- 取消录制
- 闪光灯切换
- 摄像头切换
- mov格式转成mp4
现在已经完完整整的实现了小视频的录制和保存甚至预览播放,接下来就是一个上传的步骤,当然了,可能小视频的初衷根本不是存入相册,不得不说,存入相册让我高兴良久,现在就是让我的放相册具有我想要的功能,我想要的功能是什么呢?我想要所有功能包括:开关闪光灯、切换摄像头、单击调整焦距、双击调整摄像头远近、打开相册、拍摄的视频播放预览、上传到七牛云、实时更新进度条、长按1.5秒后开始录制、松开按钮停止录制、按住按钮向上滑取消录制、点击按钮按钮放大、松开按钮按钮复原、
现在我遇到问题了,就是我无法得知我应该怎么去处理我的预览和保存和上传的逻辑,现在的情况是,以录制就直接保存到相册快乐,这样真的好嘛,其实这样挺好的,我希望的是直接在录制完成之后,捕捉预览层就变成了视频播放层,下面的录制按钮变成上传按钮,可是如果当我看到我拍摄的视频,我并不喜欢,我希望重新拍摄,那我应该做的就是重拍,也就是说当我开启视频播放时,两个按钮,一个是重拍,一个是上传。那么接下来的事情也就好办了,那个跳转相册的功能我是不要的,在我停止录制或是取消录制之后.
问题一:倒计时条在变成0之后,没有调用停止录制的方法?
问题二:当视频播放层出显示,视频并没有播放?是不是格式不对?
问题四:点击退出控制器时,没有停止捕捉和视频的播放?
点击重拍:按钮居然是选中状态、进度条居然不是屏幕宽
进度条变成0之后,并没有结束录制,录制结束之后,播放也我发正常播放,难道是因为不支持MP4格式。进度条怎么还有问题呢,我的理解就是干脆不用那个视频帧回调,自己通过CADisplay来实现更新吧,也不是妖男,貌似还更加好控制一点,现在问题来了,省掉引擎的回调方法,也真是不亦乐乎。0.015秒刷新掉一次方法,委托代理刷新进度条,考虑到底什么时候停止刷新:1、停止录制2、取消录制3、时间超过6秒。停止录制之后:进度条重新归零。现在的我就一个诉求,程序把一帧帧的视频画面到底存入了哪里,现在是每获取一帧就录制起来,我需要的是先不要录制到相册,仅仅是录制,那么这个录制又是神峨眉意思,换句话说,我现在不把它保存到相册,而又丝毫不印象预览,那么这意味着什么,我就想知道,这样子的话,这个视频到底存入了哪里,我活该如何去获取到。难道说这个return逻辑还把我难到了,肯定不是,走了这么多天,多多少少也差不多了,还学到不少东西呢,我的诉求就一个,在验证权限成功之前,绝不进入viewappler方法,那么必须做到的是,一旦权限为NO,就立马pop返回,如果是一直YES,那么就是现在有一个问题就是captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error这个回调方法里面,我有天大的疑问,为什么视频跑捕捉会话的输出路径为一个还正产给的路径,回调的方法的输入的路径是一个正产的路径,但是通过视频捕捉这个对象获取到的属性则是一个空对象,简直不科学呀!所以我必须做到的是弄明白这个关系呀,还有,我怕吸纳子啊遇到一个特别大的问题,就是我无法获知到底我这个按钮点击事件是否有效,或许我改用其它的按钮来结局这个点击事件的问题,确实当我点击微视频的时候,能够开始录制,但是貌似不能准确地识别我是取消录制还是结束录制这个信号,这可咋整呀?我迫切需要您明白这两两者的细微差别。真的挺上伤感的,弄了这么久,居然还是没有发现这个的区别,特别是进度条,当我离开这个控制器之后,进度条咋就没有被销毁呢,我还说要精简这个程序呢。现在弄成这个样子,看来我首先需要做的就是个这个控制器分层,把一些逻辑从我的控制器里抽离出去,这里还是有范本的,不过这注定会遇到很多波折,但是那又何妨,必须要做这一步,否则真的是太乱了。好的,现在就开始吧,第一步做什么?第一步是否是需要建立一个单例就让我很伤神,直觉还告诉我,还有尝试一下不用单例的引擎,还是很有必要的。所以间一个工具类引擎类吧,还是引擎最高大上。接下来就是将那些逻辑抽离出去,我的想法是,都先抽离出。=直到我抽离不出去为止。
WechatShortVideo是什么
WechatShortVideo是基于SCRecorder的仿微信短视频拍摄类库。她提供了与微信几乎一致的短视频拍摄体验。
WechatShortVideo提供了哪些功能
*支持定时长拍摄
*支持拍摄预览
*操作体验与微信短视频拍摄几乎一致
*界面部分可定制
WechatShortVideo使用配置
WechatShortVideoConfig.h提供了一些界面定制项。
//视频最大录制时间
#define VIDEO_MAX_TIME
//视频最小录制时间
#define VIDEO_VALID_MINTIME
//视频文件名
#define VIDEO_DEFAULTNAME
//视频导出路径
#define VIDEO_OUTPUTFILE
//按压在有录制按钮范围内的提示
#define OPERATE_RECORD_TIP
//按压在非录制按钮范围的提示
#define OPERATE_CANCEL_TIP
//保存按钮标题
#define SAVE_BTN_TITLE
//重录按钮标题
#define RETAKE_BTN_TITLE
//录制按钮标题
#define RECORD_BTN_TITLE
//正常提示颜色
#define NORMAL_TIPCOLOR
//警告提示颜色
#define WARNING_TIPCOLOR
提供两个方法响应录制成功与界面退出事件。
- (void)doNextWhenVideoSavedSuccess;
- (IBAction)closeAction:(UIButton *)sender;
CocoaPods支持
你可以在Podfile中加入下面一行代码来使用WechatShortVideo
pod 'WechatShortVideo'
感谢
WechatShortVideo基于SCRecorder进行开发,并使用了MBProgressHUD,感谢他们对开源社区做出的贡献。
协议
WechatShortVideo被许可在MIT协议下使用。查阅LICENSE文件来获得更多信息。
iOS开发笔记
微信小视屏模仿- AVFoundation入门
- 微信小视屏模仿
- 技术路线: iOS开发中的视频录制主要技术路线。
- AVFoundation的初步使用:先录制再压缩。Demo1
-优化方案:按帧压缩视频、边录制边压缩。Demo2 - Tips:如何从导出真机沙盒里面的文件、iOS默认可选预设
CocoaPods相关
- CocoaPods 1.0 +适配
- CocoaPods 1.0安装及适配
-利用CocoaPods发布自己的三方库 - CocoaPods 1.0私有Pods
iOS开发中的『库』
- iOS开发中的『库』(一)
- .framework是什么?怎么制作?
-谈一谈自己对动态库和静态库的理解。
-在项目中如何使用动态framework的APP?使用了动态framework的APP能上架Appstore么?
-可以通过framework的方式实现app的热修复么? -
iOS开发中的『库』(二)
-再谈一谈动态库和静态库。你真的知道XXXX和XXX系列。
-为什么使用动态库的方式来动态更新只能用在in house和develop模式却不能在使用到AppStore上呢?
-动态库到底会添加到内存中几次?
Swift Tips
-
Swift 3迁移工作总结
-一天时间将4万行Swift 2 To Swift 3的工作总结。
-迁移中的问题
仿微信小视屏iOS技术路线实践笔记[录制篇]
一周之前拿到这个需求时,我当时是懵逼的,因为自己对视频这一块几乎可以说是一无所知。在断断续续一周的研究过程之后,准备写点笔记记录一下。
需求分析
-对于一个类似微信小视屏的功能,大致需要完成的功能无非就是两块:
-视频录制
-视频播放
先讲讲视频录制-技术路线
(因为自己对视频是个小白,只能借助谷歌来搜索一些相关技术,一定有什么不对的地方)
-在iOS中与视频录制相关的技术大概有三种:
- UIImagePickerController:这是系统相机的控制器,使用很简单,但是可定制程度几乎为零。
- AVFoundation:是一个可以用来使用和创建基于时间的视听媒体的框架,它提供了一个能使用基于时间的视听数据的接口。
- ffmpeg:一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。
看上去很懵逼是不是,其实我也是懵逼的。更甚至于AVFoundation和ffmpeg两者关系我最开始都摸不透。如果你和我一样懵逼可以看一下。我写的AVFoundation和视频捕捉相关的总结。ffmpeg则需要去看雷神的博客了,很详细,也很入门。
-对于以上三种,首先UIImagePickerController肯定不在考虑范围之内了,可定制化太低。
-对于利用相机录取视频只能用AVFoundation的AVCaptureSession来捕捉。
-
ffmpeg技术更注重于后期处理技术。关于后期处理,ffmpeg应该是目前最强大的视频处理技术了,利用CPU做视频的编码和解码,俗称为软编软解,目前很火的直播技术应该都是用的ffmpeg。
-此外,对于AVFoundation而言,因为是苹果自己提供的视频处理库,也可以用于视频后期处理而且还支持硬件编码。
废话不多说,上代码。
对于AVFoundation捕捉只是还不是很清楚的可以点击这里查看。
- Demo1下载链接
录制前的准备工作
-第(1/5)步,你得有一个AVCaptureSession?对象,作为输入、输出的中间件。
@property (nonatomic, strong) AVCaptureSession *captureSession;/**<捕捉会话*/
self.captureSession = ({
//分辨率设置
AVCaptureSession *session = [[AVCaptureSession alloc] init];
if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
[session setSessionPreset:AVCaptureSessionPresetHigh];
}
session;
});
-第(2/5)步,你得有将摄像头和话筒两个AVCaptureDevice?添加到AVCaptureSession的AVCaptureDeviceInput ?中。
///初始化捕捉输入
- (BOOL)setupSessionInputs:(NSError **)error {
//添加摄像头
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:({
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
}) error:error];
if (!videoInput) { return NO; }
if ([self.captureSession canAddInput:videoInput]) {
[self.captureSession addInput:videoInput];
}else{
return NO;
}
//添加话筒
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:({
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
}) error:error];
if (!audioInput){ return NO; }
if ([self.captureSession canAddInput:audioInput]) {
[self.captureSession addInput:audioInput];
}else{
return NO;
}
return YES;
}
-第(3/5)步,你需要有一个视频输出AVCaptureMovieFileOutput ?用于从AVCaptureDevice获得的数据输出到文件中。
//初始化设备输出对象,用于获得输出数据
self.captureMovieFileOutput = ({
AVCaptureMovieFileOutput *output = [[AVCaptureMovieFileOutput alloc]init];
//设置录制模式
AVCaptureConnection *captureConnection=[output connectionWithMediaType:AVMediaTypeVideo];
if ([captureConnection isVideoStabilizationSupported ]) {
captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
//将设备输出添加到会话中
if ([self.captureSession canAddOutput:output]) {
[self.captureSession addOutput:output];
}
output;
});
-第(4/5)步,你得有一个AVCaptureVideoPreviewLayer ?的视图,用于预览AVCaptureDevice拿到的界面。
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer; /**<相机拍摄预览图层*/
//创建视频预览层,用于实时展示摄像头状态
self.captureVideoPreviewLayer = ({
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];
previewLayer.frame=CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
previewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
[self.view.layer addSublayer:previewLayer];
self.view.layer.masksToBounds = YES;
previewLayer;
});
-第(5/5)步,现在你调用[self.captureSession startRunning];
真机运行就可以看到一个录制画面了。
录制视频
用AVCaptureMovieFileOutput录制视频很简单。代码如下。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
if (![self.captureMovieFileOutput isRecording]) {
AVCaptureConnection *captureConnection=[self.captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
captureConnection.videoOrientation=[self.captureVideoPreviewLayer connection].videoOrientation;
[self.captureMovieFileOutput startRecordingToOutputFileURL:({
//录制缓存地址。
NSURL *url = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"temp.mov"]];
if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
}
url;
}) recordingDelegate:self];
}else{
[self.captureMovieFileOutput stopRecording];//停止录制
}
}
查看录制视频
-关于如何查看沙盒内容可以点击这里
-拿到的视频大概8S。15.9 M左右。Excuse me?小视屏,15.9M?
-莫急,可以压缩嘛。
压缩视频
-压缩大概花了不到0.05秒,但是视频减少了10倍左右,在1M以内了。
-(void)videoCompression{
NSLog(@"begin");
NSURL *tempurl = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"temp.mov"]];
//加载视频资源
AVAsset *asset = [AVAsset assetWithURL:tempurl];
//创建视频资源导出会话
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetMediumQuality];
//创建导出视频的URL
session.outputURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"tempLow.mov"]];
//必须配置输出属性
session.outputFileType = @"com.apple.quicktime-movie";
//导出视频
[session exportAsynchronouslyWithCompletionHandler:^{
NSLog(@"end");
}];
}
ok!利用AVFoundation模仿小视屏功能就这么实现了~总结一下,如图
录制视频哈哈哈,那是不可能的
-虽然说,我们已经利用摄像头,能录制视频,且压缩到1M以下,但是还是存在以下问题:
-我们选择的尺寸不符合小视屏的尺寸。微信视频的尺寸比例大概是4:3。可选预设
-据iOS微信小视频优化心得说这样很耗时。所有对视频的处理都需要在录制完成之后来做。
-总之还有更好的办法。
优化方案
-就前一种方案存在的不足主要有几个方面:
- 1.可选的分辨率很少,而且如果设置低分辨率的话拍摄过程中也会比较模糊。
- 2.对于封边率问题虽然可以在压缩过程中利用AVMutableComposition来实现,但是存在一个问题是只有视频录制完成以后才能处理。大概需要的步骤是录制->滤镜->码率压缩。而加滤镜的过程中,还是需要取出视频再按帧处理,再存入视频。
-完全可以设计采用一种,AVCapture拿到一帧,交给fiter处理,再利用writer根据setting写入文件。这也是iOS微信小视频优化心得所提供的思路。
-关于根据帧来操作我们可以利用AVCaptureVideoDataOutput和AVCaptureAudioDataOutput来实时的处理。
-而fiter则可以使用ffmpeg、GPUImage、CoreImage来处理。(暂时先不处理,只提供思路)
-最后就设置好参数,利用writer来处理。
总体分析
因为代码比较多,就不贴出来了,需要的可以在这里下载
优化版
-根据上面的分析,对于视频录制部分大致先分成三部分,一部分是读(DWVideoRecoder)、一部分是写(DWVideoWriter)、一部分是预览(DWPreviewView).如下图:
DWPreviewView
-主要是一个预览层,同时还需要处理用户与Session之间的交互
DWVideoRecoder
- Session的配置与控制
- Device的控制与配置
DWVideoWriter
-设置videoSetting和audioSetting的参数,将每一帧通过帧压缩与滤镜过滤之后,写入文件中
-视频具体的参数设置
- VideoOutputSettings
| Key ||
| --- | --- |
| AVVideoCodecKey |编码格式,一般选h264,硬件编码|
| AVVideoScalingModeKey |填充模式,AVVideoScalingModeResizeAspectFill拉伸填充|
| AVVideoWidthKey |视频宽度,以手机水平,home在右边的方向|
| AVVideoHeightKey |视频高度,以手机水平,home在右边的方向|
| AVVideoCompressionPropertiesKey |压缩参数| - AVVideoCompressionPropertiesKey
| Key ||
| --- | --- |
| AVVideoAverageBitRateKey |视频尺寸*比率比率10.1相当于AVCaptureSessionPresetHigh数值越大越精细|
| AVVideoMaxKeyFrameIntervalKey |关键帧最大间隔,1为每个都是关键帧,数值越大压缩率越高|
| AVVideoProfileLevelKey |默认选择AVVideoProfileLevelH264BaselineAutoLevel |
-对于压缩只需要控制比率就可以了
后记
- iOS开发真的是越来越简单了。最开始搜怎么实现的时候直接出现了好几个SDK,大概就是直接导入照着文档写两下就能用的那种。可能自己觉得这样太low所以决定自己尝试一下去实现,觉得有很多收获,视频开发算是入门了吧,写下这篇总结希望能给大家一点帮助,也给自己一个技术沉淀。
番外篇
关于AVFoundation捕捉
-
AVCaptureSession捕捉会话
-
AVCaptureSession从捕捉设备(物理)得到数据流,比如摄像头、麦克风,输出到一个或多个目的地。
-
AVCaptureSession可以动态配置输入输出的线路,在会话进行中按需重新配置捕捉环境。
-
AVCaptureSession可以额外配置一个会话预设值(session preset),用来控制捕捉数据的格式和质量。会话预设值默认为AVCaptureSessionPresetHigh。
-
AVCaptureDevice捕捉设备
-
AVCaptureDevice针对物理硬件设备定义了大量的控制方法,比如控制摄像头的对焦、曝光、白平衡和闪光灯。
-
AVCaptureDeviceInput捕捉设备的输入
-在使用捕捉设备进行处理之前,需要将它添加到捕捉会话的输入。不过一个设备不能直接添加到AVCaptureSession中,需要利用AVCaptureDeviceInput的一个实例封装起来添加。 -
AVCaptureOutput捕捉设备的输出
-如上文所提,AVCaptureSession会从AVCaptureDevice拿数据流,并输出到一个或者多个目的地,这个目的地就是AVCaptureOutput。
-首先AVCaptureOutput是一个基类,AVFoundation为我们提供了四个扩展类。 -
AVCaptureStillImageOutput捕捉静态照片(拍照)
-
AVCaptureMovieFileOutput捕捉视频(视频+音频)
-
AVCaptureVideoDataOutput视频录制数据流
-
AVCaptureAudioDataOutput音频录制数据流
-
AVCaptureVideoDataOutput和AVCaptureAudioDataOutput可以更好的音频视频实时处理
对于以上四者的关系,类似于AVCaptureSession是过滤器,AVCaptureDevice是“原始”材料,AVCaptureDeviceInput是AVCaptureDevice的收集器,AVCaptureOutput就是产物了。
-
AVCaptureConnection捕捉连接
-那么问题来了,上面四者之间的“导管”是什么呢?那就是AVCaptureConnection。利用AVCaptureConnection可以很好的将这几个独立的功能件很好的连接起来。 -
AVCaptureVideoPreviewLayer捕捉预览
-以上所有的数据处理,都是在代码中执行的,用户无法看到AVCaptureSession到底在做什么事情。所以AVFoundation为我们提供了一个叫做AVCaptureVideoPreviewLayer的东西,提供实时预览。 -
AVCaptureVideoPreviewLayer是CoreAnimation的CALayer的子类。
-关于预览层的填充模式有AVLayerVideoGravityResizeAspect、AVLayerVideoGravityResizeAspectFill、AVLayerVideoGravityResize三种
如何查看真机沙盒里面的文件
-
Xcode -> Window -> Devices
-选中真机,再右边选中你要导出沙盒的项目,然后点击最下面的设置按钮,然后Download Container.
可选预设
NSString *constAVCaptureSessionPresetPhoto;
NSString *constAVCaptureSessionPresetHigh;
NSString *constAVCaptureSessionPresetMedium;
NSString *constAVCaptureSessionPresetLow;
NSString *constAVCaptureSessionPreset352x288;
NSString *constAVCaptureSessionPreset640x480;
NSString *constAVCaptureSessionPreset1280x720;
NSString *constAVCaptureSessionPreset1920x1080;
NSString *constAVCaptureSessionPresetiFrame960x540;
NSString *constAVCaptureSessionPresetiFrame1280x720;
NSString *constAVCaptureSessionPresetInputPriority;
iOS开发中的『库』(一)
-因为这篇文章有些问题,所以建议看完之后再看下iOS开发中的『库』(二)这篇文章
看文章之前,你可以看下下面几个问题,如果你都会了,或许可以不看。
- .framework是什么?怎么制作?
-谈一谈自己对动态库和静态库的理解。
-在项目中如何使用动态framework的APP?使用了动态framework的APP能上架Appstore么?
-可以通过framework的方式实现app的热修复么?
我是前言
-最近发现很多人分不清『.framework && .a』、『动态库&&静态库』、『.tbd && .dylib』这几个东西。甚至,还有人一直以误为framework就是动态库!!鉴于网上许多文章都表述的含糊不清,再加上很多文章都比较老了,所以今天写点东西总结一下。
-首先,看文章之前,你稍微了解这么几个东西:编译过程、内存分区。下面开始!
理论篇
动态库VS.静态库
Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime
静态库
-首先你得搞清楚,这两个东西都是编译好的二进制文件。就是用法不同而已。为什么要分为动态和静态两种库呢?先看下图:
动态库
-我们可以很清楚的看到:
-对于静态库而言,在编译链接的时候,会将静态库的所有文件都添加到目标app可执行文件中,并在程序运行之后,静态库与app可执行文件一起被加载到同一块代码区中。
-
app可执行文件:这个目标app可执行文件就是ipa解压缩后,再显示的包内容里面与app同名的文件。
-对于动态库而言,在编译链接的时候,只会将动态库被引用的头文件添加到目标** app可执行文件,区别于静态库,动态库是在程序运行的时候被添加另外一块内存区域。
-下面看下苹果的官方文档中有两句对动态库和静态库**的解释。 - A better approach is for an app to load code into its address space when it’s actually needed, either at launch time or at runtime. The type of library that provides this flexibility is called dynamic library.
- 动态库:可以在运行or启动的时候加载到内存中,加载到一块**独立的于app **的内存地址中
- When an app is launched, the app’s code—which includes the code of the static libraries it was linked with—is loaded into the app’s address space.Applications with large executables suffer from slow launch times and large memory footprints
-
静态库:当程序在启动的时候,会将app的代码(包括静态库的代码)一起在加载到app所处的内存地址上。相比于静态库的方案,使用动态库将花费更多的启动时间和内存消耗。还会增加可执行文件的大小。
-举个🌰:假设UIKit编译成静态库和动态库的大小都看成1M ,加载到内存中花销1s .现在又app1和app2两个app。倘若使用静态库的方式,那么在app1启动的时候,需要花销2s同时内存有2M分配给了app1.同样的道理加上app2的启动时间和内存消耗,采用静态库的方案,一共需要花销4s启动时间、4M内存大小、4M安装包大小。那么换成动态库的时候,对于启动和app1可能花费一样的时间,但是在启动app2的时候不用再加载UIKit动态库了。减少了UIKit的重复使用问题,一共花销3s启动时间、3M内存大小、4M安装包大小。
-而很多app都会使用很多相同的库,如UIKit 、CFNetwork等。所以,苹果为了加快app启动速度、减少内存消耗、减少安装包体积大小,采用了大量动态库的形式来优化系统。dyld的共享缓存:在OS X和iOS上的动态链接器使用了共享缓存,共享缓存存于/var/db/dyld/。对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系。当加载一个Mach-O文件(一个可执行文件或者一个库)时,动态链接器首先会检查共享缓存看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了OS X和iOS上程序的启动时间。
-两者都是由*.o目标文件链接而成。都是二进制文件,闭源。
.framework VS .a
- .a是一个纯二进制文件,不能直接拿来使用,需要配合头文件、资源文件一起使用。在iOS中是作为静态库的文件名后缀。
- .framework中除了有二进制文件之外还有资源文件,可以拿来直接使用。
-在不能开发动态库的时候,其实『.framework =.a + .h + bundle』。而当Xcode 6出来以后,我们可以开发动态库后『.framework =静态库/动态库+ .h + bundle』
.tbd VS .dylib
-对于静态库的后缀名是.a,那么动态库的后缀名是什么呢?
-可以从libsqlite3.dylib这里我们可以知道.dylib就是动态库的文件的后缀名。
-那么.tbd又是什么东西呢?其实,细心的朋友都早已发现了从Xcode7我们再导入系统提供的动态库的时候,不再有.dylib了,取而代之的是.tbd。而.tbd其实是一个YAML本文文件,描述了需要链接的动态库的信息。主要目的是为了减少app的下载大小。具体细节可以看这里
小总结
-首先,相比较与静态库和动态库,动态库在包体积、启动时间还有内存占比上都是很有优势的。
-为了解决.a的文件不能直接用,还要配备.h和资源文件,苹果推出了一个叫做.framework的东西,而且还支持动态库。
Embedded VS. Linked
Embedded frameworks are placed within an app’s sandbox and are only available to that app. System frameworks are stored at the system-level and are available to all apps.
- OK,前面说了那么多,那么如果我们自己开发了一个动态framework怎么把它复制到dyld的共享缓存里面呢?
-一般来说,用正常的方式是不能滴,苹果也不允许你这么做。(当然不排除一些搞逆向的大神通过一些hack手段达到目的)
-那么,我们应该如何开发并使用我们自己开发的动态framework呢?
-那就是Embedded Binaries。 - Embedded的意思是嵌入,但是这个嵌入并不是嵌入app可执行文件,而是嵌入app的bundle文件。当一个app通过Embedded的方式嵌入一个app后,在打包之后解压ipa可以在包内看到一个framework的文件夹,下面都是与这个应用相关的动态framework。在Xcode可以在这里设置,图中红色部分:
Embedded && Link
-那么问题又来了,下面的linded feameworks and libraries又是什么呢?
-首先在linded feameworks and libraries这个下面我们可以连接系统的动态库、自己开发的静态库、自己开发的动态库。对于这里的静态库而言,会在编译链接阶段连接到app可执行文件中,而对这里的动态库而言,虽然不会链接到app可执行文件中,但是会在启动的时候就去加载这里设置的所有动态库。(ps.理论上应该是这样,但是在我实际测试中似乎加载不加载都和这个没关系。可能我的姿势不对。😂)
-如果你不想在启动的时候加载动态库,可以在linded feameworks and libraries删除,并使用dlopen加载动态库。(dlopen不是私有api。)
- (void)dlopenLoad{
NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/Dylib.framework/Dylib",NSHomeDirectory()];
[self dlopenLoadDylibWithPath:documentsPath];
}
- (void)dlopenLoadDylibWithPath:(NSString *)path
{
libHandle = NULL;
libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
if (libHandle == NULL) {
char *error = dlerror();
NSLog(@"dlopen error: %s", error);
} else {
NSLog(@"dlopen load framework success.");
}
}
关于制作过程
-关于如何制作,大家可以看下raywenderlich家的经典教程《How to Create a Framework for iOS》,中文可以看这里《创建你自己的Framework》
-阅读完这篇教程,我补充几点。
-首先,framework分为Thin and Fat Frameworks。Thin的意思就是瘦,指的是单个架构。而Fat是胖,指的是多个架构。
-要开发一个真机和模拟器都可以调试的Frameworks需要对Frameworks进行合并。合并命令是lipo
lipo。
-如果app要上架appstore在提交审核之前需要把Frameworks中模拟器的架构给去除掉。
-个人理解,项目组件化或者做SDK的时候,最好以framework的形式来做。
实践篇
framework的方式实现app的热修复
-由于Apple不希望开发者绕过App Store来更新app,因此只有对于不需要上架的应用,才能以framework的方式实现app的更新。
-但是理论上只要保持签名一致,在dlopen没有被禁止的情况下应该是行的通的。(因为没有去实践,只能这样YY了。)
-但是不论是哪种方式都得保证服务器上的framework与app的签名要保持一致。
实现大致思路
-下载新版的framework
-先到document下寻找framework。然后根据条件加载bundle or document里的framework。
NSString *fileName = @"remote";
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = nil;
if ([paths count] != 0) {
documentDirectory = [paths objectAtIndex:0];
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *bundlePath = [[NSBundle mainBundle]
pathForResource:fileName ofType:@"framework"];
BOOL loadDocument = YES;
// Check if new bundle exists
if (![manager fileExistsAtPath:bundlePath] && loadDocument) {
bundlePath = [documentDirectory stringByAppendingPathComponent:[fileName stringByAppendingString:@".framework"]];
}
-再加载framework
// Load bundle
NSError *error = nil;
NSBundle *frameworkBundle = [NSBundle bundleWithPath:bundlePath];
if (frameworkBundle && [frameworkBundle loadAndReturnError:&error]) {
NSLog(@"Load framework successfully");
}else {
NSLog(@"Failed to load framework with err: %@",error);
}
-加载类并做事情
// Load class
Class PublicAPIClass = NSClassFromString(@"PublicAPI");
if (!PublicAPIClass) {
NSLog(@"Unable to load class");
}
NSObject *publicAPIObject = [PublicAPIClass new];
[publicAPIObject performSelector:@selector(mainViewController)];
番外篇
关于lipo
$ lipo -info /Debug-iphoneos/Someframework.framwork/Someframework
# Architectures in the fat file: Someframework are: armv7 armv7s arm64
#合并
$ lipo –create a.framework b.framework –output output.framework
#拆分
$ lipo –create a.framework -thin armv7 -output a-output-armv7.framework
从源代码到app
当我们点击了build之后,做了什么事情呢?
-预处理(Pre-process):把宏替换,删除注释,展开头文件,产生.i文件。
-编译(Compliling):把之前的.i文件转换成汇编语言,产生.s文件。
-汇编(Asembly):把汇编语言文件转换为机器码文件,产生.o文件。
-链接(Link):对.o文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个.o文件进行link)。
ld && libtool
- ld :用于产生可执行文件。
- libtool:产生lib的工具。
Build phases &&Build rules && Build settings
- Build phases:主要是用来控制从源文件到可执行文件的整个过程的,所以应该说是面向源文件的,包括编译哪些文件,以及在编译过程中执行一些自定义的脚本什么的。
- Build rules:主要是用来控制如何编译某种类型的源文件的,假如说相对某种类型的原文件进行特定的编译,那么就应该在这里进行编辑了。同时这里也会大量的运用一些xcode中的环境变量,完整的官方文档在这里:Build Settings Reference
- Build settings:则是对编译工作的细节进行设定,在这个窗口里可以看见大量的设置选项,从编译到打包再到代码签名都有,这里要注意settings的section分类,同时一般通过右侧的inspector就可以很好的理解选项的意义了。
谈谈Mach-O
Mach-O-在制作framework的时候需要选择这个Mach-O Type.
-为Mach Object文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。
参考资料
后记
-水平有限,若有错误,希望多多指正!coderonevv@gmail.com
@我就叫Sunny怎么了提出的问题。
静态库链接过程-我已在iOS开发中的『库』(二)中修改完毕。
iOS开发中的『库』(二)
其实这是一篇纠(da)错(lian)篇
看文章之前,你可以看下下面几个问题,如果你都会了,或许可以不看。
-再谈一谈动态库和静态库。你真的知道XXXX和XXX系列。
-为什么使用动态库的方式来动态更新只能用在in house和develop模式却不能在使用到AppStore上呢?
-动态库到底会添加到内存中几次?
我是前言
-其实这篇文章准备明天再写的,但是看到@我就叫Sunny怎么了大半夜还帮我指出问题,睡觉这个东西,感觉还是先放一放吧。
-像我这种严重拖延症患者来说,关于iOS开发中的『库』(二),更新速度已经史无前例了。(我好像也没什么系列啊?哈哈哈哈)
-主要是iOS开发中的『库』(一)这篇文章确实有不少错误,需要弥补一下。
-当然,最需要感谢的还是@Casa Taloyum大神在我写完第一篇当天晚上给我提出了一些文中的问题,并耐心的解答了我的一些小疑问。
- ok,废话不多说,我们先开始吧。
纠错篇
静态库的处理方式
-对于一个静态库而言,其实已经是编译好的了,类似一个.o的集合(这里并没有连接,在iOS开发中的『库』(一)所描述的链接其实不对)。在build的过程中只会参与链接的过程,而这个链接的过程简单的讲就是合并,并且链接器只会将静态库中被使用的部分合并到可执行文件中去。相比较于动态库,静态库的处理起来要简单的多,具体如下图:
-链接器会将所有.o用到的global symbol和unresolved symbol放入一个临时表,而且是global symbol是不能重复的。
-对于静态库的.o ,连接器会将没有任何symbol在unresolved symbol table的给忽略。
-
unresolved symbol类似
extern int test();
--- **.h **的声明? -
global symbol类似
void test() { print("test")}
--.m的实现?
-最后,链接器会用函数的实际地址来代替函数引用。
所以,在iOS开发中的『库』(一)所提到,将头文件添加到可执行文件是不正确的。
动态库的处理方式
-首先,对于动态库而言其实分动态链接库和动态加载库两种的,这两个最本质的区别还是加载时间。
- 动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。在Linked Framework and Libraries设置的一些share libraries。【随着程序启动而启动】
-
动态加载库:当需要的时候再使用dlopen等通过代码或者命令的方式来加载。【在程序启动之后】
-但是不论是哪种动态库,相比较与静态库,动态库处理起来要棘手的多。由于动态库是动态的,所以你事先不知道某个函数的具体地址。因此动态链接器在链接函数的时候需要做大量的工作。
因为动态库在链接函数需要做大量的工作,而静态库已经实现处理好了。所以单纯的在所有都没有加载的情况下,静态库的加载速度会更快一点。而在iOS开发中的『库』(一)提到的有所不妥,正确应该是,虽然动态库更加耗时,但是对于在加载过的share libraries不需要再加载的这个前提下,使用动态库可以节省一些启动时间。
-而实现这个动态链接是使用了Procedure Linkage Table (PLT)。首先这个PLT列出了程序中每一个函数的调用,当程序开始运行,如果动态库被加载到内存中,PLT会去寻找动态的地址并记录下来,如果每个函数都被调用过的话,下一次调用就可以通过PLT直接跳转了,但是和静态库还是有点区别的是,每一个函数的调用还是需要通过一张PLT。这也正是sunny所说的所有静态链接做的事情都搬到运行时来做了,会导致更慢的原因。
动态库到底在内存哪块区域的问题
-其实这个命题最开始就跑偏了,在和@酷酷的哀殿等几个小伙伴讨论未果之后,在老司机@Casa Taloyum大神的点拨下,明白了问题出在了哪里。
-首先,不管是静态库、动态库,两者的区别和在内存哪个区域没有关系,最本质的区别是,一个的函数调用等在编译时候就已经确定,而动态库是动态加载的。换句话说,静态库修改了东西,整个程序需要重新编译,而对于动态库的修改而言,只需要重启app(重置PLT)。
-至于在内存的哪个区域,和是静态库 or 动态库没有关系。代码段、数据段这些,都是程序加载时就进入的。堆一般是文件buffer分配、对象初始化等时候用的。栈是函数出入口指针,局部常规变量用的。只要malloc都在堆里。具体的可以参照这里
-还需要提一下的是,如果是动态加载库,那么在没有加载的时候,代码段、数据段这些也是不会加载进去的。lazy load。
.a的使用
-@我就叫Sunny怎么了提出的这个问题:
.a是一个纯二进制文件,不能直接拿来使用,需要配合头文件、资源文件一起使用。
-可能他没有理解我的意思,我所说的资源文件,是如图片这类的。而他说可以不使用头文件的形式链接一个.a,好吧,我这还真不知道,但是常规使用下,使用.a还是需要配合头文件和资源文件一起的,所以相比之下使用framework更方便。
关于动态framework是否需要去除模拟器架构问题
-首先,这个理论上没有问题的,但是我自己遇到过这个坑,再加上在这里也提到了这个问题,所以保险条件下最好去掉。(感觉这是个苹果的bug)
拓展篇
动态库动态更新问题
能否动态库的方式来动态更新AppStore上的版本呢?
-原本是打算国庆的时候试一试AppStore上到底行不行的,结果还是托@Casa Taloyum大神老司机的服,他已经踩过这个坑了,他的结论是:使用动态库的方式来动态更新只能用在in house和develop模式却但不能在使用到AppStore。
-因为在上传打包的时候,苹果会对我们的代码进行一次Code Singing,包括app可执行文件和所有Embedded的动态库。因此,只要你修改了某个动态库的代码,并重新签名,那么MD5的哈希值就会不一样,在加载动态库的时候,苹果会检验这个hash值,当苹果监测到这个动态库非法时,就会造成Crash。
所以在iOS开发中的『库』(一)提到理论上是可行的这点也是不对的。
动态库到底添加几次问题
-这个问题毋庸置疑,肯定是一次。
app1和app2都有一个相同的动态framework以Embedded方式放入到各自的app中,问这个动态framework会加载几次?
-当然,不是一次,是两次。但是这不是和前面说的相违背了么,其实并不是违背,只是前面说的一次不妥当,最妥当的应该这么说:对于相同路径的动态库,系统只会加载一次。
动态库加载过程
番外篇
摘抄自个人笔记。
关于内存五大分区
- BSS段:
- BSS段( bss segment )通常是指用来存放程序中
未初始化的全局变量和静态变量
的一块内存区域。
-这里注意一个问题:一般的书上都会说全局变量和静态变量是会自动初始化的,那么哪来的未初始化的变量呢?变量的初始化可以分为显示初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话的确也会被初始化,那就是不管什么类型都初始化为0,这种没有显示初始化的就是我们这里所说的未初始化。既然都是0那么就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用
- BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小。以便内存区能在运行时分配并被有效地清零。BSS节在应用程序的二进制映象文件中并不存在,即不占用磁盘空间而只在运行的时候占用内存空间,
所以如果全局变量和静态变量未初始化那么其可执行文件要小很多
。 -
数据段(data segment)**
-通常是指用来存放程序中已经初始化的全局变量和静态变量
的一块内存区域。数据段属于静态内存分配,可以分为只读数据段
和读写数据段
。字符串常量等,但一般都是放在只读数据段中。 -
代码段(code segment/text segment)**
-通常是指用来存放程序执行代码的一块内存区域
。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,但一般都是放在只读数据段中。 -
堆(heap)**
-堆是用于存放进程运行中被动态分配的内存段
,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) -
栈(stack heap)**
-栈又称堆栈,是用户存放程序临时创建的局部变量
,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段
中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
内存分区
小Tips
-栈区中的变量不需要程序员管理
-堆区的需要程序员管理
-在iOS中堆区的内存是所有应用程序共享
-系统使用表级别结构来分配内存空间,所以逻辑地址和物理地址可能不一样
参考文献
- iOS应用架构谈本地持久化方案及动态部署
- 库
-
Dissecting shared libraries
-水平有限,若有错误,希望多多指正!coderonevv@gmail.com
Swift 3迁移工作总结
写在前面
- Swift 3.0正式版发布了差不多快一个月了,断断续续的把手上和Swift相关的迁移到了Swift 3.0。所以写点小总结。
背景
代码量(4万行)
-首先,我是今年年初才开始入手Swift的。加上Swift的ABI和API一直不稳定,所以没有在项目中大范围的使用,所以这次迁移的代码量不多,大概在4万行左右。
迁移时间(一天左右)
-迁移时间上的话,大概是花了1天左右。两个混编项目,一个Swift为主的项目。期中Swift为主的项目花了大概大半天时间,两个混编代码量差不多,但是一个花了小半天,还有一个差不多只花了半个小时(原因先留个悬念~)。
准备
在开发最初开发选择Swift的时候的很多决策也让我这次少了很多工作量。
界面用xib而不用纯代码
-阴差阳错的,和Swift相关的大部分界面都是用xib画的。而这个xib在这次迁移中得到了很大的优势,xib和SB的代码不适配Swift 3。想当初要是使用代码写的UI的话,这次迁移改动估计会多很多吧。
关于第三方库的选择:
-对于一个项目来说,三方库似乎成了一道必选菜,但是如何去选择这道菜呢?
-对于三方库,当初的选择是,能用OC就尽量用OC。毕竟可以OC可以无缝衔接到Swift,而且还相对稳定。
-在选择Swift相关的三方库时,我尽量值选择使用者比较多的库,例如Alamofire、Snap、Kingfisher、Fabric等,因为使用者比较多,开发者会更愿意去维护,而不至于跳票。所以不会存在现在许多小伙伴面临的问题,想迁移,但是有些库没有更新。至少对于我来说,当我想迁移的时候,所有和Swift相关的三方库都已经迁移到了3.0了。
得益于上面两点,在迁移过程中少了不少工作量。🙈
知识储备升级
-先了解了一下Swift 2到Swift 3的变动,及变动的原因。(看完心中一万头草泥马飞过,但是其实是越来越好了)
- Swift官博
- swift-evolution
-
Swift 3新特性一览
-然后把语法文档快速的重温了一遍。 - Swift Programming Language
- 中文版
迁移中的问题
Any && AnyObject
-我想在做迁移和做完迁移的同学改的最多的一个就是as AnyObjct?
吧?
-至少对于我来说是的。
-和这个相关的基本是集合类型。在Swift 2中我们一个用[AnyObject]来存放任何变量,甚至于存放struct类型的String
、Array
等。但是按道理Swift的AnyObject指的是类,而Any才是包括struct
、class
、func
等所有类型。但是为何Struct可以放入[AnyObject]呢?在Swift 2的时候会针对String、Int等Struct进行一个Implicit Bridging Conversions。而到了Swift 3则进行了一个**Fully eliminate implicit bridging conversions from Swift改动。
-当然在我的项目中[AnyObject]其实是小事,最麻烦的就是[String:AnyObject]。因为当初写项目的时候,还是处于OC To Swift的阶段所以对于Dictionary,基本采用了[String:AnyObject],所以在修改的时候,在很多地方为了这个修改。
-起初,我是照着Xcode的提示,在Dictionary后面的value后面加了一个as AnyObjct?
-后来渐渐的发现我做了一件很傻比的事情,其实我只要把[String:AnyObject]改为[String:Any]就可以了。😂
-这也就是为什么在第一混编的项目中我花了那么多时间去修改代码了!得益于混编的第二个项目学习了Yep的思路,是把[String:AnyObject]
命名为一个叫做JSONDictionary
的类型。所以在Any && AnyObect**这个事情上,就花了一点点时间。
// Swift 2
var json = [String:AnyObect]()
json["key1"] = 1
json["key2"] = "2"
// to Swift 3 Step 1
var json = [String:AnyObect]()
json["key1"] = 1 as AnyObject?
json["key2"] = "2" as AnyObject?
// to Swift 3 Step 2
var json = [String:Any]()
json["key1"] = 1
json["key2"] = "2"
// Swift 2
public typealias JSONDictionary = [String: AnyObject]
// To Swift 3 Step 2
public typealias JSONDictionary = [String: Any]
Alamofire等三方库支持iOS8
-虽然说我使用的三方库都在第一时间将库升级到了Swift 3,但是期中Alamofire和Snap两个库最低适配只支持到了iOS 9,为了避免和产品撕逼,不得不想办法解决这个适配问题。下面以Alamofire为例
-其实三方库么,不一定只用Cocoapods的。所以打算下载代码然后直接撸源码。
-先Alamofire的Xcode修改为最低适配8.0,然后编译查找不通过的函数,并删除。(其实这些函数都是iOS 9新加的函数,所以删除不影响什么。)
-大概花了半个小时左右就可以删完了,然后直接拖到项目中就可以了~
- Snap其实只要拖进去就好了,暂时不需要修改什么。
//其实都是!os(watchOS)这个宏下面的
#if !os(watchOS)
@discardableResult
public func stream(withHostName hostName: String, port: Int) -> StreamRequest {
return SessionManager.default.stream(withHostName: hostName, port: port)
}
@discardableResult
public func stream(with netService: NetService) -> StreamRequest {
return SessionManager.default.stream(with: netService)
}
#endif
@escaping
-这个是我在适配中最蛋疼的坑
-首先在看swift-evolution只是了解到@escaping必须显示声明。但是不知道@escaping的闭包,在函数体内无法再修改。
let pedonmeter:CMPedometer = CMPedometer()
func getPedometerDataFromDate(_ datet:Date?, withHandler handler: @escaping (CMPedometerData?, Error?) -> ()){
//编译错误
pedonmeter.queryPedometerDataFromDate(startTime, toDate:endTime, withHandler: { (pedometerData:CMPedometerData?, error:NSError?) -> Void in
guard let pedometerData = pedometerData else { return }
handler(pedometerData, error)
//做一些事情
})
//最后逼不得已只能不修改了,函数外面就做一些事情了
pedonmeter.queryPedometerData(from: startTime, to: endTime, withHandler:handler as! CMPedometerHandler)
}
Result of call to 'funtion' is unused
-这其实不是一个编译错误,但是这个警告最开始让我有点懵逼.返回值不用难道要我都修改一下?
-最开始其实我是这么修改的let _ = funtion()
,但是后面在看SE-0047的时候发现@discardableResult
也是可以达到这个效果的。
Date && NSDate
-因为有个项目中使用的DateTools这个工具。它有一个NSDate + Tools的分类。
-但是在写Swift 3的过程中我发现如果变量是Date类型的无法使用NSDate + Tools这个类型,必须显示声明date as NSDate这样才能调用分类的一些个方法。
-这个让使用OC的库的时候会感觉十分不舒服,毕竟很多NS的前缀去掉了。所有都显示声明太不友好了。
CAAnimationDelegate
-这个其实好像是Xcode 8的修改。因为之前CAAnimationDelegate是一个分类。大概声明如下:
@interface NSObject (CAAnimationDelegate)
- (void)animationDidStart:(CAAnimation *)anim;
-
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;
@end
-之前是在vc中只要重写一下animationDidStart
函数就可以了。但是新的不行,起初以为是Swift 3的变化,但是其实是Xcode 8中的修改。将CAAnimationDelegate变成了一个协议。我感觉这个修改是为了适配Swift 3?变化如下:
@protocol CAAnimationDelegate
@optional
- (void)animationDidStart:(CAAnimation *)anim;
-
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;
@end
因为宽度时间比较长,其他的暂时想不到了。未完待续吧...
其他
-还有许多微妙的变化让你似乎看不懂这个语言了,所以建议在适配之前看一下下面的文章。
- Swift 3新特性一览
- [Swift 3.0 - Released on September 13, 2016]https://github.com/apple/swift-evolution/blob/master/releases/swift-3_0.md
-还有@卓同学的Swift 3必看系列
-还有几个不错的总结 - Swift 3 by 顾鹏
- 适配Swift 3的一点小经验和坑 by 图拉鼎
总结
-总的说来这次迁移没有想象中的那么痛苦,虽然提案的改动很大,但是得益于Xcode 8的迁移工具,这次迁移花费时间不多,当然也有可能和我的代码量有关系~
-在迁移完之后,再看代码,会发现Swift更加的优雅了,至少相比于2来说好了很多,至于好在哪里?你自己写写不就知道了咯。
-最后,终于可以把Xocde 7卸载,再也不用担心两个一起开无脑闪退了!!!
-最后对于明年的Swift 4只想说快来吧~分分钟把你解决!
-其实适配之路才刚刚开始,因为Xcode 8自动转的代码并没有很好的Swift 3化。目前只是说在Swift 3可以编译通过了而已~