iOS 面试题面试合集

2020 iOS最新面试题

2020-12-18  本文已影响0人  leejobs

1.包学习这个项目是哪方面的, 你负责哪些模块 ?

这方面个人觉得分两方面.假如你是管理层的,可以说下你是如何解决团队中的冲突,如何代码模块化.假如你是技术这块的,可以说下你在工作中遇到了哪些技术难点,你是怎么去解决这个技术难点的等等.

2.是有视频相关的东西吗, 是有点播吗 ?

国内常见的直播协议有几个:RTMP、HLS、HTTP-FLV

RTMP,全称 Real Time Messaging Protocol,即实时消息传送协议。Adobe 公司为 Flash 播放器和服务器之间音视频数据传输开发的私有协议。工作在 TCP 之上的明文协议,默认使用端口 1935。协议中的基本数据单元成为消息(Message),传输的过程中消息会被拆分为更小的消息块(Chunk)单元。最后将分割后的消息块通过 TCP 协议传输,接收端再反解接收的消息块恢复成流媒体数据。

RTMP 主要有以下几个优点:RTMP 是专为流媒体开发的协议,对底层的优化比其它协议更加优秀,同时它 Adobe Flash 支持好,基本上所有的编码器(摄像头之类)都支持 RTMP 输出。现在 PC 市场巨大,PC 主要是 Windows,Windows 的浏览器基本上都支持 Flash。另外RTMP适合长时间播放,曾经有过测试,联系 100 万秒,即 10 天多连续播放没有出现问题。最后 RTMP 的延迟相对较低,一般延时在 1-3s 之间,一般的视频会议,互动式直播,完全是够用的。

当然 RTMP 并没有尽善尽美,它也有不足的地方。一方面是它是基于 TCP 传输,非公共端口,可能会被防火墙阻拦;另一方面,也是比较坑的一方面是 RTMP 为 Adobe 私有协议,很多设备无法播放,特别是在 iOS 端,需要使用第三方解码器才能播放。

FLV (Flash Video) 是 Adobe 公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个 FLV 由 The FLV Header, The FLV Body 以及其它 Tag 组成。因此加载速度极快。采用 FLV 格式封装的文件后缀为 .flv。

而我们所说的 HTTP-FLV 即将流媒体数据封装成 FLV 格式,然后通过 HTTP 协议传输给客户端。

HTTP-FLV 依靠 MIME 的特性,根据协议中的 Content-Type 来选择相应的程序去处理相应的内容,使得流媒体可以通过 HTTP 传输。相较于 RTMP 协议,HTTP-FLV 能够好的穿透防火墙,它是基于 HTTP/80 传输,有效避免被防火墙拦截。除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。

说了这么多优点,也来顺便说下 HTTP-FLV 的缺点,由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好。因为网络流量较大,它也不适合做拉流协议。

上述两个协议都是有Adobe公司推出的,而下面要讲的 HLS (HTTP Live Streaming) 则是苹果公司基于 HTTP 的流媒体传输协议。主要应用于 iOS 设备,包含(iPhone, iPad, iPod touch) 以及 Mac OSX 提供音视频直播服务和录制内容(点播)等服务。

相对于常见的流媒体协议,HLS 最大的不同在于它并不是一下请求完整的数据流。它会在服务器端将流媒体数据切割成连续的时长较短的 ts 小文件,并通过 M3U8 索引文件按序访问 ts 文件。客户端只要不停的按序播放从服务器获取到的文件,从而实现播放音视频。

相较 RTMP 而言,使用 HLS 在 HTML5 页面上实现播放非常简单:

直接:

或者:

HLS 的优势:

Apple 的全系列产品支持:由于 HLS 是苹果提出的,所以在 Apple 的全系列产品包括 iPhone、 iPad、safari 都不需要安装任何插件就可以原生支持播放 HLS, 现在 Android 也加入了对 HLS 的支持。

穿透防火墙。基于 HTTP/80 传输,有效避免防火墙拦截

性能高。通过 HTTP 传输, 支持网络分发,CDN 支持良好,且自带多码率自适应,Apple 在提出 HLS 时,就已经考虑了码流自适应的问题。

HLS 的劣势:

实时性差,延迟高。HLS 的延迟基本在 10s+ 以上

文件碎片。特性的双刃剑,ts 切片较小,会造成海量小文件,对存储和缓存都有一定的挑战

流媒体协议 RTMP, HTTP-FLV, HLS 简单对比

RTMP 协议为流媒体而设计,在推流中用的比较多,同时大多 CDN 厂商支持RTMP 协议。

HTTP-FLV 使用类似 RTMP流式的 HTTP 长连接,需由特定流媒体服务器分发的,兼顾两者的优点。以及可以复用现有 HTTP 分发资源的流式协议。它的实时性和 RTMP 相等,与 RTMP 相比又省去了部分协议交互时间,首屏时间更短,可拓展的功能也更多。

HLS 作为苹果提出的直播协议,在 iOS 端占据了不可撼动的地位,Android 端也同时提供相应的支持。

又拍云一站式直播解决方案基于又拍云 CDN,支持 RTMP、HTTP-FLV 和 HLS 三大直播协议,并且通过智能调度、链路保障、追帧处理、丢帧处理以及业界首创的 HLS+ 技术,将 RTMP、HTTP-FLV 直播延迟控制在1秒内,将 HLS 直播延时控制在 4 秒左右。

3.你们的点播列表, 视频源是什么格式的 ? 大概是多长时间的 ?

有做过快速播放吗, 比如点开就秒开这种的 ?

会做一些预加载这些处理吗, 如果做的话你会有什么方案 ?

本文主要介绍视频云 SDK 的点播播放功能,在此之前,先了解如下一些基本知识会大有裨益:

直播和点播

直播(LIVE)的视频源是主播实时推送的。因此,主播停止推送后,播放端的画面也会随即停止,而且由于是实时直播,所以播放器在播直播 URL 的时候是没有进度条的。

点播(VOD)的视频源是云端的一个视频文件,只要未被从云端移除,视频就可以随时播放, 播放中您可以通过进度条控制播放位置,腾讯视频和优酷土豆等视频网站上的视频观看就是典型的点播场景。

协议的支持

通常使用的点播协议如下,现在比较流行的是 HLS(以“http”打头,以“.m3u8”结尾)的点播地址:

特别说明

视频云 SDK 不会对播放地址的来源做限制,即您可以用它来播放腾讯云或非腾讯云的播放地址。但视频云 SDK 中的播放器只支持 FLV 、RTMP 和 HLS(m3u8)三种格式的直播地址,以及 MP4、HLS(m3u8)和 FLV 三种格式的点播地址。

对接攻略

step 1: 创建 Player

视频云 SDK 中的 TXVodPlayer 模块负责实现点播播放功能。

TXVodPlayer *_txVodPlayer = [[TXVodPlayer alloc] init];[_txVodPlayer setupVideoWidget:_myView insertIndex:0]

step 2: 渲染 View

接下来我们要给播放器的视频画面找个地方来显示,iOS 系统中使用 view 作为基本的界面渲染单位,所以您只需要准备一个 view 并调整好布局就可以了。

[_txVodPlayer setupVideoWidget:_myView insertIndex:0]

内部原理上讲,播放器并不是直接把画面渲染到您提供的 view(示例代码中的 _myView)上,而是在这个 view 之上创建一个用于 OpenGL 渲染的子视图(subView)。

