音频iOS开发资料收集音视频

视频录制

2017-02-16  本文已影响3333人  Carden

最终诉求?

拍摄、保存、播放、上传。就这四个步骤,当然首先拍摄就有许许多多的优化小功能,切换摄像头、单击跳帧焦距、双击远近距离切换、上移取消拍摄。

保存功能就只有一个保存到本地沙盒的功能。

播放自然就涉及到原生或是三方库来播放的问题,用什么第三方库来播放。

上传就有几种情况了,第一种上传到聊天服务器,第二种上传到七牛云。

持续录制?

进度条的逻辑?

上移取消?

结束录制?

微信小视频逻辑?

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手势。

视频捕获逻辑?

摄像头和麦克风授权?

访问相机和麦克风需要先获得用户授权,授权的状态包括用户未选择、用户想授权却没资格、用户拒绝授权、用户同意授权。

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的文件
属性:是否正在录制、是否暂停录制、当前录制时间、录制最长时间、代理回调引擎处理结果、视频保存路径、预览图层

方法?

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入门

CocoaPods相关

iOS开发中的『库』

Swift Tips

仿微信小视屏iOS技术路线实践笔记[录制篇]

一周之前拿到这个需求时,我当时是懵逼的,因为自己对视频这一块几乎可以说是一无所知。在断断续续一周的研究过程之后,准备写点笔记记录一下。

需求分析

-对于一个类似微信小视屏的功能,大致需要完成的功能无非就是两块:
-视频录制
-视频播放


先讲讲视频录制-技术路线

(因为自己对视频是个小白,只能借助谷歌来搜索一些相关技术,一定有什么不对的地方)
-在iOS中与视频录制相关的技术大概有三种:

看上去很懵逼是不是,其实我也是懵逼的。更甚至于AVFoundation和ffmpeg两者关系我最开始都摸不透。如果你和我一样懵逼可以看一下。我写的AVFoundation和视频捕捉相关的总结。ffmpeg则需要去看雷神的博客了,很详细,也很入门。
-对于以上三种,首先UIImagePickerController肯定不在考虑范围之内了,可定制化太低。
-对于利用相机录取视频只能用AVFoundationAVCaptureSession来捕捉。


废话不多说,上代码。

对于AVFoundation捕捉只是还不是很清楚的可以点击这里查看。

录制前的准备工作

-第(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?添加到AVCaptureSessionAVCaptureDeviceInput ?中。

///初始化捕捉输入
- (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];//停止录制
}
}

查看录制视频

-关于如何查看沙盒内容可以点击这里
-拿到的视频大概8S15.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微信小视频优化心得说这样很耗时。所有对视频的处理都需要在录制完成之后来做。
-总之还有更好的办法。


优化方案

-就前一种方案存在的不足主要有几个方面:

总体分析

因为代码比较多,就不贴出来了,需要的可以在这里下载
-根据上面的分析,对于视频录制部分大致先分成三部分,一部分是读(DWVideoRecoder)、一部分是写(DWVideoWriter)、一部分是预览(DWPreviewView).如下图:

优化版优化版

DWPreviewView

-主要是一个预览层,同时还需要处理用户与Session之间的交互

DWVideoRecoder

DWVideoWriter

-设置videoSetting和audioSetting的参数,将每一帧通过帧压缩与滤镜过滤之后,写入文件中
-视频具体的参数设置


后记


番外篇

关于AVFoundation捕捉

对于以上四者的关系,类似于AVCaptureSession是过滤器,AVCaptureDevice是“原始”材料,AVCaptureDeviceInput是AVCaptureDevice的收集器,AVCaptureOutput就是产物了。

如何查看真机沙盒里面的文件

可选预设

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 && .a』、『动态库&&静态库』、『.tbd && .dylib』这几个东西。甚至,还有人一直以误为framework就是动态库!!鉴于网上许多文章都表述的含糊不清,再加上很多文章都比较老了,所以今天写点东西总结一下。
-首先,看文章之前,你稍微了解这么几个东西:编译过程、内存分区。下面开始!


理论篇

