浅谈iOS的结构设计+附带原生H264编解码

291 阅读14分钟

对于团队合同开发,最好统一环境。例如ruby,xcode cocoapod版本,对于版本的升级,最好团队里安排一个专门负责吃螃蟹的,进行新版本或新特性的体验,再进行团队技术分享。

架构:

前提:model是主管业务模型的数据和行为。数据的(网络)请求、本地(数据库或缓存)读取都是在此中model的范畴之内,基于数据分解的业务逻辑层、网络工具、存储层都是其中。view是与UI视图相关的层级,用于展示接收到的业务数据(model中来的),或者接收用户的交互行为发出某种事件。

mvc:苹果推荐的一种开发架构,将Model和View分离在两个互相独立的空间中,通过在VC层进行逻辑操作,将Model中的数据渲染到View层的UI上或者将View层级的UI事件产生的数据变动同步到model层中。这种方式的实现,随着功能的不断添加,逻辑处理层会导致单个vc的代码越来越多变得臃肿,阅读性比较低,且不易管理。

mvp:此架构中的p相当于mvc中的v层级的存在,而UIVewController被划分为View层了。有了p层的存在,viewController类的代码就相对比较清晰。

m-v-vm:vm(Model of View)是视图的模型,封装的是视图的表示逻辑和数据,专门学习了下,说是包括视图的属性和命令或视图的状态和行为,感觉太抽象。从我个人项目中的实践经验理解,比较突出的一点是实现了双向绑定功能。在vm层,可以定义数据的网络请求行为,实现对数据基于不同业务粒度的分解以及派发业务数据到指定的功能视图上,可以对UI交互产生的事件也可做到基于Model层的即时响应。结合RxSwift是极佳的体验。

viper: 有明确的职责分离和解耦,模块职责清晰。\

(1) view:负责用户界面的展示与响应用户操作,将事件传递给Presenter。\

(2) interactor: 负责业务逻辑,它提供数据和功能的接口,供presenter使用。\

(3) Presenter: 负责将View和Interactor连接起来,它接收View传递过来的用户操作,然后根据业务逻辑调用Interactor提供的接口,再将处理结果展示到View上。\

(4) Entity:负责存储数据。\

(5) Router:负责处理不同模块之间的跳转和导航,它根据Presenter的指令,决定展示哪个View,同时也负责数据的传递。

一套好的app设计方案,应该可以根据需求的变化而不断地进行演进,其遵循着通用的设计原则,以项目结构清晰作为团队发展的协作基准。本人项目工作中的经验,以严格的方式遵照某种架构设计是有点困难的,随着项目的不断庞大,最终以单一功能为组件进行的集成开发,可能是一个不错的协作方式,但这也引入其他的问题,例如共享数据的读取、通用功能的api调用,模块之间的接口约定。这就考验作为架构师对整体架构下的子功能模块间逻辑协同的设计能力,同时还要把握app编译+启动流程的整个逻辑时机以提高app启动性能。\

特别提醒的是,我们还要考虑因为政策方面的事宜,对app运营产生的影响。例如918的悼念,这个日子的app界面搭配;或者突发的重大事件例如512地震,能够及时进行app上的相应消息同步。

多环境:

通过Build Configuration 和 Manage Schemes的方式管理多环境。通过一个Scheme对应一个xcconfig文件的方式,完成一个环境下的Build Settings的配置。可以在这个xcconfig文件中定义一个变量例如PRIMARY_KEY,在info.plist文件中配置对应的Key:"PRIMARY_KEY"- Value: "${PRIMARY_KEY}"键值对,然后可通过读取info.plist文件的方式拿到xcconfig中的"PRIMARY_KEY"的值。\

也可以通过在一个Project中构建多个Target(修改appIcon、info-plist )的方式实现多环境。\