如果您要调整渲染画面的大小,只需要调整您所常见的 view 的大小和位置即可,SDK 会让视频画面跟着您的 view 的大小和位置进行实时的调整。

如何做动画?

针对 view 做动画是比较自由的,不过请注意此处动画所修改的目标属性应该是 transform 属性而不是 frame 属性。

[UIViewanimateWithDuration:0.5animations:^{            _myView.transform =CGAffineTransformMakeScale(0.3,0.3);// 缩小1/3}];

step 3: 启动播放

TXVodPlayer 支持两种播放模式,您可以根据需要自行选择

通过 url 方式

TXVodPlayer 内部会自动识别播放协议,您只需要将您的播放 URL 传给 startPlay 函数即可。

NSString* url =@"http://1252463788.vod2.myqcloud.com/xxxxx/v.f20.mp4";[_txVodPlayer startPlay:url ];

通过 fileId 方式

TXPlayerAuthParams *p = [TXPlayerAuthParams new];p.appId =1252463788;p.fileId =@"4564972819220421305";[_txVodPlayer startPlayWithParams:p];

在 媒资管理 找到对应的文件。点开后在右侧视频详情中,可以看到 fileId。

通过 fileId 方式播放,播放器会向后台请求真实的播放地址。如果此时网络异常或 fileId 不存在,则会收到PLAY_ERR_GET_PLAYINFO_FAIL事件,反之收到PLAY_EVT_GET_PLAYINFO_SUCC表示请求成功。

step 4: 画面调整

view:大小和位置

如需修改画面的大小及位置,直接调整 setupVideoWidget 的参数 view 的大小和位置,SDK 会让视频画面跟着您的 view 的大小和位置进行实时的调整。

setRenderMode:铺满或适应

可选值含义

RENDER_MODE_FILL_SCREEN将图像等比例铺满整个屏幕,多余部分裁剪掉,此模式下画面不会留黑边,但可能因为部分区域被裁剪而显示不全。

RENDER_MODE_FILL_EDGE将图像等比例缩放,适配最长边,缩放后的宽和高都不会超过显示区域,居中显示,画面可能会留有黑边。

setRenderRotation:画面旋转

可选值含义

HOME_ORIENTATION_RIGHThome 在右边

HOME_ORIENTATION_DOWNhome 在下面

HOME_ORIENTATION_LEFThome 在左边

HOME_ORIENTATION_UPhome 在上面

step 5: 播放控制

// 调整进度[_txVodPlayer seek:slider.value];// 暂停播放[_txVodPlayer pause];// 恢复播放[_txVodPlayer resume];

step 6: 结束播放

结束播放时,如果要退出当前的 UI 界面,要记得用 removeVideoWidget 销毁 view 控件,否则会产生内存泄露或闪屏问题。

// 停止播放[_txVodPlayer stopPlay];[_txVodPlayer removeVideoWidget];// 记得销毁 view 控件

step 7: 屏幕截图

通过调用 snapshot 您可以截取当前视频为一帧画面,此功能只会截取当前直播流的视频画面,如果您需要截取当前的整个 UI 界面,请调用 iOS 的系统 API 来实现。

step 8: 变速播放

点播播放器支持变速播放,通过接口setRate设置点播播放速率来完成,支持快速与慢速播放,如0.5X、1.0X、1.2X、2X等。

// 设置1.2倍速播放[_txVodPlayer setRate:1.2];// ...// 开始播放[_txVodPlayer startPlay:url];

step 9: 本地缓存 [UGC 版本暂不支持]

在短视频播放场景中,视频文件的本地缓存是很刚需的一个特性,对于普通用户而言,一个已经看过的视频再次观看时,不应该再消耗一次流量。

格式支持

SDK 支持 HLS(m3u8)和 MP4 两种常见点播格式的缓存功能。

何时开启?

SDK 并不默认开启缓存功能,对于用户回看率不高的场景,也并不推荐您开启此功能。

如何开启?

开启此功能需要配置两个参数:本地缓存目录及需要缓存的视频个数。

TXVodPlayConfig _config = [[TXVodPlayConfig alloc] init];// 设置缓存路径_config.cacheFolderPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0];// 设置最多缓存多少个文件,避免缓存太多数据_config.maxCacheItems =10;[_txVodPlayer setConfig: _config];// ...// 开始播放[_txVodPlayer startPlay:playUrl];

step 10: 预加载

在短视频播放场景中,预加载功能对于流畅的观看体验很有帮助:在观看当前视频的同时,在后台加载即将要播放的下一个视频 URL,这样一来,当用户真正切换到下一个视频时,已经不需要从头开始加载了,而是可以做到立刻播放。

这就是视频播放中无缝切换的背后技术支撑,您可以使用 TXVodPlayer 中的 isAutoPlay 开关来实现这个功能,具体做法如下:

// 播放视频 A: 如果将 isAutoPlay 设置为 YES, 那么 startPlay 调用会立刻开始视频的加载和播放NSString* url_A =@"http://1252463788.vod2.myqcloud.com/xxxxx/v.f10.mp4";_player_A.isAutoPlay =YES;[_player_A startPlay:url_A];// 在播放视频 A 的同时,预加载视频 B,做法是将 isAutoPlay 设置为 NONSString* url_B =@"http://1252463788.vod2.myqcloud.com/xxxxx/v.f20.mp4";_player_B.isAutoPlay =NO;[_player_B startPlay:url_B];

等到视频 A 播放结束,自动(或者用户手动切换到)视频 B 时,调用 resume 函数即可实现立刻播放。

-(void) onPlayEvent:(TXVodPlayer *)player event:(int)EvtID withParam:(NSDictionary*)param{// 在视频 A 播放结束的时候,直接启动视频 B 的播放,可以做到无缝切换if(EvtID == PLAY_EVT_PLAY_END) {            [_player_A stopPlay];            [_player_B setupVideoWidget:mVideoContainer insertIndex:0];            [_player_B resume];        }}

step 11: 贴片广告

autoPlay 还可以用来做贴片广告功能,由于设置了 autoPlay 为 NO 之后,播放器会立刻加载但又不会立刻播放,因此可以在此时展示贴片广告,等广告播放结束,在使用 resume 函数立即开始视频的播放。

step 12: 加密播放 [UGC 版本暂不支持]

视频加密方案主要用于在线教育等需要对视频版权进行保护的场景。如果要对您的视频资源进行加密保护,就不仅仅需要在播放器上做改造,还需要对视频源本身进行加密转码,亦需要您的后台和终端研发工程师都参与其中。在 视频加密解决方案 中您会了解到全部细节内容。

目前 TXVodPlayer 也是支持加密播放的,您可以使用通过 URL 携带身份认证信息的方案,该种方案下 SDK 的调用方式跟普通情况没有什么区别。 您也可以使用 Cookie 携带身份认证信息的方案,该种方案下,需要您通过 TXVodPlayConfig 中的 headers 字段设置 cookie 信息于 HTTP 请求头中。

step 13: HTTP-REF [UGC 版本暂不支持]

TXVodPlayConfig 中的 headers 可以用来设置 HTTP 请求头,例如常用的防止 URL 被到处拷贝的 Referer 字段(腾讯云可以提供更加安全的签名防盗链方案),以及用于验证客户端身份信息的 Cookie 字段。

step 14: 硬件加速

对于蓝光级别(1080p)的画质,简单采用软件解码的方式很难获得较为流畅的播放体验,所以如果您的场景是以游戏直播为主,一般都推荐开启硬件加速。

软解和硬解的切换需要在切换之前先 stopPlay,切换之后再 startPlay,否则会产生比较严重的花屏问题。

[_txVodPlayer stopPlay];  _txVodPlayer.enableHWAcceleration =YES;  [_txVodPlayer startPlay:_flvUrl type:_type];

step 15: 多码率文件 [UGC 版本暂不支持]

SDK 支持 hls 的多码率格式,方便用户切换不同码率的播放流。在收到 PLAY_EVT_PLAY_BEGIN 事件后,可以通过下面方法获取多码率数组

NSArray*bitrates = [_txVodPlayer supportedBitrates];//获取多码率数组

在播放过程中,可以随时通过-[TXVodPlayer setBitrateIndex:]切换码率。切换过程中,会重新拉取另一条流的数据,因此会有稍许卡顿。SDK 针对腾讯云的多码率文件做过优化,可以做到切换无卡顿。

进度展示

点播进度分为两个指标:加载进度播放进度,SDK 目前是以事件通知的方式将这两个进度实时通知出来的。

您可以为 TXVodPlayer 对象绑定一个 TXVodPlayListener 监听器,进度通知会通过 PLAY_EVT_PLAY_PROGRESS 事件回调到您的应用程序,该事件的附加信息中即包含上述两个进度指标。

-(void) onPlayEvent:(TXVodPlayer *)playerevent:(int)EvtID withParam:(NSDictionary*)param {if(EvtID == PLAY_EVT_PLAY_PROGRESS) {// 加载进度, 单位是秒, 小数部分为毫秒floatplayable = [param[EVT_PLAYABLE_DURATION] floatValue];                [_loadProgressBar setValue:playable];// 播放进度, 单位是秒, 小数部分为毫秒floatprogress = [param[EVT_PLAY_PROGRESS] floatValue];                [_seekProgressBar setValue:progress];// 视频总长, 单位是秒, 小数部分为毫秒floatduration = [param[EVT_PLAY_DURATION] floatValue];// 可以用于设置时长显示等等}}

事件监听

除了 PROGRESS 进度信息,SDK 还会通过 onPlayEvent(事件通知) 和 onNetStatus(状态反馈)同步给您的应用程序很多其它的信息:

1. 播放事件

事件 ID数值含义说明

PLAY_EVT_PLAY_BEGIN2004视频播放开始,如果有转菊花什么的这个时候该停了

PLAY_EVT_PLAY_PROGRESS2005视频播放进度,会通知当前播放进度、加载进度和总体时长

PLAY_EVT_PLAY_LOADING2007视频播放 loading,如果能够恢复,之后会有 LOADING_END 事件

PLAY_EVT_VOD_LOADING_END2014视频播放 loading 结束,视频继续播放

2. 结束事件

事件 ID数值含义说明

PLAY_EVT_PLAY_END2006视频播放结束

PLAY_ERR_NET_DISCONNECT-2301网络断连,且经多次重连亦不能恢复,更多重试请自行重启播放

PLAY_ERR_HLS_KEY-2305HLS 解密 key 获取失败

3. 警告事件

如下的这些事件您可以不用关心,它只是用来告知您 SDK 内部的一些事件。

事件 ID数值含义说明

PLAY_WARNING_VIDEO_DECODE_FAIL2101当前视频帧解码失败

PLAY_WARNING_AUDIO_DECODE_FAIL2102当前音频帧解码失败

PLAY_WARNING_RECONNECT2103网络断连,已启动自动重连(重连超过三次就直接抛送 PLAY_ERR_NET_DISCONNECT 了)

PLAY_WARNING_HW_ACCELERATION_FAIL2106硬解启动失败,采用软解

4. 连接事件

此外还有几个连接服务器的事件,主要用于测定和统计服务器连接时间,您也无需关心:

事件 ID数值含义说明

PLAY_EVT_VOD_PLAY_PREPARED2013播放器已准备完成,可以播放

PLAY_EVT_RCV_FIRST_I_FRAME2003网络接收到首个可渲染的视频数据包(IDR)

5. 分辨率事件

以下事件用于获取画面变化信息,您也无需关心:

事件 ID数值含义说明

PLAY_EVT_CHANGE_RESOLUTION2009视频分辨率改变

PLAY_EVT_CHANGE_ROATION2011MP4 视频旋转角度

视频宽高

视频的宽高(分辨率)是多少?

站在 SDK 的角度,如果只是拿到一个 URL 字符串,它是回答不出这个问题的。要知道视频画面的宽和高各是多少个 pixel,SDK 需要先访问云端服务器,直到加载到能够分析出视频画面大小的信息才行,所以对于视频信息而言,SDK 也只能以通知的方式告知您的应用程序。

onNetStatus 通知每秒都会被触发一次,目的是实时反馈当前的推流器状态,它就像汽车的仪表盘,可以告知您目前 SDK 内部的一些具体情况,以便您能对当前网络状况和视频信息等有所了解。

评估参数含义说明

NET_STATUS_CPU_USAGE当前瞬时 CPU 使用率

NET_STATUS_VIDEO_WIDTH视频分辨率 - 宽

NET_STATUS_VIDEO_HEIGHT视频分辨率 - 高

NET_STATUS_NET_SPEED当前的网络数据接收速度

NET_STATUS_VIDEO_FPS当前流媒体的视频帧率

NET_STATUS_VIDEO_BITRATE当前流媒体的视频码率,单位 kbps

NET_STATUS_AUDIO_BITRATE当前流媒体的音频码率,单位 kbps

NET_STATUS_CACHE_SIZE缓冲区(jitterbuffer)大小,缓冲区当前长度为0,说明离卡顿就不远了

NET_STATUS_SERVER_IP连接的服务器 IP

您也可直接调用-[TXVodPlayer width]和-[TXVodPlayer height]直接获取当前宽高.

视频信息

如果通过 fileId 方式播放且请求成功,SDK 会将一些请求信息通知到上层。您需要在收到PLAY_EVT_GET_PLAYINFO_SUCC事件后,解析 param 中的信息。

视频信息含义说明

EVT_PLAY_COVER_URL视频封面地址

EVT_PLAY_URL视频播放地址

EVT_PLAY_DURATION视频时长

离线下载

点播离线播放是一个非常普遍的需求,用户可以在有网络的地方先下载好视频,等到了无网络的环境可以再次观看。SDK 提供了播放本地文件的能力,但仅限于 mp4 和 flv 这种单一文件格式,HLS 流媒体因为无法保存到本地,所以不能本地播放。现在,您可以通过TXVodDownloadManager将 HLS 下载到本地,以实现离线播放 HLS 的能力。

step1:准备工作

TXVodDownloadManager被设计为单例,因此您不能创建多个下载对象。用法如下

TXVodDownloadManager *downloader = [TXVodDownloadManager shareInstance];[downloader setDownloadPath:"<指定您的下载目录>"];

step2:开始下载

开始下载有两种方式:url 和 fileid。url 方式非常简单,只需要传入下载地址即可

[downloader startDownloadUrl:@"http://1253131631.vod2.myqcloud.com/26f327f9vodgzp1253131631/f4bdff799031868222924043041/playlist.m3u8"]

fileid 下载至少需要传入 appId 和 fileId

TXPlayerAuthParams *auth = [TXPlayerAuthParamsnew];auth.appId =1252463788;auth.fileId =@"4564972819220421305";TXVodDownloadDataSource *dataSource = [TXVodDownloadDataSourcenew];dataSource.auth = auth;[downloader startDownload:dataSource];

step3:任务信息

在接收任务信息前,需要先设置回调 delegate。

downloader.delegate= self;

可能收到的任务回调有:

-[TXVodDownloadDelegate onDownloadStart:]

任务开始,表示 SDK 已经开始下载。

-[TXVodDownloadDelegate onDownloadProgress:]

任务进度,下载过程中,SDK 会频繁回调此接口,您可以在这里更新进度显示

-[TXVodDownloadDelegate onDownloadStop:]

任务停止,当您调用是stopDownload停止下载,收到此消息表示停止成功

-[TXVodDownloadDelegate onDownloadFinish:]

下载完成,收到此回调表示已全部下载。此时下载文件可以给 TXVodPlayer 播放

-[TXVodDownloadDelegate onDownloadError:errorMsg:]

下载错误,下载过程中遇到网络断开会回调此接口,同时下载任务停止。所有错误码请参考TXDownloadError。

由于 downloader 可以同时下载多个任务,所以回调接口里带上了TXVodDownloadMediaInfo对象,您可以访问 url 或 dataSource 判断下载源,同时还可以获取到下载进度、文件大小等信息。

step4:中断下载

停止下载请调用-[TXVodDownloadManager stopDownload:]方法,参数为-[TXVodDownloadManager sartDownloadUrl:]返回的对象。SDK 支持断点续传,当下载目录没有发生改变时,下次下载同一个文件时会从上次停止的地方重新开始。

如果您不需要重新下载,请调用-[TXVodDownloadManager deleteDownloadFile:]方法删除文件,以释放存储空间。

4.音视频编辑的做过吗 ? GPUImage 在项目中用过吗 ?

视频处理的实现流程如上图所示,将输入的视频内容进行裁剪、分割、复制、旋转、滤镜等视频过渡和特效处理,此外还支持添加字幕和贴纸,处理后的视频内容通过混合器进行混合,最后编码输出。视频处理主要是利用ffmpeg进行相应的处理,将原始的视频进行解码,然后将解码后的YUV数据映射到GPU的纹理上进行相应的参数调节,然后将调好的参数配置进行编辑处理。 音频处理的实现流程如上图所示,首先需要从视频源中分离出音频轨道,形成原生的音频轨道,然后将添加的背景音乐的音频轨道插入原声的音频轨道中,通过调整原声和背景音乐的音量,形成原始音频和背景音乐的混合,最后通过媒体合并将混合后的音频轨道与处理完的视频轨道进行相关类合成。

GPUImage是使用GPU处理图像的、他可以对图片、实时画面、视频进行处理。他允许你自定义滤镜、支持iOS4.0。然而,目前缺乏核心形象的一些更高级的功能,比如面部检测。

GPUImage Official icon

GPUImage的结构图

图片来自:http://blog.csdn.net/qq_29846663/article/details/53707482

美颜基本概念

GPU:(Graphic Processor Unit图形处理单元)手机或者电脑用于图像处理和渲染的硬件

GPU工作原理:CPU指定显示控制器工作,显示控制器根据CPU的控制到指定的地方去取数据和指令, 目前的数据一般是从显存里取,如果显存里存不下,则从内存里取, 内存也放不下,则从硬盘里取,当然也不是内存放不下,而是为了节省内存的话,可以放在硬盘里,然后通过指令控制显示控制器去取。

OpenGL ES:(Open Graphics Library For Embedded(嵌入的) Systems开源嵌入式系统图形处理框架),一套图形与硬件接口,用于把处理好的图片显示到屏幕上。

GPUImage:是一个基于OpenGL ES 2.0图像和视频处理的开源iOS框架,提供各种各样的图像处理滤镜,并且支持照相机和摄像机的实时滤镜,内置120多种滤镜效果,并且能够自定义图像滤镜。

滤镜处理的原理:就是把静态图片或者视频的每一帧进行图形变换再显示出来。它的本质就是像素点的坐标和颜色变化

GPUImage处理画面原理

GPUImage采用链式方式来处理画面,通过addTarget:方法为链条添加每个环节的对象,处理完一个target,就会把上一个环节处理好的图像数据传递下一个target去处理,称为GPUImage处理链。

比如:墨镜原理,从外界传来光线,会经过墨镜过滤,在传给我们的眼睛,就能感受到大白天也是乌黑一片,哈哈。

一般的target可分为两类

中间环节的target, 一般是各种filter, 是GPUImageFilter或者是子类.

最终环节的target, GPUImageView:用于显示到屏幕上, 或者GPUImageMovieWriter:写成视频文件。

GPUImage处理主要分为3个环节

source(视频、图片源) -> filter(滤镜) -> final target (处理后视频、图片)

GPUImaged的Source:都继承GPUImageOutput的子类,作为GPUImage的数据源,就好比外界的光线,作为眼睛的输出源

GPUImageVideoCamera:用于实时拍摄视频

GPUImageStillCamera:用于实时拍摄照片

GPUImagePicture:用于处理已经拍摄好的图片,比如png,jpg图片

GPUImageMovie:用于处理已经拍摄好的视频,比如mp4文件

GPUImage的filter:GPUimageFilter类或者子类,这个类继承自GPUImageOutput,并且遵守GPUImageInput协议,这样既能流进,又能流出,就好比我们的墨镜,光线通过墨镜的处理,最终进入我们眼睛

GPUImage的final target:GPUImageView,GPUImageMovieWriter就好比我们眼睛,最终输入目标。

Part two: 有关GPUImage的研究成果

图像处理之GPUImage图片滤镜http://blog.csdn.net/xoxo_x/article/details/53507016

图像处理之CPU图片滤镜 

http://blog.csdn.net/xoxo_x/article/details/53518322

图像处理之CoreImage图片滤镜 

http://blog.csdn.net/xoxo_x/article/details/53518529

iOS GPUImage研究序一:内置滤镜: 

http://blog.csdn.net/Xoxo_x/article/details/57082804

iOS GPUImage研究二:捕获图像stillCamera写入相册 

http://blog.csdn.net/Xoxo_x/article/details/57086446

iOS GPUImage研究三:视频采集并添加实时滤镜 

http://blog.csdn.net/xoxo_x/article/details/58357978

iOS GPUImage研究四:为视频文件添加滤镜 

http://blog.csdn.net/Xoxo_x/article/details/58818703

iOS GPUImage研究五:短视频拍摄(滤镜、文件写入) 

http://blog.csdn.net/Xoxo_x/article/details/70198469

iOS GPUImage研究六:为视频添加图片水印 

http://blog.csdn.net/xoxo_x/article/details/71055867

iOS GPUImage研究七:动态相册初探(水印) 

http://blog.csdn.net/xoxo_x/article/details/71076584

将图片保存到本地 

http://blog.csdn.net/xoxo_x/article/details/53517878

基于IM实现直播礼物效果 

http://blog.csdn.net/xoxo_x/article/details/52044388

GPUImage之为视频添加10种原生滤镜 

http://blog.csdn.net/xoxo_x/article/details/52749033

GPUImage–流行的美颜滤镜

GPUImageBeautifyFilter 

http://blog.csdn.net/xoxo_x/article/details/52743107

GPUImage基于OpenGL ES 2.0,比基于CPU的图形和视频处理更快速.

GPUImage把OpenGL ES封装为简洁的Objective-C接口.

Part Three: 有关GPUImage的导入方式

iOS GPUImage研究序二:更简单GPUImage导入方式 

http://blog.csdn.net/xoxo_x/article/details/60323297

Part Four: 相关参考资料

https://github.com/Guikunzhi/BeautifyFaceDemo

已经没办法运行,需要cd 文件夹 pod install

http://download.csdn.net/detail/xoxo_x/9642503

我整理过的,去除不需要的东西2016.09.28

2、 文章不错

http://www.cnblogs.com/salam/p/4980992.html

http://blog.csdn.net/jcp312097937/article/details/45849341

http://www.ios122.com/2015/08/gpuimage/

3、GPUImage中的VideoCamera

http://developer.apple.com/library/ios/samplecode/RosyWriter/RosyWriter.zipApple

官方对视频流的处理,GPUImage中的VideoCamera部分代码就是根据这个写的

4、http://blog.csdn.net/Xoxo_x/article/details/52523466

我写的不怎么样有时候会蹦,基于coreImage在GPU渲染,图像处理的不好,但能正常运行。

5、http://blog.csdn.net/Xoxo_x/article/details/52523912 coreImage

图片处理,看这个就可以,我也是借鉴别人的,但很全

6、 原文链接:http://nshipster.com/gpuimage/

英文CPU与GPU的比较https://leafduo.com/articles/2013/05/13/gpuimage/中文翻译

7、这个也不错哦https://github.com/loyinglin/GPUImage

GPUImage详细解析(一) 

http://url.cn/2FwjqIr

GPUImage详细解析(二) 

http://url.cn/2GsW2qg

GPUImage详细解析(三)- 实时美颜滤镜 

http://url.cn/2F0SKgR

GPUImage详细解析(四)模糊图片处理

http://url.cn/2GX6JEQ

GPUImage详细解析(五)滤镜视频录制 

http://url.cn/2GHCo71

GPUImage详细解析(六)-用视频做视频水印 

http://url.cn/2JrhR7V

GPUImage详细解析(七)文字水印和动态图像水印 

http://url.cn/2IzRshs

GPUImage详细解析(八)视频合并混音 

http://url.cn/2DmUYiA

GPUImage详细解析(九)图像的输入输出和滤镜通道 

http://url.cn/2EFjlGp

** 下载官方GPUImage,Github地址:https://github.com/BradLarson/GPUImage

5.flex 框架是做什么用的

FLEX会赐予你SuperPower!!!

FLEX是一个需要注入式的一种框架,从描述来看,功能非常多。主要来讲的话能够对正在运行的应用进行样式的修改和控件的读取。

可以查看控件的坐标和属性

看任何一个对象的属性和成员变量

动态修改属性和成员变量

动态的调用实例和类方法

6.swift 用了多长时间了 ?

自行编辑Swift语法树,SwiftUI等

7.埋点你们是怎么上报的 ? 如果让你自己做的话, 自动埋点有什么方案 ?

参考:https://tech.meituan.com/2017/03/02/mt-mobile-analytics-practice.html

8.swift 语言有 runtim 机制吗 ?

参考:https://www.jianshu.com/p/cfd56c76f7a0

9.线程之间的通信有几种方式, 你都是怎么做通信的 ?

在iOS多线程开发中,有NSObject、NSThread、GCD、NSOpeartion几种方式,对应的线程间通信也有几种

1、NSObject. ...

2、GCD. ...

3、NSOperation. ...

1、URL Scheme. ...

2、Keychain. ...

3、UIPasteboard.

10.你是怎么控制 app 的质量的, 怎么控制崩溃率的 ?

参考:https://juejin.cn/post/6844904022621552654

11.你做过二进制重排的优化, 大概说一下启动流程是什么样的 ?

参考:https://www.jianshu.com/p/cab1a0e24089

12.category 是在什么时候加载的 ?

本文系学习Objective-C的runtime源码时整理所成,主要剖析了category在runtime层的实现原理以及和category相关的方方面面,内容包括:

初入宝地-category简介

连类比事-category和extension

挑灯细览-category真面目

追本溯源-category如何加载

旁枝末叶-category和+load方法

触类旁通-category和方法覆盖

更上一层-category和关联对象

1、初入宝地-category简介

category是Objective-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。除此之外,apple还推荐了category的另外两个使用场景1

可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,a)可以减少单个文件的体积 b)可以把不同的功能组织到不同的category里 c)可以由多个开发者共同完成一个类 d)可以按需加载想要的category 等等。

声明私有方法

不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个使用场景:

模拟多继承

把framework的私有方法公开

Objective-C的这个语言特性对于纯动态语言来说可能不算什么,比如javascript,你可以随时为一个“类”或者对象添加任意方法和实例变量。但是对于不是那么“动态”的语言而言,这确实是一个了不起的特性。

2、连类比事-category和extension

extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。(详见2

但是category则完全不一样,它是在运行期决议的。 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

3、挑灯细览-category真面目

我们知道,所有的OC类和对象,在runtime层都是用struct表示的,category也不例外,在runtime层,category用结构体category_t(在objc-runtime-new.h中可以找到此定义),它包含了:

1)、类的名字(name)

2)、类(cls)