动态库VS.静态库

Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime
-首先你得搞清楚,这两个东西都是编译好的二进制文件。就是用法不同而已。为什么要分为动态和静态两种库呢?先看下图:

静态库静态库
动态库动态库
-我们可以很清楚的看到:
-对于静态库而言,在编译链接的时候,会将静态库所有文件都添加到目标app可执行文件中,并在程序运行之后,静态库app可执行文件一起被加载到同一块代码区中。

.framework VS .a

.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.

- (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进行合并。合并命令是lipolipo
-如果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

Build phases &&Build rules && Build settings


谈谈Mach-O

Mach-OMach-O

-在制作framework的时候需要选择这个Mach-O Type.
-为Mach Object文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。


参考资料


后记

-水平有限,若有错误,希望多多指正!coderonevv@gmail.com

@我就叫Sunny怎么了提出的问题。

静态库链接过程静态库链接过程
-我已在iOS开发中的『库』(二)中修改完毕。

iOS开发中的『库』(二)

其实这是一篇纠(da)错(lian)篇

看文章之前,你可以看下下面几个问题,如果你都会了,或许可以不看。

-再谈一谈动态库和静态库。你真的知道XXXX和XXX系列。
-为什么使用动态库的方式来动态更新只能用在in housedevelop模式却不能在使用到AppStore上呢?
-动态库到底会添加到内存中几次?


我是前言

-其实这篇文章准备明天再写的,但是看到@我就叫Sunny怎么了大半夜还帮我指出问题,睡觉这个东西,感觉还是先放一放吧。
-像我这种严重拖延症患者来说,关于iOS开发中的『库』(二),更新速度已经史无前例了。(我好像也没什么系列啊?哈哈哈哈)
-主要是iOS开发中的『库』(一)这篇文章确实有不少错误,需要弥补一下。
-当然,最需要感谢的还是@Casa Taloyum大神在我写完第一篇当天晚上给我提出了一些文中的问题,并耐心的解答了我的一些小疑问。


纠错篇

静态库的处理方式

-对于一个静态库而言,其实已经是编译好的了,类似一个.o的集合(这里并没有连接,在iOS开发中的『库』(一)所描述的链接其实不对)。在build的过程中只会参与链接的过程,而这个链接的过程简单的讲就是合并,并且链接器只会将静态库中被使用的部分合并到可执行文件中去。相比较于动态库,静态库的处理起来要简单的多,具体如下图:

静态库链接过程静态库链接过程
-链接器会将所有.o用到的global symbolunresolved symbol放入一个临时表,而且是global symbol是不能重复的。
-对于静态库的.o ,连接器会将没有任何symbolunresolved symbol table的给忽略。

所以,在iOS开发中的『库』(一)所提到,将头文件添加到可执行文件是不正确的。

动态库的处理方式

-首先,对于动态库而言其实分动态链接库动态加载库两种的,这两个最本质的区别还是加载时间。

因为动态库在链接函数需要做大量的工作,而静态库已经实现处理好了。所以单纯的在所有都没有加载的情况下,静态库的加载速度会更快一点。而在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 housedevelop模式却但不能在使用到AppStore
-因为在上传打包的时候,苹果会对我们的代码进行一次Code Singing,包括app可执行文件和所有Embedded的动态库。因此,只要你修改了某个动态库的代码,并重新签名,那么MD5的哈希值就会不一样,在加载动态库的时候,苹果会检验这个hash值,当苹果监测到这个动态库非法时,就会造成Crash。
所以在iOS开发中的『库』(一)提到理论上是可行的这点也是不对的。

动态库到底添加几次问题

-这个问题毋庸置疑,肯定是一次。

app1和app2都有一个相同的动态framework以Embedded方式放入到各自的app中,问这个动态framework会加载几次?
-当然,不是一次,是两次。但是这不是和前面说的相违背了么,其实并不是违背,只是前面说的一次不妥当,最妥当的应该这么说:对于相同路径的动态库,系统只会加载一次。


动态库加载过程动态库加载过程

番外篇

摘抄自个人笔记。

关于内存五大分区


内存分区内存分区

小Tips

-栈区中的变量不需要程序员管理
-堆区的需要程序员管理
-在iOS中堆区的内存是所有应用程序共享
-系统使用表级别结构来分配内存空间,所以逻辑地址和物理地址可能不一样


参考文献

Swift 3迁移工作总结

写在前面

背景

代码量(4万行)

-首先,我是今年年初才开始入手Swift的。加上Swift的ABI和API一直不稳定,所以没有在项目中大范围的使用,所以这次迁移的代码量不多,大概在4万行左右。

迁移时间(一天左右)

-迁移时间上的话,大概是花了1天左右。两个混编项目,一个Swift为主的项目。期中Swift为主的项目花了大概大半天时间,两个混编代码量差不多,但是一个花了小半天,还有一个差不多只花了半个小时(原因先留个悬念~)。

准备

在开发最初开发选择Swift的时候的很多决策也让我这次少了很多工作量。

界面用xib而不用纯代码

-阴差阳错的,和Swift相关的大部分界面都是用xib画的。而这个xib在这次迁移中得到了很大的优势,xib和SB的代码不适配Swift 3。想当初要是使用代码写的UI的话,这次迁移改动估计会多很多吧。

关于第三方库的选择:

-对于一个项目来说,三方库似乎成了一道必选菜,但是如何去选择这道菜呢?
-对于三方库,当初的选择是,能用OC就尽量用OC。毕竟可以OC可以无缝衔接到Swift,而且还相对稳定。
-在选择Swift相关的三方库时,我尽量值选择使用者比较多的库,例如AlamofireSnapKingfisherFabric等,因为使用者比较多,开发者会更愿意去维护,而不至于跳票。所以不会存在现在许多小伙伴面临的问题,想迁移,但是有些库没有更新。至少对于我来说,当我想迁移的时候,所有和Swift相关的三方库都已经迁移到了3.0了。

得益于上面两点,在迁移过程中少了不少工作量。🙈

知识储备升级

-先了解了一下Swift 2到Swift 3的变动,及变动的原因。(看完心中一万头草泥马飞过,但是其实是越来越好了)

迁移中的问题

Any && AnyObject

-我想在做迁移和做完迁移的同学改的最多的一个就是as AnyObjct?吧?
-至少对于我来说是的。
-和这个相关的基本是集合类型。在Swift 2中我们一个用[AnyObject]来存放任何变量,甚至于存放struct类型的StringArray等。但是按道理Swift的AnyObject指的是类,而Any才是包括structclassfunc等所有类型。但是为何Struct可以放入[AnyObject]呢?在Swift 2的时候会针对StringIntStruct进行一个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,但是期中AlamofireSnap两个库最低适配只支持到了iOS 9,为了避免和产品撕逼,不得不想办法解决这个适配问题。下面以Alamofire为例
-其实三方库么,不一定只用Cocoapods的。所以打算下载代码然后直接撸源码。
-先Alamofire的Xcode修改为最低适配8.0,然后编译查找不通过的函数,并删除。(其实这些函数都是iOS 9新加的函数,所以删除不影响什么。)
-大概花了半个小时左右就可以删完了,然后直接拖到项目中就可以了~

//其实都是!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

因为宽度时间比较长,其他的暂时想不到了。未完待续吧...

其他

-还有许多微妙的变化让你似乎看不懂这个语言了,所以建议在适配之前看一下下面的文章。

总结

-总的说来这次迁移没有想象中的那么痛苦,虽然提案的改动很大,但是得益于Xcode 8的迁移工具,这次迁移花费时间不多,当然也有可能和我的代码量有关系~
-在迁移完之后,再看代码,会发现Swift更加的优雅了,至少相比于2来说好了很多,至于好在哪里?你自己写写不就知道了咯。
-最后,终于可以把Xocde 7卸载,再也不用担心两个一起开无脑闪退了!!!
-最后对于明年的Swift 4只想说快来吧~分分钟把你解决!
-其实适配之路才刚刚开始,因为Xcode 8自动转的代码并没有很好的Swift 3化。目前只是说在Swift 3可以编译通过了而已~

上一篇 下一篇

猜你喜欢

热点阅读