如果想在Debug阶段查看导出符号,可在xcconfig文件中进行配置"objcdump --macho --exports-trie ##MACHO-Path##"命令和"TTY=##终端窗口位置##",然后command+B之后即可看到终端对应窗口出现的导出符号。

函数响应式编程:RXswift

RXswift有几个核心点: \

Observable:产生事件; \

ObserVer:响应事件; \

Operator:创建变化组合事件;\

Disposable:管理绑定(订阅)的生命周期;\

Schedulers:线程队列调配。\

具体详情,可查看[RxSwift中文文档](beeth0ven.github.io/RxSwift-Chi…

CocoaPod管理第三方库(略)

结构设计:

swiftLint: 代码语法检查,用于规范代码。此pod库的安装,只安装在development环境。在Build Phases里面添加一个Run SwiftLint.并配置"${PODS_ROOT}/SwiftLint/swiftlint"。之后,每次编译之后,SwiftLint都会自动执行检查。

国际化语言:在PROJECT->info->Localization添加想要支持的新语言,确认添加之后,可以在Resource中看到刚刚添加的.strings文件。\

.strings文件是一个资源文件,用于存储各种语言的文本。 \

自动生成代码的管理工具:SwiftGen

字体动态化: 

ULabel的动态字体支持的代码


let lab = UILabel()

lab.font = UIFont.preferredFont(forTextStyle: .body)

lab.adjustsFontForContentSizeCategory = true

动态颜色:语义化颜色支持。


//xcode支持的最大iOS版本

#ifdef __IPHONE_13_0 && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0

    if (@available(iOS 13.0, *)) { 

        UITraitCollection *trait = [UITraitCollection traitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style]; 

        UIColor *traitColor = [self resolvedColorWithTraitCollection:trait]; 

    } 

// or 强制关闭暗黑模式

self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;

#endif

路由组件:先简单说一个http的url的定义。例如 http://127.0.0.1:8080/page/index?name=0001&pwd=2 ;http是协议部分;127.0.0.1 是域名部分,向访问的目的地;8080是端口;端口后的第一个"/"到最后一个"/"为止,是虚拟目录部分,即page是虚拟目录部分;从最后一个"/"到"?"之间的部分文件名部分,即index;从"?"后开始到"#"为止之间的部分是参数部分(此url无"#",则说明"?"后面的都是参数部分)。更准确地,应该拿路由器的原理来说明最好,由于工作http的url最常见,所以拿来分析。通过上述分析,我们类比的方式,来iOS工程中,实现路由的思想。

\

(1)定义一个router-name : router-handler的规则。router-name是由"scheme:dest-page:jmup-type?key1=name1&&key2=name2"的规则组成。router-handler则是进行回调的操作。使用前,先检测路由表[String : HandlerName]是否已经注册当前router-name,如果没有,则添加进路由表。然后进行页面跳转的操作和传递handler-closure行为。在封装层面,可以将router-name定义成RouterName类,在类内完成对scheme(有默认值)、dest-page、参数的组装和拆解。router-Handler通过typealias的定义Task模板。实施过程中,需要注意是否支持Universal Links。\

假如页面需要传递图片,该如何操作?直接传递一个UIImage的data对象并不是一个好的解决方式。假如app内部有实现因防止图片过多导致的OOM的解决方案,例如对图片实现了LRU的缓存方案,那么可以传递图片所在的node信息;如果没有,可以实现一种专门为路由而生的中转站RouterImageStation,在router设置一个是否含有image的Bool标识。如果有则取出赋值并置nil。

(2) 基于 类-协议-协议方法实现的type内容 的方式,创建一个遵守指定协议的map字典。map字典内容是以type为key,遵守此协议的类为value的方式,结合着后台下发的map集合,对于携带此map的事件,会根据type进行跳转指定页面。(简略版)

(3)CTMediator:利用了runtime的机制。对于每一个业务VC,需要单独建立Target_XXX类(创建VC和参数解封传递)和一个CTMediator的分类(实现路由路径的选择逻辑以及参数包装)。在业务实施时,通过[CTMediator sharedInstance]调用分类的方法来实现指定页面的跳转。内部包含了一个字典用于缓存了"Target_XXX"与其生成类的k-v关系。

pod库:通过pod lib create projName 创建一个名为projName的pod库,需要配置pod.spec文件,在每次添加类文件后需要进行pod install;添加assets资源或者第三方依赖库需要打开相应注释。Bundle文件的读取方式也和app中的方式略不同。可以建立公有库和私有库的协同开发方式。pod的使用上,也可以配置类型为静态库或动态库。

page结构:基于业务划分的页面结构,要能够在逻辑上做到清晰化。对于app的骨架,一般会使用UITabBarController+UINavigationController的方式,或者继承重写。对基类的封装,网络请求刷新,model解析,弹窗链的封装,是基本的功能要求,无需赘言。我的观点是,为了更好的维护页面结构,可以为每一个tabItem对应的vc,都设置一个二级基类。model的解析,有可能存在跨模块的情况,使用protocol的方式比较有前瞻性。

本地存储:NSUserDefault、归结档、plist文件、SQlite、FMDB、Keychain Services。

A/B测试:其实就是为了测试哪种设置更符合用户的兴趣。通过大数测试的反馈效果,进行用户的未来的行为预测。远程控制开关,涉及到用户隐私的,需要声明和授权。同时由于universal links的使用,需要识别当前的环境,以避免黑客的攻击。

flutter跨平台:

flutter的使用,更多是基于移动端简单页面的实现,而对于像地图、音视频编辑这种可能涉及细粒度的功能要求,不建议使用flutter。并不是说flutter完成不了,而是说这个层面的疑难杂症可能不只是在flutter上就可以轻松解决,更需要专深的技能水平。

fish-redux: 目前是3.0状态,fish-redux结合了Redux的状态管理特点,基于状态驱动,组装式组件化,函数式高性能应用框架。Android Studio安装了fish-redux之后,项目可选择创建一个Page类型的FishReduxTemplate,会看到自动生成action-effect-page-reducer-state-view6个file(如果select file默认选择)。

state定义初始状态或克隆体,init的初始化内部逻辑,结合着Reducer是一个Model层的范畴;action定义行为,effect:相当于Controller层;View即是创建视图的View 层。Page提供了对外的接口。也可创建Component组件等。

flutterBoost: 是一个插件,作为flutter与原生沟通的桥梁,解决页面共存和通信问题。FlutterBoost的理念将flutter页面像WebView那样来使用。原生和flutter互动的通信本质,是基于MethodChannel建立通道的。当然还有EventChannel、MessageChannel的存在。

flutter Widget添加iOS view: 继承FlutterPlatformView实例对象并由FlutterPlatformViewFactory包装后,通过FlutterPluginRegistrar进行唯一标识的注册。在flutter端通过UIKitView(ViewType: ##唯一标识##)的方式可拿到原生的指定类型的view。

[将flutter集成到iOS应用](flutter.cn/docs/develo…

git版本管理:

git-flow:工作区-暂存区(索引区)-本地仓库-远程仓库

常用命令: git checkout、branch、 add/commit、 push、fetch/pull、merge/rebase、status/stash、cherry-pick、reset、revert。

git没有目录的概念,保存的是文件路径的方式。对每一个修改的文件做hash值为key,把文件内容压缩作为value存放到.git/object/目录下,会生成index文件,期间会有index-tree的概念 。git每次commit的是一个快照。stash是另外开辟一块缓存内存。rebase:A rebase B ,就把A分支与B分支中 从分开点开始以后的提交点AB当前的最新commit 全部 放到B后面。也就是谁rebase的谁放到前面。git reset默认处理是没有提交的代码。

fastlane:

[fastlane安装使用说明](juejin.cn/post/697023…

Jenkins持续化集成

jenkins使用java编写,主要用于持续地、自动地构建/测试软件项目、监控外部运行的。。

[安装Jenkins与使用](www.jenkins.io/zh/doc/book…

业务统计:

埋点与crash统计:国内腾讯的bugly或者国外提供商Google的firebase-Crashlytics工具。

VideoToolBox实现H264编解码:


// 创建编码Session

VTCompressionSessionRef encodingSession;

OSStatus status = VTCompressionSessionCreate(..., &encodingSession);

//设置Session参数:例如实时采集、画质、GOP-size、帧率、码率等

VTSessionSetProperty(encodingSession, kVTCompressionPropertyKey_RealTime,kCFBooleanTrue);

//画质

VTSessionSetProperty(encodingSession, kVTCompressionProperty_ProfileLevel, kVTProfile_H264_Baseline_AutoLevel);

//IDR帧间隔时长:

int frameInterval = 15;

kVTCompressionKeyProperty_MaxKeyFrameInterval : CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);

//fps

int fps = 15;

VTSessionSetProperty(encodingSession, kVTCompressionProperty_ExpectedFrameRate, CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps));

//平均码率:

int biteRate = imgWidth * imgHeight * 3 * 4; (这是极高码率的计算公式);

VTSessionSetProperty(encodingSession, kVTCompressionProperty_AverageBitRate, CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitrate));

//kVT_CompressionKeyProperty_DataRateLimits设置硬性码率限制,就是每秒码率的上限

//开始编码

VTCompressionSessionPrepareToEncoderFrames(encodingSession);

//AVCaptureVideoDataOutputSampleBufferDelegate回调,

//得到采集的音视频数据CMSampleBufferRef sampleBuffer。

//原始视频帧数据(未编码)

CVImageBuffRef imgBuf = CMSampleBufferGetImageBuffer(sampleBuffer);

//VTEncodeInfoFlags 编码的信息状态 flag: kVTEncodeInfo_FrameDropped同步,kVTEncodeInfo_Asynchronous异步

//设置帧时间

CMTime pts = CMTimeMake(frameCount++, 1000);

//向VTCompressionSession中喂媒体流数据

OSStatus code = VTCompressionSessionEncodeFrame(encodingSession, imgBuf, pts, kCMTimeInvalid, NULL, NULL, &flag);

//H264硬编码完成后,回调VTCompressionOutputCallback,将编码后的数据CMSampleBuffer转换成H264码流,解析出参数集SPS & PPS,加上startCode组装成NALU。

//判断sampleBuffer是否准备好

CMSampleBufferDataIsReady(sampleBuffer);

//判断当前帧是否为关键帧

CFArrayRef arr = CMSampleBufferGetSampleAttache,entsArray(sampleBuffer, true);

CFDictionaryRef dic = CFArrayGetValueAtIndex(arr, 0);

/*kCMSampleAttachmentKey_NotSync: 

A sync sample, also known as a key frame or IDR (Instantaneous Decoding Refresh), can be decoded without requiring any previous samples to have been decoded. Samples following a sync sample also do not require samples prior to the sync sample to have been decoded. Samples are assumed to be sync samples by default — set the value for this key to kCFBooleanTrue for samples which should not be treated as sync samples

*/

bool isKeyFrame =CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);

//关键帧处理

//图像格式描述

CMFormatDescriptionRef fmt = CMSampleBufferGetFormatDescription(sampleBuffer);

//sps-pps:

/*

This function parses the AVC decoder configuration record contained in a H.264 video format description and returns the NAL unit at the given index from it. These NAL units are typically parameter sets (e.g. SPS, PPS), but may contain others as specified by ISO/IEC 14496-15 (e.g. user-data SEI).

*

* index: 参数集的索引,0表示第1个参数集sps; 1表示第2个参数集pps

* parameterSetPointerOut: 一个指针用于接收参数集

* parameterSetSizeOut: 参数集的大小

* parameterSetCountOut: 参数集的个数

*/

CMVideoFormatDescriptionGetH264ParameterSetAtIndex(fmt, index, ...);

//取到H264参数集合中的sps和pps后,对其进行转Data,写入第一帧

//定义起始码

const char *startCode = "\x00\x00\x00\01";

size_t length = sizeof(startCode);

   NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,**YES**)lastObject]stringByAppendingPathComponent:@"temp.h264"];

NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath: filePath];

//写入帧头 sps & pps

[fileHandle writeData: startCodeData];\

[fileHandle writeData: spsData];

[fileHandle writeData: startCodeData];\

[fileHandle writeData: ppsData];

CMBlockBufferRef blockBuf = CMSampleBufferGetDataBuffer(sampleBuffer);

//获得blockbuffer 数据的描述信息:从偏移量开始的大小以及总大小totalLength,访问地址dataPointer,

CMBlockBufferGetDataPointer(blockBuf,...);

//循环获取nalu数据,知道全部写入本地,大端模式转系统端模式

//Nalu的前4个字节是大端模式的一帧的长度

static const int naluHeader = 4;

size_t didReadOffset = 0;

while(didReadOffset < totalLength - naluHeader){

unit32_t NALUnitLength = 0;

//获取nalu 数据长度

memcpy(&NALUnitLength, dataPointer + didReadOffset, naluHeader);

//大端转系统端

NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);

//获取一帧长度的NALU数据写入到本地,先添加startCodeData。

[fileHandle writeData: startCodeData]

[fileHandle writeData: [NSData dataWithBytes:(didReadOffset + bufferOffset+naluHeader) length: NALUnitLength]];

didReadOffset += NALUnitLength + naluHeader;

}

//编码结束时