3)、category中所有给类添加的实例方法的列表(instanceMethods)

4)、category中所有添加的类方法的列表(classMethods)

5)、category实现的所有协议的列表(protocols)

6)、category中添加的所有属性(instanceProperties)

typedefstruct category_t {constchar*name;classref_tcls;struct method_list_t *instanceMethods;struct method_list_t *classMethods;struct protocol_list_t *protocols;struct property_list_t *instanceProperties;}category_t;

从category的定义也可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。

我们先去写一个category看一下category到底为何物:

MyClass.h:

#import <Foundation/Foundation.h>@interface MyClass : NSObject- (void)printName;@end@interface MyClass(MyAddition)@property(nonatomic,copy)NSString*name;- (void)printName;@end

MyClass.m:

#import "MyClass.h"@implementation MyClass- (void)printName{NSLog(@"%@",@"MyClass");}@end@implementation MyClass(MyAddition)- (void)printName{NSLog(@"%@",@"MyAddition");}@end

我们使用clang的命令去看看category到底会变成什么:

clang -rewrite-objc MyClass.m

好吧,我们得到了一个3M大小,10w多行的.cpp文件(这绝对是Apple值得吐槽的一点),我们忽略掉所有和我们无关的东西,在文件的最后,我们找到了如下代码片段:

staticstruct /*_method_list_t*/ {unsignedintentsize;// sizeof(struct _objc_method)unsignedintmethod_count;struct _objc_method method_list[1];} _OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {sizeof(_objc_method),1,{{(struct objc_selector *)"printName","v16@0:8", (void*)_I_MyClass_MyAddition_printName}}};staticstruct /*_prop_list_t*/ {unsignedintentsize;// sizeof(struct _prop_t)unsignedintcount_of_properties;struct _prop_t prop_list[1];} _OBJC_$_PROP_LIST_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {sizeof(_prop_t),1,{{"name","T@\"NSString\",C,N"}}};extern"C"__declspec(dllexport)struct _class_t OBJC_CLASS_$_MyClass;staticstruct _category_t _OBJC_$_CATEGORY_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) =

{"MyClass",0,// &OBJC_CLASS_$_MyClass,(conststruct_method_list_t*)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition,0,0,(conststruct_prop_list_t*)&_OBJC_$_PROP_LIST_MyClass_$_MyAddition,};staticvoidOBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition(void) {_OBJC_$_CATEGORY_MyClass_$_MyAddition.cls = &OBJC_CLASS_$_MyClass;}#pragma section(".objc_inithooks$B", long, read, write)__declspec(allocate(".objc_inithooks$B"))staticvoid*OBJC_CATEGORY_SETUP[] = {(void*)&OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition,};staticstruct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {&OBJC_CLASS_$_MyClass,};staticstruct _class_t *_OBJC_LABEL_NONLAZY_CLASS_$[] = {&OBJC_CLASS_$_MyClass,};staticstruct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {&_OBJC_$_CATEGORY_MyClass_$_MyAddition,};

我们可以看到,

1)、首先编译器生成了实例方法列表OBJC$_CATEGORY_INSTANCE_METHODSMyClass$_MyAddition和属性列表OBJC$_PROP_LISTMyClass$_MyAddition,两者的命名都遵循了公共前缀+类名+category名字的命名方式,而且实例方法列表里面填充的正是我们在MyAddition这个category里面写的方法printName,而属性列表里面填充的也正是我们在MyAddition里添加的name属性。还有一个需要注意到的事实就是category的名字用来给各种列表以及后面的category结构体本身命名,而且有static来修饰,所以在同一个编译单元里我们的category名不能重复,否则会出现编译错误。

2)、其次,编译器生成了category本身OBJC$_CATEGORYMyClass$_MyAddition,并用前面生成的列表来初始化category本身。