VTCompressionSessionCompleteFrames(encodingSession, kCMTimeValid);

VTCompressionSessionInvalidate(encodingSession);

CFRelease(encodingSession);

encodingSession = NULL;

//***************************

//====>初始化解码器 

/* <CoreMedia>

创建一个适合于原始H.264流的格式描述。

参数集的数据可以来自原始的NAL单元,并且必须有所需的任何仿真预防字节。

: 定义2个参数集(sps, pps)

*/

CMVideoFormatDescriptionRef decodeDesc;

CMVideFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2,..., &decodeDesc);

//设置解码参数options:<CoreVideo>

//kCVPixelBufferPixelFormatTypeKey(YUV像素格式)

//kCVPixelBufferWidthKey & kCVPixelBufferHeightKey

//kCVPixelBufferOpenGLCompatibilityKey

//设置解码回调:

VTDecompressionOutputCallbackRecord cb;

cb.decompressionOutputCallback;//回调函数

cb.decompressionOutputRefCon ;// 传递的参数

//创建解码会话

VTDecompressionSessionRef decodeSession;

VTDecompressionSessionCreate(kCFAllocatorDefault, decodeDesc, NULL, options, &cd, &decodeSession);

//设置解码会话属性

VTSessionSetProperty(decodeSession, kVTDecompressionPropertykey_RealTime, kCFBooleanTrue);

//=====>NALU解码,去除前4个字节startCode, 读取第5个字节读取nalu类型

uint8_t *naluData; uint32_t naluSize;//naluSize = naluData.length - 4;

// 5:关键帧; 6:增强信息; 7: sps; 8: pps

int type = (naluData[4] & 0x1F);

//关键帧解码

CMSampleBufferRef sampleBuf = NULL;

/*typedef CVImageBufferRef CVPixelBufferRef;

*/

CMBlockBufferRef blockBuf = NULL;

//通过a memory block(naluData)创建CMBlockBuffer

CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, naluData, naluSize,..., &blockBuf);

//创建CMSampleBuffer

CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuf, &decodeDesc,..., &sampleBuf);

//向视频解码器提示低功耗模式

VTDecodeFrameFlags frameFlag = kVTDecodeFrame_1xRealTimePlayback;

//异步解码

VTDecodeInfoFlags infoFlag = kVTDecodeInfo_Asynchronous;

//解码实施

//对sps,pps,都是直接进行从naluData第4个字节开始长度为naluSize的copy到指定buffer空间。

//此后,才会对VTDecompressionSessionRef对象进行实例化

CMPixelBufferRef outPixelBuf = NULL;

//解码 关键帧 数据并输出给outPixelBuf

VTDecompressionSessionDecodeFrame(decodeSession, sampleBuf, frameFlag, &outPixelBuf, infoFlag);