3)、最后,编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组L_OBJC_LABELCATEGORY$(当然,如果有多个category,会生成对应长度的数组^_^),用于运行期category的加载。

到这里,编译器的工作就接近尾声了,对于category在运行期怎么加载,我们下节揭晓。

4、追本溯源-category如何加载

我们知道,Objective-C的运行是依赖OC的runtime的,而OC的runtime和其他系统库一样,是OS X和iOS通过dyld动态加载的。

想了解更多dyld地同学可以移步这里(3)。

对于OC运行时,入口方法如下(在objc-os.mm文件中):

void_objc_init(void){staticboolinitialized =false;if(initialized)return;    initialized =true;// fixme defer initialization until an objc-using image is found?environ_init();    tls_init();    lock_init();    exception_init();// Register for unmap first, in case some +load unmaps something_dyld_register_func_for_remove_image(&unmap_image);    dyld_register_image_state_change_handler(dyld_image_state_bound,1/*batch*/, &map_images);    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized,0/*not batch*/, &load_images);}

category被附加到类上面是在map_images的时候发生的,在new-ABI的标准下,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:

// Discover categories. for(EACH_HEADER) {category_t**catlist =            _getObjc2CategoryList(hi, &count);for(i =0; i < count; i++) {category_t*cat = catlist[i];class_t*cls = remapClass(cat->cls);if(!cls) {// Category's target class is missing (probably weak-linked).// Disavow any knowledge of this category.catlist[i] =NULL;if(PrintConnecting) {                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with ""missing weak-linked target class",                                cat->name, cat);                }continue;            }// Process this category. // First, register the category with its target class. // Then, rebuild the class's method lists (etc) if // the class is realized. BOOL classExists = NO;if(cat->instanceMethods ||  cat->protocols                ||  cat->instanceProperties)            {                addUnattachedCategoryForClass(cat, cls, hi);if(isRealized(cls)) {                    remethodizeClass(cls);                    classExists = YES;                }if(PrintConnecting) {                    _objc_inform("CLASS: found category -%s(%s) %s",                                getName(cls), cat->name,                                classExists ?"on existing class":"");                }            }if(cat->classMethods  ||  cat->protocols/* ||  cat->classProperties */)            {                addUnattachedCategoryForClass(cat, cls->isa, hi);if(isRealized(cls->isa)) {                    remethodizeClass(cls->isa);                }if(PrintConnecting) {                    _objc_inform("CLASS: found category +%s(%s)",                                getName(cls), cat->name);                }            }        }    }

首先,我们拿到的catlist就是上节中讲到的编译器为我们准备的category_t数组,关于是如何加载catlist本身的,我们暂且不表,这和category本身的关系也不大,有兴趣的同学可以去研究以下Apple的二进制格式和load机制。

略去PrintConnecting这个用于log的东西,这段代码很容易理解:

1)、把category的实例方法、协议以及属性添加到类上

2)、把category的类方法和协议添加到类的metaclass上

值得注意的是,在代码中有一小段注释 /* || cat->classProperties */,看来苹果有过给类添加属性的计划啊。

我们接着往里看,category的各种列表是怎么最终添加到类上的,就拿实例方法列表来说吧:

在上述的代码片段里,addUnattachedCategoryForClass只是把类和category做一个关联映射,而remethodizeClass才是真正去处理添加事宜的功臣。

static void remethodizeClass(class_t *cls){    category_list *cats;    BOOL isMeta;    rwlock_assert_writing(&runtimeLock);    isMeta = isMetaClass(cls);// Re-methodizing: check for more categoriesif((cats = unattachedCategoriesForClass(cls))) {        chained_property_list *newproperties;constprotocol_list_t**newprotos;if(PrintConnecting) {            _objc_inform("CLASS: attaching categories to class '%s' %s",                        getName(cls), isMeta ?"(meta)":"");        }// Update methods, properties, protocolsBOOL vtableAffected = NO;        attachCategoryMethods(cls, cats, &vtableAffected);              newproperties = buildPropertyList(NULL, cats, isMeta);if(newproperties) {            newproperties->next = cls->data()->properties;            cls->data()->properties = newproperties;        }              newprotos = buildProtocolList(cats,NULL, cls->data()->protocols);if(cls->data()->protocols  &&  cls->data()->protocols != newprotos) {            _free_internal(cls->data()->protocols);        }        cls->data()->protocols = newprotos;              _free_internal(cats);// Update method caches and vtablesflushCaches(cls);if(vtableAffected) flushVtables(cls);    }}

而对于添加类的实例方法而言,又会去调用attachCategoryMethods这个方法,我们去看下attachCategoryMethods:

static void attachCategoryMethods(class_t *cls, category_list *cats,

                      BOOL *inoutVtablesAffected){if(!cats)return;if(PrintReplacedMethods) printReplacements(cls, cats);    BOOL isMeta = isMetaClass(cls);method_list_t**mlists = (method_list_t**)        _malloc_internal(cats->count *sizeof(*mlists));// Count backwards through cats to get newest categories firstintmcount =0;inti = cats->count;    BOOL fromBundle = NO;while(i--) {method_list_t*mlist = cat_method_list(cats->list[i].cat, isMeta);if(mlist) {            mlists[mcount++] = mlist;            fromBundle |= cats->list[i].fromBundle;        }    }    attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected);    _free_internal(mlists);}

attachCategoryMethods做的工作相对比较简单,它只是把所有category的实例方法列表拼成了一个大的实例方法列表,然后转交给了attachMethodLists方法(我发誓,这是本节我们看的最后一段代码了^_^),这个方法有点长,我们只看一小段:

for(uint32_tm =0;            (scanForCustomRR || scanForCustomAWZ)  &&  m < mlist->count;            m++)        {            SEL sel = method_list_nth(mlist, m)->name;if(scanForCustomRR  &&  isRRSelector(sel)) {                cls->setHasCustomRR();                scanForCustomRR =false;            }elseif(scanForCustomAWZ  &&  isAWZSelector(sel)) {                cls->setHasCustomAWZ();                scanForCustomAWZ =false;            }        }// Fill method list arraynewLists[newCount++] = mlist;    .    .    .// Copy old methods to the method list arrayfor(i =0; i < oldCount; i++) {        newLists[newCount++] = oldLists[i];    }

需要注意的有两点:

1)、category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA

2)、category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。