VideoToolBox实现aac编解码:


//======>根据第一个样本SampleBuffer创建音频转换器

CMFormatDescriptionRef fmtDesc = CMSampleBufferFormatDescription(SampleBuffer); AudioStreamBasicDescription a_inDesc = *CMAudioFormatDescriptionGetStreamBasicDescription(fmtDesc);

//配置输出音频参数描述:采样格式、采样率、声道数等

CMAudioFormatDescription a_outDesc;

//检索音频格式属性值,填充输出相关信息

AudioFormatGetProperty(formatID, ..., &a_outDesc);

//***===>通过指定的formatID和kAppleSoftwareAudioCodecManufacturer获取编码器的描述信息

//检索音频属性信息,得到aac编码器总字节大小size

AudioClassDescription *a_classDesc;

//AudioFormatGetPropertyInfo:从给出的属性检索信息

//size信息: The size in bytes of the current value of the property

AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders, ...,&(UInt32)size)

//将满足aac编码的编码器信息写入数组

//count = size/sizeof(AudioClassDescription):aac编码器的个数

AudioClassDescription desc[count];

//AudioFormatGetProperty:检索指定属性data

AudioFormatGetProperty(kAudioForamtProperty_Encoders,..., &desc);

//遍历满足aac编码条件的编码器组寻找最适合的编码器,拿到AudioClassDescription对象a_classDesc

//满足条件:指定的formatID和kAppleSoftwareAudioCodecManufacture 软编码

//创建并初始化converter转换器

AudioConverterRef a_converter;

const AudioStreamBasicDescription in_src_fmt, out_dst_fmt;

AudioConverterNewSpecific(&in_src_fmt, &out_dst_fmt, ..., &a_converter);

//设置编码器质量、比特率

UInt32 quality = kAudioConverterAuality_Medium;

AudioConvertSetProperty(a_converter, kAudioConverterCodeQuality, sizeof(quality),&quality);

AudioConvertSetProperty(a_converter, kAudioConverterEncodeBitRate,...);

//======> 编码操作

//从回调的SampleBuffer读取BlockBuffer,并进一步读取音频buffer和音频size;

CMBlockBufferRef blockBuf = CMSampleBufferGetDataBuffer(sampleBuffer);

//PCM缓存区地址 :char *pcmBuf;大小:size_t pcmBufSize;

CMBlockBufferGetDataPointer(blockBuf,..., &pcmBufSize, &pcmBuf);

//开辟缓冲区空间并初始化,长度为pcmBufSize,

uint8_t *temp_pcmBuf = malloc(pcmBufSize);

memset(temp_pcmBuf,0, pcmBufSize);

//将pcm数据包装到AudioBufferList对象out_a_buf_list的mBuffers中,并设置mNumberBuffers、mNumberChannels、mDataByteSize参数

//转换数据:对音频输入数据做处理,并设置输入数据的回调,做转换器准好接收新的数据后,会重复

//调用此回调,将转换后的输出数据写入到out_a_buf_list

AudioConverterFillComplexBuffer(转换器, 输入数据回调,...,&out_a_buf_list,NULL);

//通过out_a_buf_list将转换后的音频数据转化为二进制NSData,写入文件