5、旁枝末叶-category和+load方法

我们知道,在类和category中都可以有+load方法,那么有两个问题:

1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?

2)、这么些个+load方法,调用顺序是咋样的呢? 鉴于上述几节我们看的代码太多了,对于这两个问题我们先来看一点直观的:

项目结构

我们的代码里有MyClass和MyClass的两个category (Category1和Category2),MyClass和两个category都添加了+load方法,并且Category1和Category2都写了MyClass的printName方法。 在Xcode中点击Edit Scheme,添加如下两个环境变量(可以在执行load方法以及加载category的时候打印log信息,更多的环境变量选项可参见objc-private.h):

环境变量

运行项目,我们会看到控制台打印很多东西出来,我们只找到我们想要的信息,顺序如下:

objc[1187]: REPLACED: -[MyClass printName] by category Category1 objc[1187]: REPLACED: -[MyClass printName] by category Category2 . . . objc[1187]: LOAD: class ‘MyClass’ scheduled for +load objc[1187]: LOAD: category ‘MyClass(Category1)’ scheduled for +load objc[1187]: LOAD: category ‘MyClass(Category2)’ scheduled for +load objc[1187]: LOAD: +[MyClass load] . . . objc[1187]: LOAD: +[MyClass(Category1) load] . . . objc[1187]: LOAD: +[MyClass(Category2) load]

所以,对于上面两个问题,答案是很明显的: 1)、可以调用,因为附加category到类的工作会先于+load方法的执行 2)、+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。 目前的编译顺序是这样的:

编译顺序1

我们调整一个Category1和Category2的编译顺序,run。我们可以看到控制台的输出顺序变了:

编译顺序2

objc[1187]: REPLACED: -[MyClass printName] by category Category2 objc[1187]: REPLACED: -[MyClass printName] by category Category1 . . . objc[1187]: LOAD: class ‘MyClass’ scheduled for +load objc[1187]: LOAD: category ‘MyClass(Category2)’ scheduled for +load objc[1187]: LOAD: category ‘MyClass(Category1)’ scheduled for +load objc[1187]: LOAD: +[MyClass load] . . . objc[1187]: LOAD: +[MyClass(Category2) load] . . . objc[1187]: LOAD: +[MyClass(Category1) load]

虽然对于+load的执行顺序是这样,但是对于“覆盖”掉的方法,则会先找到最后一个编译的category里的对应方法。 这一节我们只是用很直观的方式得到了问题的答案,有兴趣的同学可以继续去研究一下OC的运行时代码。

6、触类旁通-category和方法覆盖

鉴于上面几节我们已经把原理都讲了,这一节只有一个问题:

怎么调用到原来类中被category覆盖掉的方法? 对于这个问题,我们已经知道category其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,所以我们只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法:

Class currentClass = [MyClassclass];MyClass *my = [[MyClass alloc] init];if(currentClass) {unsignedintmethodCount;    Method *methodList = class_copyMethodList(currentClass, &methodCount);    IMP lastImp =NULL;    SEL lastSel =NULL;for(NSIntegeri =0; i < methodCount; i++) {        Method method = methodList[i];NSString*methodName = [NSStringstringWithCString:sel_getName(method_getName(method))        encoding:NSUTF8StringEncoding];if([@"printName"isEqualToString:methodName]) {            lastImp = method_getImplementation(method);            lastSel = method_getName(method);        }    }typedefvoid(*fn)(id,SEL);if(lastImp !=NULL) {        fn f = (fn)lastImp;        f(my,lastSel);    }    free(methodList);}

7、更上一层-category和关联对象

如上所见,我们知道在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值,这个时候可以求助关联对象来实现。

MyClass+Category1.h:

#import "MyClass.h"@interface MyClass (Category1)@property(nonatomic,copy)NSString*name;@end

MyClass+Category1.m:

#import "MyClass+Category1.h"#import <objc/runtime.h>@implementation MyClass (Category1)+ (void)load{NSLog(@"%@",@"load in Category1");}- (void)setName:(NSString*)name{    objc_setAssociatedObject(self,"name",                            name,                            OBJC_ASSOCIATION_COPY);}- (NSString*)name{NSString*nameObject = objc_getAssociatedObject(self,"name");returnnameObject;}@end

但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?

我们去翻一下runtime的源码,在objc-references.mm文件中有个方法_object_set_associative_reference:

void_object_set_associative_reference(id object,void*key, id value,uintptr_tpolicy) {// retain the new value (if any) outside the lock.ObjcAssociation old_association(0, nil);    id new_value = value ? acquireValue(value, policy) : nil;    {        AssociationsManager manager;AssociationsHashMap &associations(manager.associations());disguised_ptr_tdisguised_object = DISGUISE(object);if(new_value) {// break any existing association.AssociationsHashMap::iterator i = associations.find(disguised_object);if(i != associations.end()) {// secondary table existsObjectAssociationMap *refs = i->second;                ObjectAssociationMap::iterator j = refs->find(key);if(j != refs->end()) {                    old_association = j->second;                    j->second = ObjcAssociation(policy, new_value);                }else{                    (*refs)[key] = ObjcAssociation(policy, new_value);                }            }else{// create the new association (first time).ObjectAssociationMap *refs =newObjectAssociationMap;                associations[disguised_object] = refs;                (*refs)[key] = ObjcAssociation(policy, new_value);                _class_setInstancesHaveAssociatedObjects(_object_getClass(object));            }        }else{// setting the association to nil breaks the association.AssociationsHashMap::iterator i = associations.find(disguised_object);if(i !=  associations.end()) {                ObjectAssociationMap *refs = i->second;                ObjectAssociationMap::iterator j = refs->find(key);if(j != refs->end()) {                    old_association = j->second;                    refs->erase(j);                }            }        }    }// release the old value (outside of the lock).if(old_association.hasValue()) ReleaseValue()(old_association);}

我们可以看到所有的关联对象都由AssociationsManager管理,而AssociationsManager定义如下:

class AssociationsManager {staticOSSpinLock _lock;staticAssociationsHashMap *_map;// associative references:  object pointer -> PtrPtrHashMap.public:    AssociationsManager()  { OSSpinLockLock(&_lock); }    ~AssociationsManager()  { OSSpinLockUnlock(&_lock); }AssociationsHashMap &associations() {if(_map ==NULL)            _map =newAssociationsHashMap();return*_map;    }};

AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。

而在对象的销毁逻辑里面,见objc-runtime-new.mm:

void *objc_destructInstance(id obj) {if(obj) {        Class isa_gen = _object_getClass(obj);class_t*isa = newcls(isa_gen);// Read all of the flags at once for performance.boolcxx = hasCxxStructors(isa);boolassoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);// This order is important.if(cxx) object_cxxDestruct(obj);if(assoc) _object_remove_assocations(obj);if(!UseGC) objc_clear_deallocating(obj);    }returnobj;}

嗯,runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

后记

正如侯捷先生所讲-“源码面前,了无秘密”,Apple的Cocoa Touch框架虽然并不开源,但是Objective-C的runtime和Core Foundation却是完全开放源码的(在http://www.opensource.apple.com/tarballs/可以下载到全部的开源代码)。

本系列runtime源码学习将会持续更新,意犹未尽的同学可以自行到上述网站下载源码学习。行笔简陋,如有错误,望指正。

13.你的 OC 用了多长时间了 ?

面试官考察的是你的进阶能力,自由解答

14.flutter是自学的, 还是说在项目中用到了 ?

面试官考察的是你的学习能力,自由解答

15.splite 也有涉及, 你们的数据是怎么入库的, 说一下大概流程 ?

怎样增加字段 ? 怎样删除字段 ?

存储过程简单来说:创建库-表-查询-插入

"ALTER TABLE mytable ADD source INTEGER DEFAULT 0, time TEXT, dura TEXT"

alter table record drop column name; 

16.编码格式有深入研究过吗 ?

参考:https://www.jianshu.com/p/ed6f1073267e

17.说一下双重签名机制 ?你刚才说的是测试环境, 如果是正式环境呢 ?

因为苹果的安全策略,通过签名机制保证手机上的每个App都是经过苹果认证的。

App的安装方式有四种:

通过App Store安装。

开发者可以通过Xcode安装。

Ad-Hoc测试证书打包的App,数量限制100。

In-House企业版证书打包App,信任企业证书后可以使用。

一、 通过App Store安装

通过App Store安装

由苹果生成一对公私钥,公钥内置与iOS设备中,私钥由苹果保管。

开发者上传App给苹果审核后,苹果用私钥对App数据进行签名,发布至App Store。

iOS设备下载App后,用公钥进行验证,若正确,则证明App是由苹果认证过的。

二、通过Xcode安装(真机调试)

由于不需要提交苹果审核,所以苹果没办法对App进行签名,因此苹果采用了双重签名的机制。Mac电脑有一对公私钥,苹果还是原来的一对公私钥。

Xcode真机调试

(图文步骤不一定相同)

开发时需要真机测试时,需要从钥匙串中的证书中心创建证书请求文件(CSR),并传至苹果服务器。

Apple使用私钥对 CSR 签名,生成一份包含Mac公钥信息及Apple对它的签名,被称为证书(CER:即开发证书,发布证书)。

编译完一个App后,Mac电脑使用私钥对App进行签名。

在安装App时,根据当前配置把CER证书一起打包进App。

iOS设备通过内置的Apple的公钥验证CER是否正确,证书验证确保Mac公钥时经过苹果认证的。

再使用CER文件中Mac的公钥去验证App的签名是否正确,确保安装行为是经过苹果允许的。

苹果只是确定这里的安装行为是否合法,不会验证App内容是否修改。

注: 证书请求文件(CertificateSigningRequest.certSigningRequest),用于绑定电脑,文件中应该有Mac电脑的公钥。

三、通过Ad-Hoc正式打包安装

Xcode打包App生成ipa文件,通过iTunes或者蒲公英等第三方发布平台,安装到手机上。流程步骤基本和真机调试相同,差别在于第4步:

开发时需要打包测试或发布时,需要从钥匙串中的证书中心创建证书请求文件(CSR),并传至苹果服务器。

Apple使用私钥对 CSR 签名,生成一份包含Mac公钥信息及Apple对它的签名,被称为证书(CER:即开发证书,发布证书)。

编译完一个App后,Mac电脑使用私钥对App进行签名。

编译签名完之后,要导出ipa文件,导出时,需要选择一个保存的方法(App Store/Ad Hoc/Enterprise/Development),就是选择将上一步生成的CER一起打包进App。

iOS设备通过内置的Apple的公钥验证CER是否正确,证书验证确保Mac公钥是经过苹果认证的。

再使用CER文件中Mac的公钥去验证App的签名是否正确,确保安装行为是经过苹果允许的。

四、In-House企业版证书打包

企业版证书签名验证流程和Ad-Hoc差不多。只是企业版不限制设备数,而且需要用户在iOS设备上手动点击信任证书。

附加一些东西

通过真机调试安装和证书打包安装,不加限制,可能会导致被滥用(不通过App Store,只通过第三方发布平台就能安装),因此苹果加了两个限制:在苹果注册过的设备才可以安装;签名只针对某一个App。

在上述第4步,打包证书进App中时,还需要加上允许安装的设备ID和App对应的APPID等数据(Profile文件)。

根据数字签名的原理,只要数字签名通过验证,第 5 步这里的设备 IDs / AppID / Mac公钥 就都是经过苹果认证的,无法被修改,苹果就可以限制可安装的设备和App,避免滥用。

苹果还要控制iCloud/push/后台运行等,这些都需要苹果授权签名,苹果把这些权限开关统称为:Entitlements,去让苹果授权。

因此证书中可能包含很多东西,不符合规定的格式规范,所以有了Provisioning Profile(描述文件),描述中包含了证书以及其他所有的信息及信息的签名。

四种签名方式的区别

签名方式说明

App Store用于发布到App Store。使用的是发布证书(Cer)。

Ad Hoc安装到指定设备上,用于测试。使用的是发布证书(Cer)。

Enterprise企业版证书签名。

Development安装到指定设备,用于测试。使用的是开发证书(Cer)。

总结一下最终流程

苹果签名完整流程

Mac电脑和苹果分别有一套公私钥,苹果的私钥在后台,公钥存放在每个iOS设备,Mac的私钥存放在电脑,公钥后面要发送给苹果服务器。

Mac从钥匙串生成CSR(就是或者包含公钥),上传至苹果服务器。

苹果服务器使用私钥对CSR进行签名,得到包含Mac公钥以及其签名的数据,称为证书(Cer文件)。

从苹果后台申请Appid,配置好设备ID列表及App的其他权限信息,使用苹果的私钥进行签名生成描述文件(Provisioning Profile),和第 3 步的证书Cer一并下载到Mac安装,钥匙串会自动将Cer与之前生成CSR文件的私钥关联(公私钥对应)。

使用Mac编译App后,使用Mac私钥进行签名,并把 描述文件 打包进App,文件名为embedded.mobileprovision。

安装App时,iOS设备取得证书,使用内置的Apple私钥去验证Cer及embedded.mobileprovision文件。

保证Cer及embedded.mobileprovision是经过苹果认证之后,从Cer中取出Mac公钥,验证App签名,及设备id列表、权限开关是否对应。

其他人想要编译签名App时应怎么做?

简单就是把私钥给他。私钥也是从 钥匙串 中导出,就是.p12文件,其他Mac导入私钥后就可以正常使用了。

查看ipa包中注册的设备ID

解压.ipa文件,得到App数据包,显示包内容,找到embedded.mobileprovision文件所在目录,运行命令security cms -D -i embedded.mobileprovision

名词概念说明

证书请求文件(CertificateSigningRequest.certSigningRequest)本地公钥。

证书(Cer)公钥及苹果签名后的信息。

Entitlements包含了 App 权限开关列表。

p12本地私钥,可以导入到其他电脑。

Provisioning Profile包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

18.我看你有写个反调试, 这个你一般都用在什么工作中 ?

参考:https://www.jianshu.com/p/a3fc10c70a29

19.如果你要想黑别人家的 app, 你会怎么做, 比如说我想拿到他的一些方法 ?

比如说 登录注册的时候, 我想跳过验证, 你会怎么做 ?

逆向工程

20.内存有几个区有了解过吗 ?

如果定义一个字符串, 比如说 let name = "a"

这个是在哪个区 ?

代码区
上一篇下一篇

猜你喜欢

热点阅读