iOS开发
Jekyll
2021-01-12T18:13:12+08:00
//allluckly.cn/
Bison
//allluckly.cn/
lbjobvip@163.com
//allluckly.cn/ffmpeg/ffmpeg4
//allluckly.cn/ffmpeg/ffmpeg4
2017-07-20T00:00:00+08:00
2017-07-20T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="http://upload-images.jianshu.io/upload_images/671504-ada1e50c34918b9b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="FFmpeg_allluckly.cn.png" />
<a href="http://blog.allluckly.cn/ffmpeg/ffmpeg1/">Mac编译ffmpeg获取FFmpeg-iOS</a>
<a href="https://blog.allluckly.cn/ffmpeg/ffmpeg2/">ffmpeg的H.264解码</a>
<a href="https://blog.allluckly.cn/ffmpeg/ffmpeg3/">FFmpeg-iOS推流器的简单封装</a></p>
<p>今天咱来讲讲在iOS 平台上利用ffmpeg获取到摄像头和麦克风,代码很少,后面再加上iOS 自带的获取摄像头的例子;</p>
<h2 id="ffmpeg获取摄像头麦克风">FFmpeg获取摄像头麦克风</h2>
<ul>
<li>首先导入必要的头文件</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#include <stdio.h>
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
#ifdef __cplusplus
};
#endif
</code></pre></div></div>
<p>具体代码简单封装了一下,如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)showDevice{
avdevice_register_all();
AVFormatContext *pFormatCtx = avformat_alloc_context();
AVDictionary* options = NULL;
av_dict_set(&options,"list_devices","true",0);
AVInputFormat *iformat = av_find_input_format("avfoundation");
printf("==AVFoundation Device Info===\n");
avformat_open_input(&pFormatCtx,"",iformat,&options);
printf("=============================\n");
if(avformat_open_input(&pFormatCtx,"0",iformat,NULL)!=0){
printf("Couldn't open input stream.\n");
return ;
}
}
</code></pre></div></div>
<p>运行一下可以看到日志区域的打印信息如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>==AVFoundation Device Info===
2017-07-20 16:59:36.325150+0800 LBffmpegDemo[2040:821433] [MC] System group container for systemgroup.com.apple.configurationprofiles path is /private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles
2017-07-20 16:59:36.326529+0800 LBffmpegDemo[2040:821433] [MC] Reading from public effective user settings.
[AVFoundation input device @ 0x145d0100] AVFoundation video devices:
[AVFoundation input device @ 0x145d0100] [0] Back Camera
[AVFoundation input device @ 0x145d0100] [1] Front Camera
[AVFoundation input device @ 0x145d0100] AVFoundation audio devices:
[AVFoundation input device @ 0x145d0100] [0] iPhone 麦克风
=============================
[avfoundation @ 0x153ef800] Selected framerate (29.970030) is not supported by the device
[avfoundation @ 0x153ef800] Supported modes:
[avfoundation @ 0x153ef800] 192x144@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 192x144@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 352x288@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 352x288@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 480x360@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 480x360@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 640x480@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 640x480@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 960x540@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 960x540@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 1280x720@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 1280x720@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 1280x720@[1.000000 60.000000]fps
[avfoundation @ 0x153ef800] 1280x720@[1.000000 60.000000]fps
[avfoundation @ 0x153ef800] 1920x1080@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 1920x1080@[1.000000 30.000000]fps
[avfoundation @ 0x153ef800] 2592x1936@[1.000000 20.000000]fps
[avfoundation @ 0x153ef800] 2592x1936@[1.000000 20.000000]fps
[avfoundation @ 0x153ef800] 3264x2448@[1.000000 20.000000]fps
[avfoundation @ 0x153ef800] 3264x2448@[1.000000 20.000000]fps
Couldn't open input stream.
</code></pre></div></div>
<p>显然获取到了我们的设备,前后摄像头,和麦克风;下面我们看看系统自带的获取摄像头的例子:</p>
<h2 id="ios系统自带获取摄像头">iOS系统自带获取摄像头</h2>
<ul>
<li>首先导入必须的头文件</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
</code></pre></div></div>
<ul>
<li>然后是一些全局的属性</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@property(nonatomic, strong) AVCaptureSession *captureSession;
@property(nonatomic, strong) AVCaptureDevice *captureDevice;
@property(nonatomic, strong) AVCaptureDeviceInput *captureDeviceInput;
@property(nonatomic, strong) AVCaptureVideoDataOutput *captureVideoDataOutput;
@property(nonatomic, assign) CGSize videoSize;
@property(nonatomic, strong) AVCaptureConnection *videoCaptureConnection;
@property(nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
</code></pre></div></div>
<ul>
<li>最后是简单封装的代码</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)getMovieDevice:(UIView *)view{
self.captureSession = [[AVCaptureSession alloc] init];
// captureSession.sessionPreset = AVCaptureSessionPresetMedium;
self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
self.videoSize = [self getVideoSize:self.captureSession.sessionPreset];
self.captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
self.captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:self.captureDevice error:&error];
if([self.captureSession canAddInput:self.captureDeviceInput])
[self.captureSession addInput:self.captureDeviceInput];
else
NSLog(@"Error: %@", error);
dispatch_queue_t queue = dispatch_queue_create("myEncoderH264Queue", NULL);
self.captureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
[self.captureVideoDataOutput setSampleBufferDelegate:self queue:queue];
#if encodeModel
// nv12
NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
kCVPixelBufferPixelFormatTypeKey,
nil];
#else
// 32bgra
NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA],
kCVPixelBufferPixelFormatTypeKey,
nil];
#endif
self.captureVideoDataOutput.videoSettings = settings;
self.captureVideoDataOutput.alwaysDiscardsLateVideoFrames = YES;
if ([self.captureSession canAddOutput:self.captureVideoDataOutput]) {
[self.captureSession addOutput:self.captureVideoDataOutput];
}
// 保存Connection,用于在SampleBufferDelegate中判断数据来源(是Video/Audio?)
self.videoCaptureConnection = [self.captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
#pragma mark -- AVCaptureVideoPreviewLayer init
self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
self.previewLayer.frame = view.layer.bounds;
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; // 设置预览时的视频缩放方式
[[self.previewLayer connection] setVideoOrientation:AVCaptureVideoOrientationPortrait]; // 设置视频的朝向
[self.captureSession startRunning];
[view.layer addSublayer:self.previewLayer];
}
- (CGSize)getVideoSize:(NSString *)sessionPreset {
CGSize size = CGSizeZero;
if ([sessionPreset isEqualToString:AVCaptureSessionPresetMedium]) {
size = CGSizeMake(480, 360);
} else if ([sessionPreset isEqualToString:AVCaptureSessionPreset1920x1080]) {
size = CGSizeMake(1920, 1080);
} else if ([sessionPreset isEqualToString:AVCaptureSessionPreset1280x720]) {
size = CGSizeMake(1280, 720);
} else if ([sessionPreset isEqualToString:AVCaptureSessionPreset640x480]) {
size = CGSizeMake(640, 480);
}
return size;
}
#pragma mark -- AVCaptureVideo(Audio)DataOutputSampleBufferDelegate method
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
// 这里的sampleBuffer就是采集到的数据了,但它是Video还是Audio的数据,得根据connection来判断
if (connection == self.videoCaptureConnection) {
// Video
// NSLog(@"在这里获得video sampleBuffer,做进一步处理(编码H.264)");
#if encodeModel
// encode
#else
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// int pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
// switch (pixelFormat) {
// case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
// NSLog(@"Capture pixel format=NV12");
// break;
// case kCVPixelFormatType_422YpCbCr8:
// NSLog(@"Capture pixel format=UYUY422");
// break;
// default:
// NSLog(@"Capture pixel format=RGB32");
// break;
// }
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
// render
[openglView render:pixelBuffer];
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
#endif
}
// else if (connection == _audioConnection) {
//
// // Audio
// NSLog(@"这里获得audio sampleBuffer,做进一步处理(编码AAC)");
// }
}
</code></pre></div></div>
<p><a href="https://github.com/AllLuckly/LBffmpegDemo">LBffmpegDemo下载地址</a></p>
<p>到此iOS平台获取摄像头告一段落,有时间再慢慢写FFmpeg在iOS平台的一些其他的使用方法;有对ffmpeg感兴趣的朋友可以关注我!😄</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">博主app上线啦,快点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://blog.allluckly.cn/ffmpeg/ffmpeg3/">ffmpeg-iOS推流器</a><br /></p>
<p><a href="//allluckly.cn/ffmpeg/ffmpeg4">FFmpeg-iOS获取摄像头麦克风</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on July 20, 2017.</p>
//allluckly.cn/ffmpeg/ffmpeg3
//allluckly.cn/ffmpeg/ffmpeg3
2017-07-12T00:00:00+08:00
2017-07-12T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="http://upload-images.jianshu.io/upload_images/671504-ada1e50c34918b9b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="FFmpeg_allluckly.cn.png" />
<a href="http://blog.allluckly.cn/ffmpeg/ffmpeg1/">Mac编译ffmpeg获取FFmpeg-iOS</a>
<a href="https://blog.allluckly.cn/ffmpeg/ffmpeg2/">ffmpeg的H.264解码</a>
由上俩篇文章,我们已经对ffmpeg有了一定的了解和应用了,接下来让我们一起学习怎么利用ffmpeg推流。
在推流之前我们需搭建一个本地的nginx推流服务器用来测试。</p>
<p>主要参考的这篇文章 <a href="http://ios.jobbole.com/89986/">iOS直播app(推流篇)</a>在这里不做过多的阐述,有兴趣的朋友可以跟着做一做。</p>
<p>期间我这边遇到的问题:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Could not symlink share/man/man8/nginx.8
/usr/local/share/man/man8 is not writable.
</code></pre></div></div>
<p>只要原因是文件夹系统没有权限导致的,解决方法是文件夹前往<code class="language-plaintext highlighter-rouge">/usr/local/share/man/man8</code>文件,显示简介设置系统可读可写即可。</p>
<p>我这边配置好的nginx推流服务器的配置文件<code class="language-plaintext highlighter-rouge">nginx.conf</code>内容为</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
include servers/*;
}
rtmp {
server {
listen 1991;
application liveApp {
live on;
record off;
}
}
}
</code></pre></div></div>
<p>可整个复制进去。得到的推流服务器地址如下</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rtmp://localhost:1991/liveApp/room
</code></pre></div></div>
<p>如果先前已经做过<a href="http://blog.allluckly.cn/ffmpeg/ffmpeg1/">Mac编译ffmpeg获取FFmpeg-iOS</a>这一步的话,不需要再继续下载ffmpeg。
下面进入我们的代码阶段即可。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)pushFlow:(NSString *)input_str output_str:(NSString *)output_rtmpStr{
char input_str_full[500]={0};
char output_str_full[500]={0};
// NSString *input_str= [NSString stringWithFormat:@"resource.bundle/%@",self.input.text];
NSString *input_nsstr=[[[NSBundle mainBundle]resourcePath] stringByAppendingPathComponent:input_str];
sprintf(input_str_full,"%s",[input_nsstr UTF8String]);
sprintf(output_str_full,"%s",[output_rtmpStr UTF8String]);
printf("Input Path:%s\n",input_str_full);
printf("Output Path:%s\n",output_str_full);
AVOutputFormat *ofmt = NULL;
//Input AVFormatContext and Output AVFormatContext
AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
AVPacket pkt;
char in_filename[500]={0};
char out_filename[500]={0};
int ret, i;
int videoindex=-1;
int frame_index=0;
int64_t start_time=0;
//in_filename = "cuc_ieschool.mov";
//in_filename = "cuc_ieschool.h264";
//in_filename = "cuc_ieschool.flv";//Input file URL
//out_filename = "rtmp://localhost/publishlive/livestream";//Output URL[RTMP]
//out_filename = "rtp://233.233.233.233:6666";//Output URL[UDP]
strcpy(in_filename,input_str_full);
strcpy(out_filename,output_str_full);
av_register_all();
//Network
avformat_network_init();
//Input
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
printf( "Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
printf( "Failed to retrieve input stream information");
goto end;
}
for(i=0; i<ifmt_ctx->nb_streams; i++)
if(ifmt_ctx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
av_dump_format(ifmt_ctx, 0, in_filename, 0);
//Output
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_filename); //RTMP
//avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", out_filename);//UDP
if (!ofmt_ctx) {
printf( "Could not create output context\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt = ofmt_ctx->oformat;
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
AVStream *in_stream = ifmt_ctx->streams[i];
AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
if (!out_stream) {
printf( "Failed allocating output stream\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
if (ret < 0) {
printf( "Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
//Dump Format------------------
av_dump_format(ofmt_ctx, 0, out_filename, 1);
//Open output URL
if (!(ofmt->flags & AVFMT_NOFILE)) {
ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
if (ret < 0) {
printf( "Could not open output URL '%s'", out_filename);
goto end;
}
}
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0) {
printf( "Error occurred when opening output URL\n");
goto end;
}
start_time=av_gettime();
while (1) {
AVStream *in_stream, *out_stream;
//Get an AVPacket
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
break;
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if(pkt.pts==AV_NOPTS_VALUE){
//Write PTS
AVRational time_base1=ifmt_ctx->streams[videoindex]->time_base;
//Duration between 2 frames (us)
int64_t calc_duration=(double)AV_TIME_BASE/av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
//Parameters
pkt.pts=(double)(frame_index*calc_duration)/(double)(av_q2d(time_base1)*AV_TIME_BASE);
pkt.dts=pkt.pts;
pkt.duration=(double)calc_duration/(double)(av_q2d(time_base1)*AV_TIME_BASE);
}
//Important:Delay
if(pkt.stream_index==videoindex){
AVRational time_base=ifmt_ctx->streams[videoindex]->time_base;
AVRational time_base_q={1,AV_TIME_BASE};
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - start_time;
if (pts_time > now_time)
av_usleep(pts_time - now_time);
}
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
/* copy packet */
//Convert PTS/DTS
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
//Print to Screen
if(pkt.stream_index==videoindex){
printf("Send %8d video frames to output URL\n",frame_index);
frame_index++;
}
//ret = av_write_frame(ofmt_ctx, &pkt);
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0) {
printf( "Error muxing packet\n");
break;
}
av_packet_unref(&pkt);
}
//写文件尾(Write file trailer)
av_write_trailer(ofmt_ctx);
end:
avformat_close_input(&ifmt_ctx);
/* close output */
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
printf( "Error occurred.\n");
return;
}
return;
}
</code></pre></div></div>
<p>运行app的时候用VLC播放器打开我们的推流地址即可看到推流效果。</p>
<p><img src="http://upload-images.jianshu.io/upload_images/671504-e2e2aa53d769b2a5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="allluckly.cn" />
<a href="https://github.com/AllLuckly/LBffmpegDemo">LBffmpegDemo下载地址</a></p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">博主app上线啦,快点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/年终总结/zongjie2015">菜鸟程序员2015年年终总结</a><br /></p>
<p><a href="//allluckly.cn/ffmpeg/ffmpeg3">FFmpeg-iOS推流器的简单封装</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on July 12, 2017.</p>
//allluckly.cn/ffmpeg/ffmpeg2
//allluckly.cn/ffmpeg/ffmpeg2
2017-07-11T00:00:00+08:00
2017-07-11T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="http://upload-images.jianshu.io/upload_images/671504-ada1e50c34918b9b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="FFmpeg_allluckly.cn.png" /></p>
<p>新建工程,导入由<a href="http://www.jianshu.com/p/5511eea1b29e">Mac编译ffmpeg获取FFmpeg-iOS</a>编译好的<code class="language-plaintext highlighter-rouge">FFmpeg-iOS</code>,然后导入系统依赖的库</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AudioToolbox.framework
CoreMedia.framework
VideoToolbox.framework
libiconv.tbd
libbz2.tbd
libz.tbd
</code></pre></div></div>
<p>编译的时候报错: ‘libavcodec/avcodec.h’ file not found ,修改Header search paths 里的路径:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$(PROJECT_DIR)/FFmpeg-iOS/include
</code></pre></div></div>
<p>然后导入头文件</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/*----------------解码必须导入-----------------*/
#import "avformat.h"
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
/*-------------------------------------------*/
</code></pre></div></div>
<p>运行一下项目不报错的话,就可以开始解码渲染之旅了。下面我们先从解码开始;</p>
<h3 id="h264解码">h.264解码</h3>
<p>ffmpeg对视频文件进行解码的大致流程:</p>
<p>1.注册所有容器格式和CODEC: av_register_all()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
av_register_all();
});
</code></pre></div></div>
<p>2.打开文件: av_open_input_file()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> pFormatContext = avformat_alloc_context();
NSString *fileName = [[NSBundle mainBundle] pathForResource:@"zjd.h264" ofType:nil];
if (fileName == nil)
{
NSLog(@"Couldn't open file:%@",fileName);
return;
}
//[1]函数调用成功之后处理过的AVFormatContext结构体;[2]打开的视音频流的URL;[3]强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat;[4]附加的一些选项,一般情况下可以设置为NULL。)
if (avformat_open_input(&pFormatContext, [fileName cStringUsingEncoding:NSASCIIStringEncoding], NULL, NULL) != 0)
{
NSLog(@"无法打开文件");
return;
}
</code></pre></div></div>
<p>3.从文件中提取流信息: av_find_stream_info()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (avformat_find_stream_info(pFormatContext, NULL) < 0) {
NSLog(@"无法提取流信息");
return;
}
</code></pre></div></div>
<p>4.穷举所有的流,查找其中种类为CODEC_TYPE_VIDEO</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> int videoStream = -1;
for (int i = 0; i < pFormatContext -> nb_streams; i++)
{
if (pFormatContext -> streams[i] -> codec -> codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
}
}
if (videoStream == -1) {
NSLog(@"Didn't find a video stream.");
return;
}
</code></pre></div></div>
<p>5.查找对应的解码器: avcodec_find_decoder()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pCodecCtx = pFormatContext->streams[videoStream]->codec;
AVCodec *pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (pCodec == NULL) {
NSLog(@"pVideoCodec not found.");
return NO;
}
</code></pre></div></div>
<p>6.打开编解码器: avcodec_open()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avcodec_open2(pCodecCtx, pCodec, NULL);
</code></pre></div></div>
<p>7.为解码帧分配内存: avcodec_alloc_frame()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pFrame = av_frame_alloc();
</code></pre></div></div>
<p>8.不停地从码流中提取中帧数据: av_read_frame()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> AVPacket packet;
av_init_packet(&packet);
if (av_read_frame(pFormatContext, &packet) >= 0)
{//已经读取到了数据
}else{//没读取到数据
}
</code></pre></div></div>
<p>9.判断帧的类型,对于视频帧调用: avcodec_decode_video()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> //作用是解码一帧视频数据。输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
av_free_packet(&packet);
if (frameFinished) //解码成功
{
}
</code></pre></div></div>
<p>10.解码完后,释放解码器: avcodec_close()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avcodec_close()
</code></pre></div></div>
<p>11.关闭输入文件:av_close_input_file()</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>av_close_input_file()
</code></pre></div></div>
<p>最后封装的代码如下</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
/*input_str 输入的文件路径
*output_str 输出的文件路径
*return 解码后的信息
*/
#pragma mark - 基于FFmpeg的视频解码器
- (NSString *)decoder:(NSString *)input_str output_str:(NSString *)output_str{
AVFormatContext *pFormatCtx;
int i, videoindex;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVFrame *pFrame,*pFrameYUV;
uint8_t *out_buffer;
AVPacket *packet;
int y_size;
int ret, got_picture;
struct SwsContext *img_convert_ctx;
FILE *fp_yuv;
int frame_cnt;
clock_t time_start, time_finish;
double time_duration = 0.0;
char input_str_full[500]={0};
char output_str_full[500]={0};
char info[1000]={0};
// NSString *input_str= [NSString stringWithFormat:@"resource.bundle/sintel.mov"];
// NSString *output_str= [NSString stringWithFormat:@"resource.bundle/test.yuv"];
NSString *input_nsstr=[[[NSBundle mainBundle]resourcePath] stringByAppendingPathComponent:input_str];
NSString *output_nsstr=[[[NSBundle mainBundle]resourcePath] stringByAppendingPathComponent:output_str];
sprintf(input_str_full,"%s",[input_nsstr UTF8String]);
sprintf(output_str_full,"%s",[output_nsstr UTF8String]);
printf("Input Path:%s\n",input_str_full);
printf("Output Path:%s\n",output_str_full);
//注册编解码器
av_register_all();
//打开网络流
avformat_network_init();
//初始化AVFormatContext
pFormatCtx = avformat_alloc_context();
if(avformat_open_input(&pFormatCtx,input_str_full,NULL,NULL)!=0){
printf("Couldn't open input stream.\n");
return nil;
}
if(avformat_find_stream_info(pFormatCtx,NULL)<0){
printf("Couldn't find stream information.\n");
return nil;
}
videoindex=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
if(videoindex==-1){
printf("Couldn't find a video stream.\n");
return nil;
}
pCodecCtx = avcodec_alloc_context3(NULL);
if (pCodecCtx == NULL)
{
printf("Could not allocate AVCodecContext\n");
return nil;
}
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoindex]->codecpar);
pCodec = avcodec_find_decoder(pCodecCtx->codec_id); //指向AVCodec的指针.查找解码器
if(pCodec==NULL){
printf("Couldn't find Codec.\n");
return nil;
}
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
printf("Couldn't open codec.\n");
return nil;
}
pFrame=av_frame_alloc();
pFrameYUV=av_frame_alloc();
out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height,1));
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,
AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1);
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
sprintf(info, "[Input ]%s\n", [input_str UTF8String]);
sprintf(info, "%s[Output ]%s\n",info,[output_str UTF8String]);
sprintf(info, "%s[Format ]%s\n",info, pFormatCtx->iformat->name);
sprintf(info, "%s[Codec ]%s\n",info, pCodecCtx->codec->name);
sprintf(info, "%s[Resolution]%dx%d\n",info, pCodecCtx->width,pCodecCtx->height);
fp_yuv=fopen(output_str_full,"wb+");
if(fp_yuv==NULL){
printf("Cannot open output file.\n");
return nil;
}
frame_cnt=0;
time_start = clock();
while(av_read_frame(pFormatCtx, packet)>=0){
if(packet->stream_index==videoindex){
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if(ret < 0){
printf("Decode Error.\n");
return nil;
}
if(got_picture){
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
//Output info
char pictype_str[10]={0};
switch(pFrame->pict_type){
case AV_PICTURE_TYPE_I:sprintf(pictype_str,"I");break;
case AV_PICTURE_TYPE_P:sprintf(pictype_str,"P");break;
case AV_PICTURE_TYPE_B:sprintf(pictype_str,"B");break;
default:sprintf(pictype_str,"Other");break;
}
printf("Frame Index: %5d. Type:%s\n",frame_cnt,pictype_str);
frame_cnt++;
}
}
av_packet_unref(packet);
}
//flush decoder
//FIX: Flush Frames remained in Codec
while (1) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if (ret < 0)
break;
if (!got_picture)
break;
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
int y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
//Output info
char pictype_str[10]={0};
switch(pFrame->pict_type){
case AV_PICTURE_TYPE_I:sprintf(pictype_str,"I");break;
case AV_PICTURE_TYPE_P:sprintf(pictype_str,"P");break;
case AV_PICTURE_TYPE_B:sprintf(pictype_str,"B");break;
default:sprintf(pictype_str,"Other");break;
}
printf("Frame Index: %5d. Type:%s\n",frame_cnt,pictype_str);
frame_cnt++;
}
time_finish = clock();
time_duration=(double)(time_finish - time_start);
sprintf(info, "%s[Time ]%fus\n",info,time_duration);
sprintf(info, "%s[Count ]%d\n",info,frame_cnt);
sws_freeContext(img_convert_ctx);
fclose(fp_yuv);
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
NSString * info_ns = [NSString stringWithFormat:@"%s", info];
// NSLog(@"解码后的信息%@",info_ns);
NSLog(@"文件输出路径%@",output_nsstr);
return info_ns;
}
</code></pre></div></div>
<p>参考资料:<a href="http://blog.csdn.net/leixiaohua1020/article/details/47072257">雷神的ffmpeg解码</a></p>
<p><a href="https://github.com/AllLuckly/LBffmpegDemo">LBffmpegDemo下载地址</a>;</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">博主app上线啦,快点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/年终总结/zongjie2015">菜鸟程序员2015年年终总结</a><br /></p>
<p><a href="//allluckly.cn/ffmpeg/ffmpeg2">FFmpeg-iOS的H.264解码</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on July 11, 2017.</p>
//allluckly.cn/ffmpeg/ffmpeg1
//allluckly.cn/ffmpeg/ffmpeg1
2017-07-11T00:00:00+08:00
2017-07-11T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="http://upload-images.jianshu.io/upload_images/671504-ada1e50c34918b9b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="FFmpeg_allluckly.cn.png" /></p>
<p>ffmpeg 的用处在此就不做太多的阐述,感兴趣的朋友可以自行百度。</p>
<p>下面开始正文:</p>
<ol>
<li>
<p>下载 <a href="https://github.com/libav/gas-preprocessor">gas-preprocessor</a></p>
</li>
<li>
<p>复制<code class="language-plaintext highlighter-rouge">gas-preprocessor</code> 文件中的<code class="language-plaintext highlighter-rouge">gas-preprocessor.pl</code>文件 到<code class="language-plaintext highlighter-rouge">/usr/local/bin/</code>文件夹下</p>
</li>
<li>
<p>打开权限</p>
</li>
</ol>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod 777 /usr/local/bin/gas-preprocessor.pl
</code></pre></div></div>
<p>4.安装 yasm</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew install yasm
</code></pre></div></div>
<p>安装完成时如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>==> Downloading https://homebrew.bintray.com/bottles/yasm-1.3.0.el_capitan.bottl
######################################################################## 100.0%
==> Pouring yasm-1.3.0.el_capitan.bottle.1.tar.gz
🍺 /usr/local/Cellar/yasm/1.3.0: 44 files, 3.1M
</code></pre></div></div>
<p>5.下载MAC上<a href="https://github.com/kewlbear/FFmpeg-iOS-build-script">ffmpeg能编译的脚本</a></p>
<p>6.终端cd + 文件夹目录,进入下载的文件夹中,然后编译脚本</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./build-ffmpeg.sh
</code></pre></div></div>
<p>这时你可以喝杯水压压惊了,坐等编译完成。如果没安装FFmpeg,这个命令会自动安装FFmpeg,时间可能有点久。
全部编译完成可以得到 <code class="language-plaintext highlighter-rouge">FFmpeg-iOS</code>文件</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">博主app上线啦,快点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/年终总结/zongjie2015">菜鸟程序员2015年年终总结</a><br /></p>
<p><a href="//allluckly.cn/ffmpeg/ffmpeg1">Mac编译ffmpeg获取FFmpeg-iOS</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on July 11, 2017.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao75
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao75
2017-05-03T00:00:00+08:00
2017-05-03T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<h1 id="前言">前言</h1>
<p><code class="language-plaintext highlighter-rouge">DNS劫持</code>指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应。</p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br /></p>
<p>来源:<a href="http://sindrilin.com/apm/2017/03/31/DNS劫持">SindriLin的小巢</a><br /></p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao75/1.jpg" alt="1" /><br /></p>
<p><code class="language-plaintext highlighter-rouge">DNS劫持</code>的主要表现为看视频,点击之后莫名其妙的跳到了某些广告网站。正常情况下,当我们点击某个链接的时候,会向一个称作<code class="language-plaintext highlighter-rouge">DNS服务器</code>的东西发出请求,把链接转换成机器能够识别的<code class="language-plaintext highlighter-rouge">ip</code>地址,其过程如下:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao75/2.jpg" alt="1" /><br /></p>
<p>域名-><code class="language-plaintext highlighter-rouge">ip</code>地址的过程被称作<code class="language-plaintext highlighter-rouge">DNS解析</code>。在这个过程中,由于<code class="language-plaintext highlighter-rouge">DNS</code>请求报文是明文状态,可能会在请求过程中被监测,然后攻击者伪装<code class="language-plaintext highlighter-rouge">DNS</code>服务器向主机发送带有假<code class="language-plaintext highlighter-rouge">ip</code>地址的响应报文,从而使得主机访问到假的服务器。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao75/3.jpg" alt="1" /><br /></p>
<h1 id="nsurlprotocol">NSURLProtocol</h1>
<p><code class="language-plaintext highlighter-rouge">NSURLProtocol</code>是苹果提供给开发者的黑魔法之一,大部分的网络请求都能被它拦截并且篡改,以此来改变URL的加载行为。这使得我们不必改动网络请求的业务代码,也能在需要的时候改变请求的细节。作为一个抽象类,我们必须继承自<code class="language-plaintext highlighter-rouge">NSURLProtocol</code>才能实现中间攻击的功能。</p>
<ul>
<li>
<p>是否要处理对应的请求。由于网页存在动态链接的可能性,简单的返回<code class="language-plaintext highlighter-rouge">YES</code>可能会创建大量的<code class="language-plaintext highlighter-rouge">NSURLProtocol</code>对象,因此我们需要保证每个请求能且仅能被返回一次<code class="language-plaintext highlighter-rouge">YES</code></p>
<ul>
<li>(BOOL)canInitWithRequest: (NSURLRequest *)request;</li>
<li>(BOOL)canInitWithTask: (NSURLSessionTask *)task;</li>
</ul>
</li>
<li>
<p>是否要对请求进行重定向,或者修改请求头、域名等关键信息。返回一个新的<code class="language-plaintext highlighter-rouge">NSURLRequest</code>对象来定制业务</p>
<ul>
<li>(NSURLRequest *)canonicalRequestForRequest: (NSURLRequest *)request;</li>
</ul>
</li>
<li>
<p>如果处理请求返回了<code class="language-plaintext highlighter-rouge">YES</code>,那么下面两个回调对应请求开始和结束阶段。在这里可以标记请求对象已经被处理过</p>
<ul>
<li>(void)startLoading;</li>
<li>(void)stopLoading;</li>
</ul>
</li>
</ul>
<p>当发起网络请求的时候,系统会像注册过的<code class="language-plaintext highlighter-rouge">NSURLProtocol</code>发起询问,判断是否需要处理修改该请求,通过一下代码来注册你的子类</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[NSURLProtocol registerClass: [CustomURLProtocol class]];
</code></pre></div></div>
<h1 id="dns解析">DNS解析</h1>
<p>一般情况下,考虑<code class="language-plaintext highlighter-rouge">DNS劫持</code>大多发生在使用<code class="language-plaintext highlighter-rouge">webView</code>的时候。相较于使用网页,正常的网络请求即便被劫持了无非是返回错误的数据、或者干脆<code class="language-plaintext highlighter-rouge">404</code>,而且对付劫持,普通请求还有其他方案选择,所以本文讨论的是如何处理网页加载的劫持。</p>
<h5 id="localdns">LocalDNS</h5>
<p><code class="language-plaintext highlighter-rouge">LocalDNS</code>是一种常见的防劫持方案。简单来说,在网页发起请求的时候获取请求域名,然后在本地进行解析得到<code class="language-plaintext highlighter-rouge">ip</code>,返回一个直接访问网页<code class="language-plaintext highlighter-rouge">ip</code>地址的请求。结构体<code class="language-plaintext highlighter-rouge">struct hostent</code>用来表示地址信息:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>struct hostent {
char *h_name; // official name of host
char **h_aliases; // alias list
int h_addrtype; // host address type——AF_INET || AF_INET6
int h_length; // length of address
char **h_addr_list; // list of addresses
};
</code></pre></div></div>
<p>C函数<code class="language-plaintext highlighter-rouge">gethostbyname</code>使用递归查询的方式将传入的域名转换成<code class="language-plaintext highlighter-rouge">struct hostent</code>结构体,但是这个函数存在一个缺陷:由于采用递归方式查询域名,常常会发生超时。但是<code class="language-plaintext highlighter-rouge">gethostbyname</code>本身不支持超时处理,所以这个函数调用的时候放到操作队列中执行,并且采用信号量等待<code class="language-plaintext highlighter-rouge">1.5</code>秒查询:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+ (struct hostent *)getHostByName: (const char *)hostName {
__block struct hostent * phost = NULL;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSOperationQueue * queue = [NSOperationQueue new];
queue.maxConcurrentOperationCount = 1;
[queue addOperationWithBlock: ^{
phost = gethostbyname(hostName);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 1.5 * NSEC_PER_SEC));
[queue cancelAllOperations];
return phost;
}
</code></pre></div></div>
<p>然后通过函数<code class="language-plaintext highlighter-rouge">inet_ntop</code>把结构体中的地址信息符号化,获得C字符串类型的地址信息。提供<code class="language-plaintext highlighter-rouge">getIpAddressFromHostName</code>方法隐藏对<code class="language-plaintext highlighter-rouge">ipv4</code>和<code class="language-plaintext highlighter-rouge">ipv6</code>地址的处理细节:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+ (NSString *)getIpv4AddressFromHost: (NSString *)host {
const char * hostName = host.UTF8String;
struct hostent * phost = [self getHostByName: hostName];
if ( phost == NULL ) { return nil; }
struct in_addr ip_addr;
memcpy(&ip_addr, phost->h_addr_list[0], 4);
char ip[20] = { 0 };
inet_ntop(AF_INET, &ip_addr, ip, sizeof(ip));
return [NSString stringWithUTF8String: ip];
}
+ (NSString *)getIpv6AddressFromHost: (NSString *)host {
const char * hostName = host.UTF8String;
struct hostent * phost = [self getHostByName: hostName];
if ( phost == NULL ) { return nil; }
char ip[32] = { 0 };
char ** aliases;
switch (phost->h_addrtype) {
case AF_INET:
case AF_INET6: {
for (aliases = phost->h_addr_list; *aliases != NULL; aliases++) {
NSString * ipAddress = [NSString stringWithUTF8String: inet_ntop(phost->h_addrtype, *aliases, ip, sizeof(ip))];
if (ipAddress) { return ipAddress; }
}
} break;
default:
break;
}
return nil;
}
+ (NSString *)getIpAddressFromHostName: (NSString *)host {
NSString * ipAddress = [self getIpv4AddressFromHost: host];
if (ipAddress == nil) {
ipAddress = [self getIpv6AddressFromHost: host];
}
return ipAddress;
}
</code></pre></div></div>
<h1 id="适配ipv6">适配IPv6</h1>
<p>苹果明确现在的的应用要支持<code class="language-plaintext highlighter-rouge">IPv6</code>地址,对于开发者来说,并没有太大的改动,无非是将<code class="language-plaintext highlighter-rouge">gethostbyname</code>改成另外一个函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>phost = gethostbyname2(host, AF_INET6);
</code></pre></div></div>
<p>另外就是解析域名过程中优先获取<code class="language-plaintext highlighter-rouge">IPv6</code>的地址而不是<code class="language-plaintext highlighter-rouge">IPv4</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+ (NSString *)getIpAddressFromHostName: (NSString *)host {
NSString * ipAddress = [self getIpv6AddressFromHost: host];
if (ipAddress == nil) {
ipAddress = [self getIpv4AddressFromHost: host];
}
return ipAddress;
}
</code></pre></div></div>
<h1 id="扩展">扩展</h1>
<p><code class="language-plaintext highlighter-rouge">localDNS</code>直接进行解析获取的<code class="language-plaintext highlighter-rouge">ip</code>地址可能不是最优选择,另一种做法是让应用每次启动后从服务器下发对应的<code class="language-plaintext highlighter-rouge">DNS</code>解析列表,直接从列表中获取<code class="language-plaintext highlighter-rouge">ip</code>地址访问。这种做法对比递归式的查询,无疑效率要更高一些,需要注意的是在下发请求过程中如何避免解析列表被中间人篡改。</p>
<p>因为请求地址可能无效,需要以<code class="language-plaintext highlighter-rouge">ip</code>映射<code class="language-plaintext highlighter-rouge">host</code>的映射表来保证在访问无效的地址之后能重新使用原来的域名发起请求。另外确定<code class="language-plaintext highlighter-rouge">ip</code>无效后应该维护一个无效地址表,用来域名解析后判断是否继续使用地址访问。整个域名解析过程大概如下:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao75/4.jpg" alt="1" /><br /></p>
<p>此外,如果你的应用还没有服务器下发<code class="language-plaintext highlighter-rouge">DNS</code>解析列表这一业务,那么直接使用<code class="language-plaintext highlighter-rouge">Local DNS</code>解析可能会遇到解析出来的<code class="language-plaintext highlighter-rouge">ip</code>无效问题。目前上面代码的处理是如果<code class="language-plaintext highlighter-rouge">ip</code>无效,发起回调让<code class="language-plaintext highlighter-rouge">webView</code>重新加载。除此之外有另外一种解决方案。应用本地存储一张需要访问到的域名表,然后在程序启动之后异步执行域名解析过程,参照<a href="http://nszzy.me/2016/09/07/dns-resolving/">DNS解析失败的处理 (支持IPv6)</a>一文,提前做好无效解析的处理。</p>
<h1 id="webkit">WebKit</h1>
<p><code class="language-plaintext highlighter-rouge">WKWebView</code>是苹果推出的<code class="language-plaintext highlighter-rouge">UIWebView</code>的替代方案,但前者还不够优秀以至于使用后者开发的大有人在。另外使用<code class="language-plaintext highlighter-rouge">NSURLProtocol</code>实现防<code class="language-plaintext highlighter-rouge">DNS劫持</code>功能的时候,在调起<code class="language-plaintext highlighter-rouge">canInitWithRequest:</code>后就再无下文。通过查阅资料发现想实现<code class="language-plaintext highlighter-rouge">WebKit</code>的请求拦截需要调用一些私有方法,<a href="https://blog.yeatse.com/2016/10/26/support-nsurlprotocol-in-wkwebview/">让 WKWebView 支持 NSURLProtocol</a>文章已经做了很好的处理,在文中的基础上,笔者对注册协议的过程多加了一层处理(毕竟苹果爸爸坑起我们来绝不手软):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static inline NSString * lxd_scheme_selector_suffix() {
return @"SchemeForCustomProtocol:";
}
static inline SEL lxd_register_scheme_selector() {
const NSString * const registerPrefix = @"register";
return NSSelectorFromString([registerPrefix stringByAppendingString: lxd_scheme_selector_suffix()]);
}
static inline SEL lxd_unregister_scheme_selector() {
const NSString * const unregisterPrefix = @"unregister";
return NSSelectorFromString([unregisterPrefix stringByAppendingString: lxd_scheme_selector_suffix()]);
}
</code></pre></div></div>
<h1 id="nsurlsession">NSURLSession</h1>
<p>在<code class="language-plaintext highlighter-rouge">AFNetworking</code>替换成<code class="language-plaintext highlighter-rouge">NSURLSession</code>实现之后,常规的<code class="language-plaintext highlighter-rouge">NSURLProtocol</code>已经不能拦截请求了。为了能继续实现拦截功能,需要在<code class="language-plaintext highlighter-rouge">NSURLSessionConfiguration</code>中设置对拦截类的支持:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[LXDDNSInterceptor class];
</code></pre></div></div>
<p>由于<code class="language-plaintext highlighter-rouge">AFNetworking</code>跟<code class="language-plaintext highlighter-rouge">SDWebImage</code>都是采用默认的<code class="language-plaintext highlighter-rouge">defaultSessionConfiguration</code>初始化请求会话对象的,因此直接<code class="language-plaintext highlighter-rouge">hook</code>掉这个默认方法可以实现拦截适配:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+ (NSURLSessionConfiguration *)lxd_defaultSessionConfiguration {
NSURLSessionConfiguration * configuration = [self lxd_defaultSessionConfiguration];
configuration.protocolClasses = @[LXDDNSInterceptor class];
return configuration;
}
</code></pre></div></div>
<p>但是为了避免省字数出现<code class="language-plaintext highlighter-rouge">[NSURLSessionConfiguration new]</code>的创建方式,<code class="language-plaintext highlighter-rouge">hook</code>上面的方法并不能保证能够拦截到请求。于是我把<code class="language-plaintext highlighter-rouge">hook</code>的目标放到了<code class="language-plaintext highlighter-rouge">NSURLSession</code>上,发现存在一个类方法构造器生成实例:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+ (NSURLSession *)sessionWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id<NSURLSessionDelegate>)delegate delegateQueue: (NSOperationQueue *)queue;
</code></pre></div></div>
<p>5<code class="language-plaintext highlighter-rouge">hook</code>这个类方法,然而在<code class="language-plaintext highlighter-rouge">class_getClassMethod</code>获取所有的方法列表输出之后发现竟然不存在这个类方法,取而代之的是一个<code class="language-plaintext highlighter-rouge">init</code>构造器:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao75/5.jpg" alt="1" /><br /></p>
<p>不知道这是不是苹果有意为之来误导开发者(<em>苹果:我是爸爸,规则我来定</em>)。但是通过代码联想又无法直接输出这个函数,于是通过<code class="language-plaintext highlighter-rouge">category</code>的方式暴露这个方法名,并且<code class="language-plaintext highlighter-rouge">hook</code>掉:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/// h文件
@interface NSURLSession (LXDIntercept)
- (instancetype)initWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id<NSURLSessionDelegate>)delegate delegateQueue: (NSOperationQueue *)queue;
@end
/// m文件
@implementation NSURLSession (LXDIntercept)
+ (void)load {
Method origin = class_getClassMethod([NSURLSession class], @selector(initWithConfiguration:delegate:delegateQueue:));
Method custom = class_getClassMethod([NSURLSession class], @selector(lxd_initWithConfiguration:delegate:delegateQueue:));
method_exchangeImplementations(origin, custom);
}
- (NSURLSession *)lxd_initWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id<NSURLSessionDelegate>)delegate delegateQueue: (NSOperationQueue *)queue {
if (lxd_url_session_configure) {
lxd_url_session_configure(configuration);
}
return [self lxd_initWithConfiguration: configuration delegate: delegate delegateQueue: queue];
}
@end
</code></pre></div></div>
<p>于是,又能愉快的在项目里面玩耍网络拦截啦。</p>
<p>本文demo:<a href="https://github.com/JustKeepRunning/LXDAppMonitor">LXDAppMonitor</a></p>
<h1 id="参考资料">参考资料</h1>
<p><a href="http://nshipster.cn/nsurlprotocol/">NSURLProtocol</a>
<a href="https://segmentfault.com/a/1190000004369289">iOS网络请求优化之DNS映射</a>
<a href="http://blog.csdn.net/kaitiren/article/details/51503269">iOS应用支持IPV6,就那点事儿</a>
<a href="https://blog.yeatse.com/2016/10/26/support-nsurlprotocol-in-wkwebview/">让 WKWebView 支持 NSURLProtocol</a></p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao75">iOS开发之DNS劫持</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on May 03, 2017.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao74
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao74
2017-04-05T00:00:00+08:00
2017-04-05T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<h2 id="前言">前言:</h2>
<blockquote>
<p>如果有测试大佬发现内容不对,欢迎指正,我会及时修改。</p>
</blockquote>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br /></p>
<p>来源:<a href="http://blog.csdn.net/hello_hwc/article/details/60957515#comments">黄文臣</a><br /></p>
<p>大多数的iOS App(没有持续集成)迭代流程是这样的</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/1.png" alt="1" /><br /></p>
<p>也就是说,测试是发布之前的最后一道关卡。如果bug不能在测试中发现,那么bug
就会抵达用户,所以测试的完整性和可靠性十分重要。</p>
<p>目前,大多数App还停留在人工测试阶段,人工测试投入的成本最低,能够保证核心功能的使用,而且测试人员不需要会写代码。</p>
<p>但是,在很多测试场景下,人工测试的效率太低,容易出错。举两个常见的例子:</p>
<ul>
<li>
<p>一个App的核心功能,在每一次发布版本前的测试必定会跑一遍所有的测试用例,不管对应的业务在当前版本有没有变化(天知道开发在做业务A的时候,对业务B有没有影响),如果这次测出新的bug,测试人员在下一次发版测试中,又不得不做这些重复的工作。</p>
</li>
<li>
<p>开发在写API请求相关代码的时候没有做数据容错,测试在人工测试的时候都是正常的数据,所以测试通过。上线了之后,后台配置数据的时候出了点小问题,导致大面积崩溃,boom~。</p>
</li>
</ul>
<p>然后,老板就要过来找你了</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/2.jpg" alt="1" /><br /></p>
<p>本文所讲解的均是基于XCode 8.2.1,有些概念可能不适用于低版本的XCode</p>
<h2 id="自动化测试">自动化测试</h2>
<p>自动化测试就是写一些测试代码,用代码代替人工去完成模块和业务的测试。
其实不管是开发还是测试,如果你在不断的做重复性工作的时候,就应该问自己一个问题:是不是有更高效的办法?</p>
<p>自动化测试有很多优点:</p>
<ul>
<li>
<p>测试速度快,避免重复性的工作</p>
</li>
<li>
<p>避免regression,让开发更有信心去修改和重构代码(个人认为最大的优点)</p>
</li>
<li>
<p>具有一致性。</p>
</li>
<li>
<p>有了自动化测试,持续集成(CI)会变得更可靠。</p>
</li>
<li>
<p>迫使开发人员写出更高质量的代码。(自动化测试不通过,代码不允许合并)</p>
</li>
</ul>
<p>当然,自动化测试也有一些缺点。</p>
<ul>
<li>
<p>开发和维护成本高。</p>
</li>
<li>
<p>不能完全替代人工测试。</p>
</li>
<li>
<p>无法完全保证测试的准确性 – 让代码去判断一段逻辑是否正确很容易,但是,让代码判断一个控件显示是否正确却没那么容易。</p>
</li>
</ul>
<p>所以,在做自动化测试之前,首先要问自己几个问题?</p>
<ul>
<li>
<p>这个测试业务的变动是否频繁?</p>
</li>
<li>
<p>这个测试业务是否属于核心功能?</p>
</li>
<li>
<p>编写测试代码的成本有多少?</p>
</li>
<li>
<p>自动化测试能保证测试结果的准确么?</p>
</li>
</ul>
<p>通常,我们会选择那些业务稳定,需要频繁测试的部分来编写自动化测试脚本,其余的采用人工测试,人工测试仍然是iOS App开发中不可缺少的一部分。</p>
<h2 id="测试种类">测试种类</h2>
<p>从是否接触源代码的角度来分类:测试分为黑盒和白盒(灰盒就是黑盒白盒结合,这里不做讨论)。</p>
<p>白盒测试的时候,测试人员是可以直接接触待测试App的源代码的。白盒测试更多的是单元测试,测试人员针对各个单元进行各种可能的输入分析,然后测试其输出。白盒测试的测试代码通常由iOS开发编写。</p>
<p>黑盒测试。黑盒测试的时候,测试人员不需要接触源代码。是从App层面对其行为以及UI的正确性进行验证,黑盒测试由iOS测试完成。</p>
<p>从业务的层次上来说,测试金字塔如图:</p>
<p>而iOS测试通常只有以下两个层次:</p>
<ul>
<li>
<p>Unit,单元测试,保证每一个类能够正常工作</p>
</li>
<li>
<p>UI,UI测试,也叫做集成测试,从业务层的角度保证各个业务可以正常工作。</p>
</li>
</ul>
<h2 id="框架选择">框架选择</h2>
<p>啰里八嗦讲的这么多,自动化测试的效率怎么样,关键还是在测试框架上。那么,如何选择测试框架呢?框架可以分为两大类:XCode内置的和三方库。</p>
<p>选择框架的时候有几个方面要考虑</p>
<ul>
<li>测试代码编写的成本</li>
<li>是否可调式</li>
<li>框架的稳定性</li>
<li>测试报告(截图,代码覆盖率,…)</li>
<li>WebView的支持(很多App都用到了H5)</li>
<li>自定义控件的测试</li>
<li>是否需要源代码</li>
<li>能否需要连着电脑</li>
<li>是否支持CI(持续集成)</li>
<li>….</li>
</ul>
<p>我们首先来看看XCode内置的框架:XCTest。XCTest又可以分为两部分:Unit Test 和 UI Test,分别对应单元测试和UI测试。有一些三方的测试库也是基于XCTest框架的,这个在后文会讲到。由于是Apple官方提供的,所以这个框架会不断完善。</p>
<p>成熟的三方框架通常提供了很多封装好的有好的接口,笔者综合对比了一些,推荐以下框架:</p>
<p>单元测试:</p>
<blockquote>
<p>以下三个框架都是BDD(<a href="https://en.wikipedia.org/wiki/Behavior-driven_development">Behavior-driven development</a>) – 行为驱动开发。行为驱动开发简单来说就是先定义行为,然后定义测试用例,接着再编写代码。 实践中发现,通常没有那么多时间来先定义行为,不过BDD中的domain-specific language (DSL)能够很好的描述用例的行为。</p>
</blockquote>
<p><a href="https://github.com/kiwi-bdd/Kiwi">Kiwi</a> 老牌测试框架</p>
<p><a href="https://github.com/specta/specta">specta</a> 另一个BDD优秀框架</p>
<p><a href="https://github.com/Quick/Quick">Quick</a> 三个项目中Star最多,支持OC和Swift,优先推荐。</p>
<p>UI测试:</p>
<p><a href="https://github.com/kif-framework/KIF">KIF</a> 基于XCTest的测试框架,调用私有API来控制UI,测试用例用Objective C或Swift编写。</p>
<p><a href="https://github.com/appium/appium">appium</a> 基于Client – Server的测试框架。App相当于一个Server,测试代码相当于Client,通过发送JSON来操作APP,测试语言可以是任意的,支持android和iOS。</p>
<blockquote>
<p>篇幅有限,本文会先介绍XCtest,接着三方的Unit框架会以Quick为例,UI Test框架侧重分析KIF,appium仅仅做原理讲解。</p>
</blockquote>
<h2 id="xctest">XCTest</h2>
<p>对于XCTest来说,最后生成的是一个bundle。bundle是不能直接执行的,必须依赖于一个宿主进程。关于XCTest进行单元测试的基础(XCode的使用,异步测试,性能测试,代码覆盖率等),我在这篇文章里讲解过,这里不再详细讲解。</p>
<p><a href="http://blog.csdn.net/hello_hwc/article/details/46671053">iOS 单元测试之XCTest详解</a></p>
<h2 id="单元测试用例">单元测试用例</h2>
<p>比如,我有以下一个函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//验证一段Text是否有效。(不能以空字符开头,不能为空)
- (BOOL)validText:(NSString *)text error:(NSError *__autoreleasing *)error{
}
</code></pre></div></div>
<p>那么,我该如何为这个函数编写单元测试的代码?通常,需要考虑以下用例:</p>
<p>1.输入以空白字符或者换行符开头的,error不为空,返回 NO</p>
<p>2.输入正确的内容,error为空,返回YES</p>
<p>3.输入为nil,error不为空,返回 NO (边界条件)</p>
<p>4.输入为非NSString类型,验证不通过,返回NO (错误输入)</p>
<p>5.特殊输入字符(标点符号,非英文等等)</p>
<h2 id="ui测试">UI测试</h2>
<p>UI测试是模拟用户操作,进而从业务处层面测试。关于XCTest的UI测试,建议看看WWDC 2015的这个视频:</p>
<ul>
<li><a href="https://developer.apple.com/videos/play/wwdc2015/406/">UI Testing in Xcode</a></li>
</ul>
<p>关于UI测试,有几个核心类需要掌握</p>
<ul>
<li><a href="https://developer.apple.com/reference/xctest/xcuiapplication">XCUIApplication</a> 测试应用的代理</li>
<li><a href="https://developer.apple.com/reference/xctest/xcuielement">XCUIElement</a> 一个UI上可见的视图对象</li>
<li><a href="https://developer.apple.com/reference/xctest/xcuielementquery">XCUIElementQuery</a> 查找XCUIElement</li>
</ul>
<p>UI测试还有一个核心功能是UI Recording。选中一个UI测试用例,然后点击图中的小红点既可以开始UI Recoding。你会发现:</p>
<p>随着点击模拟器,自动合成了测试代码。(通常自动合成代码后,还需要手动的去调整)</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/3.png" alt="1" /><br /></p>
<p>在写UI测试用例的时候要注意:测试行为而不是测试代码。比如,我们测试这样一个case</p>
<p>进入Todo首页,点击add,进入添加页面,输入文字,点击save。</p>
<p>测试效果如下:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/4.gif" alt="1" /><br /></p>
<p>对应测试代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)testAddNewItems{
//获取app代理
XCUIApplication *app = [[XCUIApplication alloc] init];
//找到第一个tabeview,就是我们想要的tableview
XCUIElement * table = [app.tables elementBoundByIndex:0];
//记录下来添加之前的数量
NSInteger oldCount = table.cells.count;
//点击Add
[app.navigationBars[@"ToDo"].buttons[@"Add"] tap];
//找到Textfield
XCUIElement *inputWhatYouWantTodoTextField = app.textFields[@"Input what you want todo"];
//点击Textfield
[inputWhatYouWantTodoTextField tap];
//输入字符
[inputWhatYouWantTodoTextField typeText:@"somethingtodo"];
//点击保存
[app.navigationBars[@"Add"].buttons[@"Save"] tap];
//获取当前的数量
NSInteger newCount = table.cells.count;
//如果cells的数量加一,则认为测试成功
XCTAssert(newCount == oldCount + 1);
}
</code></pre></div></div>
<p>这里是通过前后tableview的row数量来断言成功或者失败。</p>
<h2 id="等待">等待</h2>
<p>通常,在视图切换的时候有转场动画,我们需要等待动画结束,然后才能继续,否则query的时候很可能找不到我们想要的控件。</p>
<p>比如,如下代码等待VC转场结束,当query只有一个table的时候,才继续执行后续的代码。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[self expectationForPredicate:[NSPredicate predicateWithFormat:@"self.count = 1"]
evaluatedWithObject:app.tables
handler:nil];
[self waitForExpectationsWithTimeout:2.0 handler:nil];
//后续代码....
</code></pre></div></div>
<blockquote>
<p>Tips: 当你的UI结构比较复杂的时候,比如各种嵌套childViewController,使用XCUIElementQuery的代码会很长,也不好维护。</p>
</blockquote>
<blockquote>
<p>另外,UI测试还会在每一步操作的时候截图,方便对测试报告进行验证。</p>
</blockquote>
<h2 id="查看测试结果">查看测试结果</h2>
<p>使用基于XCTest的框架,可以在XCode的report navigator中查看测试结果。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/5.png" alt="1" /><br /></p>
<p>其中:</p>
<ul>
<li>
<p>Tests 用来查看详细的测试过程</p>
</li>
<li>
<p>Coverage 用来查看代码覆盖率</p>
</li>
<li>
<p>Logs 用来查看测试的日志</p>
</li>
<li>
<p>点击图中的红色框指向的图标可以看到每一步UI操作的截图</p>
</li>
</ul>
<p>除了利用XCode的GUI,还可以通过后文提到的命令行工具来测试,查看结果。</p>
<h2 id="stubmock">Stub/Mock</h2>
<p>首先解释两个术语:</p>
<ul>
<li>
<p>mock 表示一个模拟对象</p>
</li>
<li>
<p>stub 追踪方法的调用,在方法调用的时候返回指定的值。</p>
</li>
</ul>
<p>通常,如果你采用纯存的XCTest,推荐采用OCMock来实现mock和stub,单元测试的三方库通常已集成了stub和mock。</p>
<p>那么,如何使用mock呢?举个官方的例子:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//mock一个NSUserDefaults对象
id userDefaultsMock = OCMClassMock([NSUserDefaults class]);
//在调用stringForKey的时候,返回http://testurl
OCMStub([userDefaultsMock
stringForKey:@"MyAppURLKey"]).andReturn(@"http://testurl");
</code></pre></div></div>
<p>再比如,我们要测试打开其他App,那么如何判断确实打开了其他App呢?</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id app = OCMClassMock([UIApplication class]);
OCMStub([app sharedInstance]).andReturn(app);
OCMVerify([app openURL:url]
</code></pre></div></div>
<p>使用Stub可以让我们很方便的实现这个。</p>
<p>关于OCMock的使用,推荐看看objc.io的这篇文章</p>
<ul>
<li><a href="https://objccn.io/issue-15-5/">置换测试: Mock, Stub 和其他</a></li>
</ul>
<h2 id="quick">Quick</h2>
<blockquote>
<p>Quick是建立在XCTestSuite上的框架,使用XCTestSuite允许你动态创建测试用例。所以,使用Quick,你仍让可以使用XCode的测试相关GUI和命令行工具。</p>
</blockquote>
<p>使用Quick编写的测试用例看起来是这样子的:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import Quick
import Nimble
class TableOfContentsSpec: QuickSpec {
override func spec() {
describe("the 'Documentation' directory") {
it("has everything you need to get started") {
let sections = Directory("Documentation").sections
expect(sections).to(contain("Organized Tests with Quick Examples and Example Groups"))
expect(sections).to(contain("Installing Quick"))
}
context("if it doesn't have what you're looking for") {
it("needs to be updated") {
let you = You(awesome: true)
expect{you.submittedAnIssue}.toEventually(beTruthy())
}
}
}
}
}
</code></pre></div></div>
<p>BDD的框架让测试用例的目的更加明确,测试是否通过更加清晰。使用Quick,测试用例分为两种:</p>
<h2 id="单独的用例--使用it来描述">单独的用例 – 使用it来描述</h2>
<p>it有两个参数,</p>
<ul>
<li>
<p>行为描述</p>
</li>
<li>
<p>行为的测试代码</p>
</li>
</ul>
<p>比如,以下测试Dolphin行为,它具有行为is friendly和is smart</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//Swift代码
class DolphinSpec: QuickSpec {
override func spec() {
it("is friendly") {
expect(Dolphin().isFriendly).to(beTruthy())
}
it("is smart") {
expect(Dolphin().isSmart).to(beTruthy())
}
}
}
</code></pre></div></div>
<p>可以看到,BDD的核心是行为。也就是说,需要关注的是一个类提供哪些行为。
用例集合,用describe和context描述</p>
<p>比如,验证dolphin的click行为的时候,我们需要两个用例。一个是is loud,一个是has a high frequency,就可以用describe将用例组织起来。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
describe("its click") {
it("is loud") {
let click = Dolphin().click()
expect(click.isLoud).to(beTruthy())
}
it("has a high frequency") {
let click = Dolphin().click()
expect(click.hasHighFrequency).to(beTruthy())
}
}
}
}
}
</code></pre></div></div>
<p>context可以指定用例的条件:</p>
<p>比如</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>describe("its click") {
context("when the dolphin is not near anything interesting") {
it("is only emitted once") {
expect(dolphin!.click().count).to(equal(1))
}
}
}
</code></pre></div></div>
<p>除了这些之外,Quick也支持一些切入点,进行测试前的配置:</p>
<ul>
<li>
<p>beforeEach</p>
</li>
<li>
<p>afterEach</p>
</li>
<li>
<p>beforeAll</p>
</li>
<li>
<p>afterAll</p>
</li>
<li>
<p>beforeSuite</p>
</li>
<li>
<p>afterSuite</p>
</li>
</ul>
<h2 id="nimble">Nimble</h2>
<p>由于Quick是基于XCTest,开发者当然可以收使用断言来定义测试用例成功或者失败。Quick提供了一个更有好的Framework来进行这种断言:Nimble</p>
<p>比如,一个常见的XCTest断言如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>XCTAssertTrue(ConditionCode, "FailReason")
</code></pre></div></div>
<p>在出错的时候,会提示</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>XCAssertTrue failed, balabala
</code></pre></div></div>
<p>这时候,开发者要打个断点,查看下上下文,看看具体失败的原因在哪。</p>
<p>使用Nimble后,断言变成类似</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>expect(1 + 1).to(equal(2))
expect(3) > 2
expect("seahorse").to(contain("sea"))
expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi"))
</code></pre></div></div>
<p>并且,出错的时候,提示信息会带着上下文的值信息,让开发者更容易的找到错误。</p>
<h2 id="让你的代码更容易单元测试">让你的代码更容易单元测试</h2>
<p>测试的准确性和工作量很大程度上依赖于开发人员的代码质量。</p>
<p>通常,为了单元测试的准确性,我们在写函数(方法)的时候会借鉴一些函数式编程的思想。其中最重要的一个思想就是</p>
<ul>
<li>pure function(纯函数)</li>
</ul>
<p>何为Pure function?就是如果一个函数的输入一样,那么输出一定一样。</p>
<p>比如,这样的一个函数就不是pure function。因为它依赖于外部变量value的值。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static NSInteger value = 0;
- (NSInteger)function_1{
value = value + 1;
return value;
}
</code></pre></div></div>
<p>而这个函数就是pure function,因为给定输入,输出一定一致。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (NSInteger)function_2:(NSInteger)base{
NSInteger value = base + 1;
return value;
}
</code></pre></div></div>
<p>所以,如果你写了一个没有参数,或者没有返回值的方法,那么你要小心了,很可能这个方法很难测试。</p>
<h2 id="关于mvc">关于MVC</h2>
<p>在良好的MVC架构的App中,</p>
<ul>
<li>
<p>View只做纯粹的展示型工作,把用户交互通过各种方式传递到外部</p>
</li>
<li>
<p>Model只做数据存储类工作</p>
</li>
<li>
<p>Controller作为View和Model的枢纽,往往要和很多View和Model进行交互,也是自动化包括代码维护的痛点。</p>
</li>
</ul>
<p>所以,对Controller瘦身是iOS架构中比较重要的一环,一些通用的技巧包括:</p>
<p>逻辑抽离:</p>
<ul>
<li>
<p>网络请求独立。可以每个网络请求以Command模式封装成一个对象,不要直接在Controller调用AFNetworking。</p>
</li>
<li>
<p>数据存储独立。建立独立的Store类,用来做数据持久化和缓存。</p>
</li>
<li>
<p>共有数据服务化(协议)。比如登录状态等等,通过服务去访问,这样服务提供者之需要处理服务的质量,服务使用者则信任服务提供者的结果。</p>
</li>
</ul>
<p>Controller与View解耦合</p>
<ul>
<li>
<p>建立ViewModel层,这样Controller只需要和ViewModel进行交互。</p>
</li>
<li>
<p>建立UIView子类作为容器,将一些View放到容器后再把容器作为SubView添加到Controller里</p>
</li>
<li>
<p>建立可复用的Layout层,不管是AutoLayout还是手动布局。</p>
</li>
</ul>
<p>Controller与Controller解耦合</p>
<ul>
<li>建立页面路由。每一个界面都抽象为一个URL,跳转仅仅通过Intent或者URL跳转,这样两个Controller完全独立。</li>
</ul>
<blockquote>
<p>如果你的App用Swift开发,那么面向协议编程和不可变的值类型会让你的代码更容易测试。</p>
</blockquote>
<p>当然,iOS组建化对自动化测试的帮助也很大,因为不管是基础组件还是业务组件,都可以独立测试。组建化又是一个很大的课题,这里不深入讲解了。</p>
<h2 id="kif">KIF</h2>
<p>KIF的全称是Keep it functional。它是一个建立在XCTest的UI测试框架,通过accessibility来定位具体的控件,再利用私有的API来操作UI。由于是建立在XCTest上的,所以你可以完美的借助XCode的测试相关工具(包括命令行脚本)。</p>
<blockquote>
<p>KIF是个人非常推荐的一个框架,简单易用。</p>
</blockquote>
<p>使用KIF框架强制要求你的代码支持accessibility。如果你之前没接触过,可以看看Apple的文档</p>
<p><a href="https://developer.apple.com/library/prerelease/content/documentation/UserExperience/Conceptual/iPhoneAccessibility/Introduction/Introduction.html">Accessibility…</a>
6</p>
<p>简单来说,accessibility能够让视觉障碍人士使用你的App。每一个控件都有一个描述AccessibilityLabel。在开启VoiceOver的时候,点击控件就可以选中并且听到对应的描述。</p>
<p>通常UIKit的控件是支持accessibility的,自定定义控件可以通过代码或者Storyboard上设置。</p>
<p>在Storyboard上设置:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/6.png" alt="1" /><br /></p>
<p>上面的通过Runtime Attributes设置(KVC)
下面的通过GUI来设置
通过代码设置:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[alert setAccessibilityLabel:@"Label"];
[alert setAccessibilityValue:@"Value"];
[alert setAccessibilityTraits:UIAccessibilityTraitButton];
</code></pre></div></div>
<p>如果你有些Accessibility的经验,那么你肯定知道,像TableView的这种不应该支持VoiceOver的。我们可以用条件编译来只对测试Target进行设置:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#ifdef DEBUG
[tableView setAccessibilityValue:@"Main List Table"];
#endif
#ifdef KIF_TARGET (这个值需要在build settings里设置)
[tableView setAccessibilityValue:@"Main List Table"];
#endif
</code></pre></div></div>
<p>使用KIF主要有两个核心类:</p>
<ul>
<li>
<p>KIFTestCase XCTestCase的子类</p>
</li>
<li>
<p>KIFUITestActor 控制UI,常见的三种是:点击一个View,向一个View输入内容,等待一个View的出现</p>
</li>
</ul>
<p>我们用KIF来测试添加一个新的ToDo</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)testAddANewItem{
[tester tapViewWithAccessibilityLabel:@"Add"];
[tester enterText:@"Create a test to do item" intoViewWithAccessibilityLabel:@"Input what you want todo"];
[tester tapViewWithAccessibilityLabel:@"Save"];
[tester waitForTimeInterval:0.2];
[tester waitForViewWithAccessibilityLabel:@"Create a test to do item"];
}
</code></pre></div></div>
<h2 id="命令行">命令行</h2>
<p>自动化测试中,命令行工具可以facebook的开源项目:</p>
<p><a href="https://github.com/facebook/xctool">xctool</a></p>
<p>这是一个基于xcodebuild命令的扩展,在iOS自动化测试和持续集成领域很有用,而且它支持-parallelize并行测试多个bundle,大大提高测试效率。</p>
<p>安装XCTool,</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew install xctool
</code></pre></div></div>
<p>使用</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>path/to/xctool.sh \
-workspace YourWorkspace.xcworkspace \
-scheme YourScheme \
-reporter plain:/path/to/plain-output.txt \
run-test
</code></pre></div></div>
<p>并且,xctool对于持续集成很有用,iOS常用的持续集成的server有两个:</p>
<ul>
<li><a href="https://travis-ci.org">Travis CI</a> 对于公开仓库(比如github)免费,私有仓库收费</li>
<li><a href="https://jenkins.io/index.html">Jenkins</a> 免费</li>
</ul>
<h2 id="优化你的测试代码">优化你的测试代码</h2>
<h3 id="准确的测试用例">准确的测试用例</h3>
<p>通常,你的你的测试用例分为三部分:</p>
<ul>
<li>
<p>配置测试的初始状态</p>
</li>
<li>
<p>对要测试的目标执行代码</p>
</li>
<li>
<p>对测试结果进行断言(成功 or 失败)</p>
</li>
</ul>
<h3 id="测试代码结构">测试代码结构</h3>
<p>当测试用例多了,你会发现测试代码编写和维护也是一个技术活。通常,我们会从几个角度考虑:</p>
<ul>
<li>
<p>不要测试私有方法(封装是OOP的核心思想之一,不要为了测试破坏封装)</p>
</li>
<li>
<p>对用例分组(功能,业务相似)</p>
</li>
<li>
<p>对单个用例保证测试独立(不受之前测试的影响,不影响之后的测试),这也是测试是否准确的核心。</p>
</li>
<li>
<p>提取公共的代码和操作,减少copy/paste这类工作,测试用例是上层调用,只关心业务逻辑,不关心内部代码实现。</p>
</li>
</ul>
<p>一个常见的测试代码组织如下:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/7.png" alt="1" /><br /></p>
<h2 id="appium">appium</h2>
<p>appium采用了Client Server的模式。对于App来说就是一个Server,基于<a href="https://w3c.github.io/webdriver/webdriver-spec.html">WebDriver JSON wire protocol</a>对实际的UI操作库进行了封装,并且暴露出RESTFUL的接口。然后测试代码通过HTTP请求的方式,来进行实际的测试。其中,实际驱动UI的框架根据系统版本有所不同:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>< 9.3 采用UIAutomation
>= 9.3 XCUITest
</code></pre></div></div>
<p>原因也比较简单:Apple在10.0之后,移除了UIAutomation的支持,只支持XCUITest。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao74/8.png" alt="1" /><br /></p>
<p>对比KIF,appium有它的优点:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>跨平台,支持iOS,Android
测试代码可以由多种语言编写,这对测试来说门槛更低
测试脚本独立与源代码和测试框架
</code></pre></div></div>
<p>当然,任何框架都有缺点:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>自定义控件支持不好
WebView的支持不好
</code></pre></div></div>
<h2 id="总结">总结</h2>
<p>由于我不是专业的iOS测试,关于测试的一点见解如下:</p>
<ul>
<li>
<p>单元测试还是选择BDD框架,毕竟可读性高一些,推荐<a href="https://github.com/Quick/Quick">Quick</a>(Swift),<a href="https://github.com/kiwi-bdd/Kiwi">Kiwi</a>(Objective C)</p>
</li>
<li>
<p>UI测试优先推荐<a href="https://github.com/kif-framework/KIF">KIF</a>,如果需要兼顾安卓测试,或者测试人员对OC/Swift很陌生,可以采用<a href="https://github.com/appium/appium">appium</a></p>
</li>
</ul>
<h2 id="参考资料">参考资料</h2>
<p><a href="https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/01-introduction.html#//apple_ref/doc/uid/TP40014132-CH1-SW1">Testing with Xcode</a> 官方文档,关于XCTest以及XCode有详细的讲解
<a href="https://objccn.io/issue-1-3/">objc.io</a>关于测试的资料对于官方文档的补充
<a href="http://tmq.qq.com/?s=ios">腾讯移动品质中心</a> 鹅厂移动品质中心,有很多好文章,强力推荐。
<a href="https://zhuanlan.zhihu.com/p/22283843">基于 KIF 的 iOS UI 自动化测试和持续集成</a> 美团点评技术团队写的一篇博客
<a href="https://realm.io/news/testing-in-swift/">testing-in-swift</a>
<a href="http://wereadteam.github.io/2016/08/23/Typesetter/#comments">微信读书排版引擎自动化测试方案</a></p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao74">iOS开发之自动化测试的那些干货</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on April 05, 2017.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao73
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao73
2017-03-27T00:00:00+08:00
2017-03-27T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="//allluckly.cn/images/blog/tuogao/tougao73/1.png" alt="1" /><br /></p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://sindrilin.com/monitor/2017/03/24/卡顿检测#more">SindriLin的小巢</a><br /></p>
<h2 id="前言">前言:</h2>
<p>在很早之前就有过实现一套自己的iOS监控体系,但首先是<code class="language-plaintext highlighter-rouge">instrument</code>足够的优秀,几乎所有监控相关的操作都有对应的工具。二来,也是笔者没(lan)时(de)间(zuo),项目大多也集成了第三方的统计SDK,所以迟迟没有去实现。这段时间,因为代码设计上存在的缺陷,导致项目在iphone5s以下的设备运行时会出现比较明显的卡顿现象。虽然<code class="language-plaintext highlighter-rouge">instrument</code>足够优秀,但笔者更希望在程序运行期间能及时获取卡顿信息,因此开始动手自己的卡顿检测方案。</p>
<h2 id="获取栈上下文">获取栈上下文</h2>
<p>任何监控体系在监控到目标事件发生时,获取线程的调用栈上下文是必须的,问题在于如何挂起当前线程并且获取线程信息。好在网上有大神分享了足够多的资料供笔者查阅,让笔者可以站在巨人的肩膀上来完成这部分业务。</p>
<p>demo中获取调用栈代码重写自<a href="https://github.com/bestswifter/BSBacktraceLogger">BSBacktraceLogger</a></p>
<p>,在使用之前建议能结合下方的参考资料和源代码一起阅览,知其然知其所以然。栈是一种后进先出(LIFO)的数据结构,对于一个线程来说,其调用栈的结构如下:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao73/2.png" alt="1" /><br /></p>
<p>调用栈上每一个单位被称作栈帧(stack frame),每一个栈帧由<strong>函数参数</strong>、<strong>返回地址</strong>以及<strong>栈帧中的变量</strong>组成,其中<code class="language-plaintext highlighter-rouge">Frame Pointer</code>指向内存存储了上一栈帧的地址信息。换句话说,只要能获取到栈顶的<code class="language-plaintext highlighter-rouge">Frame Pointer</code>就能递归遍历整个栈上的帧,遍历栈帧的核心代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define MAX_FRAME_NUMBER 30
#define FAILED_UINT_PTR_ADDRESS 0
NSString * _lxd_backtraceOfThread(thread_t thread) {
uintptr_t backtraceBuffer[MAX_FRAME_NUMBER];
int idx = 0;
......
LXDStackFrameEntry frame = { 0 };
const uintptr_t framePtr = lxd_mach_framePointer(&machineContext);
if (framePtr == FAILED_UINT_PTR_ADDRESS ||
lxd_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) {
return @"failed to get frame pointer";
}
for (; idx < MAX_FRAME_NUMBER; idx++) {
backtraceBuffer[idx] = frame.return_address;
if (backtraceBuffer[idx] == FAILED_UINT_PTR_ADDRESS ||
frame.previous == NULL ||
lxd_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
break;
}
}
}
</code></pre></div></div>
<p>从栈帧中我们只能获取到调用函数的地址信息,为了输出上下文数据,我们还需要根据地址进行符号化,即找到地址所在的内存镜像,然后定位该镜像中的符号表,最后从符号表中匹配地址对应的符号输出。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao73/3.png" alt="1" /><br /></p>
<p>符号化过程中包括不限于以下的数据结构:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef struct dl_info {
const char *dli_fname;
void *dli_fbase;
const char *dli_sname;
void *dli_saddr;
} Dl_info;
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Dl_info</code>存储了包括路径名、镜像起始地址、符号地址和符号名等信息</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>struct symtab_command {
uint32_t cmd;
uint32_t cmdsize;
uint32_t symoff;
uint32_t nsyms;
uint32_t stroff;
uint32_t strsize;
};
</code></pre></div></div>
<p>提供了符号表的偏移量,以及元素个数,还有字符串表的偏移和其长度。更多堆栈的资料可以参考文末最后三个链接学习。符号化的核心函数<code class="language-plaintext highlighter-rouge">lxd_dladdr</code>如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bool lxd_dladdr(const uintptr_t address, Dl_info * const info) {
info->dli_fname = NULL;
info->dli_fbase = NULL;
info->dli_sname = NULL;
info->dli_saddr = NULL;
const uint32_t idx = lxd_imageIndexContainingAddress(address);
if (idx == UINT_MAX) { return false; }
const struct mach_header * header = _dyld_get_image_header(idx);
const uintptr_t imageVMAddressSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
const uintptr_t addressWithSlide = address - imageVMAddressSlide;
const uintptr_t segmentBase = lxd_segmentBaseOfImageIndex(idx) + imageVMAddressSlide;
if (segmentBase == FAILED_UINT_PTR_ADDRESS) { return false; }
info->dli_fbase = (void *)header;
info->dli_fname = _dyld_get_image_name(idx);
const LXD_NLIST * bestMatch = NULL;
uintptr_t bestDistance = ULONG_MAX;
uintptr_t cmdPtr = lxd_firstCmdAfterHeader(header);
if (cmdPtr == FAILED_UINT_PTR_ADDRESS) { return false; }
for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
const struct load_command * loadCmd = (struct load_command *)cmdPtr;
if (loadCmd->cmd == LC_SYMTAB) {
const struct symtab_command * symtabCmd = (struct symtab_command *)cmdPtr;
const LXD_NLIST * symbolTable = (LXD_NLIST *)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
if (symbolTable[iSym].n_value == FAILED_UINT_PTR_ADDRESS) { continue; }
uintptr_t symbolBase = symbolTable[iSym].n_value;
uintptr_t currentDistance = addressWithSlide - symbolBase;
if ( (addressWithSlide >= symbolBase && currentDistance <= bestDistance) ) {
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
if (bestMatch != NULL) {
info->dli_saddr = (void *)(bestMatch->n_value + imageVMAddressSlide);
info->dli_sname = (char *)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
if (*info->dli_sname == '_') {
info->dli_sname++;
}
if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
info->dli_sname = NULL;
}
break;
}
}
cmdPtr += loadCmd->cmdsize;
}
return true;
}
</code></pre></div></div>
<p><del>整个符号化过程可以用下面的图表示</del>(*ps:经过<a href="http://www.jianshu.com/u/9c51a213b02e">joy__</a></p>
<p>说明,前面放上的图虽然在操作上类似,但是图示是fishhook的过程,因此删除旧图片*)</p>
<h2 id="关于runloop">关于RunLoop</h2>
<p><code class="language-plaintext highlighter-rouge">RunLoop</code>是一个重复接收着端口信号和事件源的死循环,它不断的唤醒沉睡,主线程的<code class="language-plaintext highlighter-rouge">RunLoop</code>在应用跑起来的时候就自动启动,<code class="language-plaintext highlighter-rouge">RunLoop</code>的执行流程由下图表示:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao73/4.png" alt="1" /><br /></p>
<p>在<a href="https://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c">CFRunLoop.c</a>中,可以看到<code class="language-plaintext highlighter-rouge">RunLoop</code>的执行代码大致如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
</code></pre></div></div>
<p>通过源码不难发现<code class="language-plaintext highlighter-rouge">RunLoop</code>处理事件的时间主要出在两个阶段:</p>
<ul>
<li>
<p><code class="language-plaintext highlighter-rouge">kCFRunLoopBeforeSources</code>和<code class="language-plaintext highlighter-rouge">kCFRunLoopBeforeWaiting</code>之间</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">kCFRunLoopAfterWaiting</code>之后</p>
</li>
</ul>
<h2 id="监控runloop状态检测超时">监控RunLoop状态检测超时</h2>
<p>通过<code class="language-plaintext highlighter-rouge">RunLoop</code>的源码我们已经知道了主线程处理事件的时间,那么如何检测应用是否发生了卡顿呢?为了找到合理的处理方案,笔者先监听<code class="language-plaintext highlighter-rouge">RunLoop</code>的状态并且输出:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static void lxdRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info) {
SHAREDMONITOR.currentActivity = activity;
dispatch_semaphore_signal(SHAREDMONITOR.semphore);
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"runloop entry");
break;
case kCFRunLoopExit:
NSLog(@"runloop exit");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"runloop after waiting");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"runloop before timers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"runloop before sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"runloop before waiting");
break;
default:
break;
}
};
</code></pre></div></div>
<p>运行之后输出的结果是滚动引发的<code class="language-plaintext highlighter-rouge">Sources</code>事件总是被快速的执行完成,然后进入到<code class="language-plaintext highlighter-rouge">kCFRunLoopBeforeWaiting</code>状态下。假如在滚动过程中发生了卡顿现象,那么<code class="language-plaintext highlighter-rouge">RunLoop</code>必然会保持<code class="language-plaintext highlighter-rouge">kCFRunLoopAfterWaiting</code>或者<code class="language-plaintext highlighter-rouge">kCFRunLoopBeforeSources</code>这两个状态之一。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao73/5.gif" alt="1" /><br /></p>
<p>为了实现卡顿的检测,首先需要注册<code class="language-plaintext highlighter-rouge">RunLoop</code>的监听回调,保存<code class="language-plaintext highlighter-rouge">RunLoop</code>状态;其次,通过创建子线程循环监听主线程<code class="language-plaintext highlighter-rouge">RunLoop</code>的状态来检测是否存在停留卡顿现象: <code class="language-plaintext highlighter-rouge">收到Sources相关的事件时,将超时阙值时间内分割成多个时间片段,重复去获取当前RunLoop的状态。如果多次处在处理事件的状态下,那么可以视作发生了卡顿现象</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define SHAREDMONITOR [LXDAppFluecyMonitor sharedMonitor]
@interface LXDAppFluecyMonitor : NSObject
@property (nonatomic, assign) int timeOut;
@property (nonatomic, assign) BOOL isMonitoring;
@property (nonatomic, assign) CFRunLoopActivity currentActivity;
+ (instancetype)sharedMonitor;
- (void)startMonitoring;
- (void)stopMonitoring;
@end
- (void)startMonitoring {
dispatch_async(lxd_fluecy_monitor_queue(), ^{
while (SHAREDMONITOR.isMonitoring) {
long waitTime = dispatch_semaphore_wait(self.semphore, dispatch_time(DISPATCH_TIME_NOW, lxd_wait_interval));
if (waitTime != LXD_SEMPHORE_SUCCESS) {
if (SHAREDMONITOR.currentActivity == kCFRunLoopBeforeSources ||
SHAREDMONITOR.currentActivity == kCFRunLoopAfterWaiting) {
if (++SHAREDMONITOR.timeOut < 5) {
continue;
}
[LXDBacktraceLogger lxd_logMain];
[NSThread sleepForTimeInterval: lxd_restore_interval];
}
}
SHAREDMONITOR.timeOut = 0;
}
});
}
</code></pre></div></div>
<h2 id="标记位检测线程超时">标记位检测线程超时</h2>
<p>与UI卡顿不同的事,事件处理往往是处在<code class="language-plaintext highlighter-rouge">kCFRunLoopBeforeWaiting</code>的状态下收到了<code class="language-plaintext highlighter-rouge">Sources</code>事件源,最开始笔者尝试同样以多个时间片段查询的方式处理。但是由于主线程的<code class="language-plaintext highlighter-rouge">RunLoop</code>在闲置时基本处于<code class="language-plaintext highlighter-rouge">Before Waiting</code>状态,这就导致了即便没有发生任何卡顿,这种检测方式也总能认定主线程处在卡顿状态。</p>
<p>就在这时候寒神(<a href="http://www.jianshu.com/u/cc1e4faec5f7">南栀倾寒</a>)推荐给我一套<code class="language-plaintext highlighter-rouge">Swift</code>的卡顿检测第三方<a href="https://github.com/zixun/ANREye">ANREye</a>,这套卡顿监控方案大致思路为:创建一个子线程进行循环检测,每次检测时设置标记位为<code class="language-plaintext highlighter-rouge">YES</code>,然后派发任务到主线程中将标记位设置为<code class="language-plaintext highlighter-rouge">NO</code>。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成<code class="language-plaintext highlighter-rouge">NO</code>。如果没有说明主线程发生了卡顿,无法处理派发任务:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao73/6.png" alt="1" /><br /></p>
<p>事后发现在特定情况下,这种检测方式会出错:当主线程被<code class="language-plaintext highlighter-rouge">async</code>大量的执行任务时,每个任务执行时间小于卡顿时间阙值,即对操作无影响。这时候由于设置标志位的<code class="language-plaintext highlighter-rouge">async</code>任务位置过于靠后,导致子线程沉睡后未能成功设置,造成卡顿误报的现象。(<em>ps:当然,实测结果是基本不可能发生这种现象</em>)这套方案解决了上面监听<code class="language-plaintext highlighter-rouge">RunLoop</code>的缺陷。结合这套方案,当主线程处在<code class="language-plaintext highlighter-rouge">Before Waiting</code>状态的时候,通过派发任务到主线程来设置标记位的方式处理常态下的卡顿检测:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dispatch_async(lxd_event_monitor_queue(), ^{
while (SHAREDMONITOR.isMonitoring) {
if (SHAREDMONITOR.currentActivity == kCFRunLoopBeforeWaiting) {
__block BOOL timeOut = YES;
dispatch_async(dispatch_get_main_queue(), ^{
timeOut = NO;
dispatch_semaphore_signal(SHAREDMONITOR.eventSemphore);
});
[NSThread sleepForTimeInterval: lxd_time_out_interval];
if (timeOut) {
[LXDBacktraceLogger lxd_logMain];
}
dispatch_wait(SHAREDMONITOR.eventSemphore, DISPATCH_TIME_FOREVER);
}
}
});
</code></pre></div></div>
<h2 id="尾言">尾言</h2>
<p>多数开发者对于<code class="language-plaintext highlighter-rouge">RunLoop</code>可能并没有进行实际的应用开发过,或者说即便了解<code class="language-plaintext highlighter-rouge">RunLoop</code>也只是处在理论的认知上。当然,也包括调用堆栈追溯的技术。本文旨在通过自身实现的卡顿监控代码来让更多开发者去了解这些深层次的运用与实践。</p>
<p>此外,上面两种检测方案可以兼并使用,甚至只使用后者进行主线程的卡顿检测也是可以的,本文demo已经上传:<a href="https://github.com/JustKeepRunning/LXDAppFluecyMonitor">LXDAppFluecyMonitor</a></p>
<p>##参考资料</p>
<p><a href="http://blog.ibireme.com/2015/05/18/runloop/">深入了解RunLoop</a>
<a href="http://www.jianshu.com/p/8123fc17fe0e">移动端监控体系之技术原理</a>
<a href="http://www.jianshu.com/p/9e1f4d771e35">趣探 Mach-O:FishHook 解析</a>
<a href="http://blog.csdn.net/jasonblog">iOS中线程Call Stack的捕获和解析1-2</a></p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao73">iOS开发优化篇之卡顿检测</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on March 27, 2017.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao72
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao72
2017-03-09T00:00:00+08:00
2017-03-09T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<h2 id="前言">前言:</h2>
<p>本文承接自上篇:<a href="http://allluckly.cn/投稿/tougao71">iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)</a></p>
<p>注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。这里有楼主标注好注释的源码,有需要的可以作为参照:CocoaAsyncSocket源码注释
如果对该框架用法不熟悉的话,可以参考楼主之前这篇文章:iOS即时通讯,从入门到“放弃”?,或者自行查阅。</p>
<p>上文我们提到了GCDAsyncSocket的初始化,以及最终connect之前的准备工作,包括一些错误检查;本机地址创建以及socket创建;服务端地址的创建;还有一些本机socket可选项的配置,例如禁止网络出错导致进程关闭的信号等。</p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/22c984eac9b9">涂耀辉</a><br /></p>
<h2 id="言归正传继续上文往下讲">言归正传,继续上文往下讲</h2>
<p>上文讲到了本文方法八–创建Socket,其中有这么一行代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//和connectInterface绑定
if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr])
{
//绑定失败,直接关闭返回
[self closeSocket:socketFD];
return SOCKET_NULL;
}
</code></pre></div></div>
<p>我们去用之前创建的本机地址去做socket绑定,接着会调用到如下方法中:</p>
<p>本文方法九–给Socket绑定本机地址</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//绑定一个Socket的本地地址
- (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr
{
// Bind the socket to the desired interface (if needed)
//无接口就不绑定,connect会自动绑定到一个不冲突的端口上去。
if (connectInterface)
{
LogVerbose(@"Binding socket...");
//判断当前地址的Port是不是大于0
if ([[self class] portFromAddress:connectInterface] > 0)
{
// Since we're going to be binding to a specific port,
// we should turn on reuseaddr to allow us to override sockets in time_wait.
int reuseOn = 1;
//设置调用close(socket)后,仍可继续重用该socket。调用close(socket)一般不会立即关闭socket,而经历TIME_WAIT的过程。
setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
}
//拿到地址
const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes];
//绑定这个地址
int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);
//绑定出错,返回NO
if (result != 0)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in bind() function"];
return NO;
}
}
//成功
return YES;
}
</code></pre></div></div>
<p>这个方法也非常简单,如果没有connectInterface则直接返回YES,当socket进行连接的时候,会自动绑定一个端口,进行连接。
如果有值,则我们开始绑定到我们一开始指定的地址上。
这里调用了两个和scoket相关的函数:
第一个是我们之前提到的配置scoket参数的函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
</code></pre></div></div>
<p>这里调用这个函数的主要目的是为了调用close的时候,不立即去关闭socket连接,而是经历一个TIME_WAIT过程。在这个过程中,socket是可以被复用的。我们注意到之前的connect流程并没有看到复用socket的代码。注意,我们现在走的连接流程是客户端的流程,等我们讲到服务端accept进行连接的时候,我们就能看到这个复用的作用了。</p>
<p>第二个是bind函数</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);
</code></pre></div></div>
<p>这个函数倒是很简单,就3个参数,socket、需要绑定的地址、地址大小。这样就把socket和这个地址(其实就是端口)捆绑在一起了。</p>
<p>这样我们就做完了最终连接前所有准备工作,本机socket有了,服务端的地址也有了。接着我们就可以开始进行最终连接了:</p>
<p>本文方法十 – 建立连接的最终方法</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//连接最终方法 3 finnal。。。
- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex
{
// If there already is a socket connected, we close socketFD and return
//已连接,关闭连接返回
if (self.isConnected)
{
[self closeSocket:socketFD];
return;
}
// Start the connection process in a background queue
//开始连接过程,在后台queue中
__weak GCDAsyncSocket *weakSelf = self;
//获取到全局Queue
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//新线程
dispatch_async(globalConcurrentQueue, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
//调用connect方法,该函数阻塞线程,所以要异步新线程
//客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
//老样子,安全判断
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
//在socketQueue中,开辟线程
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
//如果状态为已经连接,关闭连接返回
if (strongSelf.isConnected)
{
[strongSelf closeSocket:socketFD];
return_from_block;
}
//说明连接成功
if (result == 0)
{
//关闭掉另一个没用的socket
[self closeUnusedSocket:socketFD];
//调用didConnect,生成stream,改变状态等等!
[strongSelf didConnect:aStateIndex];
}
//连接失败
else
{
//关闭当前socket
[strongSelf closeSocket:socketFD];
// If there are no more sockets trying to connect, we inform the error to the delegate
//返回连接错误的error
if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL)
{
NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"];
[strongSelf didNotConnect:aStateIndex error:error];
}
}
}});
#pragma clang diagnostic pop
});
//输出正在连接中
LogVerbose(@"Connecting...");
}
</code></pre></div></div>
<p>这个方法主要就是做了一件事,调用下面一个函数进行连接:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
</code></pre></div></div>
<p>这里需要注意的是这个函数是阻塞,直到结果返回之前,线程会一直停在这行。所以这里用的是全局并发队列,开辟了一个新的线程进行连接,在得到结果之后,又调回socketQueue中进行后续操作。</p>
<p>如果result为0,说明连接成功,我们会关闭掉另外一个没有用到的socket(如果有的话)。然后调用另外一个方法做一些连接成功的初始化操作。
否则连接失败,我们会关闭socket,填充错误并且返回。</p>
<p>我们接着来看看连接成功后,初始化的方法:</p>
<p>本文方法十一 – 连接成功后的初始化</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//连接成功后调用,设置一些连接成功的状态
- (void)didConnect:(int)aStateIndex
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//状态不同
if (aStateIndex != stateIndex)
{
LogInfo(@"Ignoring didConnect, already disconnected");
// The connect operation has been cancelled.
// That is, socket was disconnected, or connection has already timed out.
return;
}
//kConnected合并到当前flag中
flags |= kConnected;
//停止连接超时
[self endConnectTimeout];
#if TARGET_OS_IPHONE
// The endConnectTimeout method executed above incremented the stateIndex.
//上面的endConnectTimeout,会导致stateIndex增加,所以需要重新赋值
aStateIndex = stateIndex;
#endif
// Setup read/write streams (as workaround for specific shortcomings in the iOS platform)
//
// Note:
// There may be configuration options that must be set by the delegate before opening the streams.
//打开stream之前必须用相关配置设置代理
// The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream.
//主要的例子是kCFStreamNetworkServiceTypeVoIP标记,只能工作在未打开的stream中?
//
// Thus we wait until after the socket:didConnectToHost:port: delegate method has completed.
//所以我们要等待,连接完成的代理调用完
// This gives the delegate time to properly configure the streams if needed.
//这些给了代理时间,去正确的配置Stream,如果是必要的话
//创建个Block来初始化Stream
dispatch_block_t SetupStreamsPart1 = ^{
NSLog(@"hello~");
#if TARGET_OS_IPHONE
//创建读写stream失败,则关闭并报对应错误
if (![self createReadAndWriteStream])
{
[self closeWithError:[self otherError:@"Error creating CFStreams"]];
return;
}
//参数是给NO的,就是有可读bytes的时候,不会调用回调函数
if (![self registerForStreamCallbacksIncludingReadWrite:NO])
{
[self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
return;
}
#endif
};
//part2设置stream
dispatch_block_t SetupStreamsPart2 = ^{
#if TARGET_OS_IPHONE
//状态不一样直接返回
if (aStateIndex != stateIndex)
{
// The socket has been disconnected.
return;
}
//如果加到runloop上失败
if (![self addStreamsToRunLoop])
{
//错误返回
[self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
return;
}
//读写stream open
if (![self openStreams])
{
//开启错误返回
[self closeWithError:[self otherError:@"Error creating CFStreams"]];
return;
}
#endif
};
// Notify delegate
//通知代理
//拿到server端的host port
NSString *host = [self connectedHost];
uint16_t port = [self connectedPort];
//拿到unix域的 url
NSURL *url = [self connectedUrl];
//拿到代理
__strong id theDelegate = delegate;
//代理队列 和 Host不为nil 且响应didConnectToHost代理方法
if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)])
{
//调用初始化stream1
SetupStreamsPart1();
dispatch_async(delegateQueue, ^{ @autoreleasepool {
//到代理队列调用连接成功的代理方法
[theDelegate socket:self didConnectToHost:host port:port];
//然后回到socketQueue中去执行初始化stream2
dispatch_async(socketQueue, ^{ @autoreleasepool {
SetupStreamsPart2();
}});
}});
}
//这个是unix domain 请求回调
else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)])
{
SetupStreamsPart1();
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socket:self didConnectToUrl:url];
dispatch_async(socketQueue, ^{ @autoreleasepool {
SetupStreamsPart2();
}});
}});
}
//否则只初始化stream
else
{
SetupStreamsPart1();
SetupStreamsPart2();
}
// Get the connected socket
int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
//fcntl,功能描述:根据文件描述词来操作文件的特性。http://blog.csdn.net/pbymw8iwm/article/details/7974789
// Enable non-blocking IO on the socket
//使socket支持非阻塞IO
int result = fcntl(socketFD, F_SETFL, O_NONBLOCK);
if (result == -1)
{
//失败 ,报错
NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)";
[self closeWithError:[self otherError:errMsg]];
return;
}
// Setup our read/write sources
//初始化读写source
[self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD];
// Dequeue any pending read/write requests
//开始下一个任务
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
</code></pre></div></div>
<p>这个方法很长一大串,其实做的东西也很简单,主要做了下面几件事:</p>
<p>把当前状态flags加上已连接,并且关闭掉我们一开始连接开启的,连接超时的定时器。</p>
<p>初始化了两个Block:SetupStreamsPart1、SetupStreamsPart2,这两个Block做的事都和读写流有关。SetupStreamsPart1用来创建读写流,并且注册回调。另一个SetupStreamsPart2用来把流添加到当前线程的runloop上,并且打开流。</p>
<p>判断是否有代理queue、host或者url这些参数是否为空、是否代理响应didConnectToHost或didConnectToUrl代理,这两种分别对应了普通socket连接和unix domin socket连接。如果实现了对应的代理,则调用连接成功的代理。</p>
<p>在调用代理的同时,调用了我们之前初始化的两个读写流相关的Block。这里值得说下的是这两个Block和代理之间的调用顺序:</p>
<ul>
<li>先执行SetupStreamsPart1后执行SetupStreamsPart2,没什么好说的,问题是代理的执行时间,想想如果我们放在SetupStreamsPart2后面是不是会导致个问题,就是用户收到消息了,但是连接成功的代理还没有被调用,这显然是不合理的。所以我们的调用顺序是SetupStreamsPart1->代理->SetupStreamsPart2</li>
</ul>
<p>所以出现了如下代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//调用初始化stream1
SetupStreamsPart1();
dispatch_async(delegateQueue, ^{ @autoreleasepool {
//到代理队列调用连接成功的代理方法
[theDelegate socket:self didConnectToHost:host port:port];
//然后回到socketQueue中去执行初始化stream2
dispatch_async(socketQueue, ^{ @autoreleasepool {
SetupStreamsPart2();
}});
}});
</code></pre></div></div>
<p>原因是为了线程安全和socket相关的操作必须在socketQueue中进行。而代理必须在我们设置的代理queue中被回调。</p>
<p>拿到当前的本机socket,调用如下函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int result = fcntl(socketFD, F_SETFL, O_NONBLOCK);
</code></pre></div></div>
<p>简单来说,这个函数类似我们之前提到的一个函数setsockopt(),都是给socket设置一些参数,以实现一些功能。而这个函数,能实现的功能更多。大家可以看看这篇文章参考参考:fcntl函数详解</p>
<p>而在这里,就是为了把socket的IO模式设置为非阻塞。很多小伙伴又要疑惑什么是非阻塞了,先别急,关于这个我们下文会详细的来谈。</p>
<p>我们初始化了读写source(很重要,所有的消息都是由这个source来触发的,我们之后会详细分析这个方法)。</p>
<p>我们做完了stream和source的初始化处理,则开始做一次读写任务(这两个方法暂时不讲,会放到之后的Read和Write篇中去讲)。</p>
<p>我们接着来讲讲这个方法中对其他方法的调用,按照顺序来,先从第2条,两个Block中对stream的处理开始。和stream相关的函数一共有6个:</p>
<p>Stream相关方法一 – 创建读写stream</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//创建读写stream
- (BOOL)createReadAndWriteStream
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//如果有一个有值,就返回
if (readStream || writeStream)
{
// Streams already created
return YES;
}
//拿到socket,首选是socket4FD,其次socket6FD,都没有才是socketUN,socketUN应该是Unix的socket结构体
int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
//如果都为空,返回NO
if (socketFD == SOCKET_NULL)
{
// Cannot create streams without a file descriptor
return NO;
}
//如果非连接,返回NO
if (![self isConnected])
{
// Cannot create streams until file descriptor is connected
return NO;
}
LogVerbose(@"Creating read and write stream...");
#pragma mark - 绑定Socket和CFStream
//下面的接口用于创建一对 socket stream,一个用于读取,一个用于写入:
CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream);
// The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case).
// But let's not take any chances.
//读写stream都设置成不会随着绑定的socket一起close,release。 kCFBooleanFalse不一起,kCFBooleanTrue一起
if (readStream)
CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
if (writeStream)
CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
//如果有一个为空
if ((readStream == NULL) || (writeStream == NULL))
{
LogWarn(@"Unable to create read and write stream...");
//关闭对应的stream
if (readStream)
{
CFReadStreamClose(readStream);
CFRelease(readStream);
readStream = NULL;
}
if (writeStream)
{
CFWriteStreamClose(writeStream);
CFRelease(writeStream);
writeStream = NULL;
}
//返回创建失败
return NO;
}
//创建成功
return YES;
}
</code></pre></div></div>
<p>这个方法基本上很简单,就是关于两个stream函数的调用:</p>
<p>创建stream的函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream);
</code></pre></div></div>
<p>这个函数创建了一对读写stream,并且把stream与这个scoket做了绑定。</p>
<p>设置stream属性:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
</code></pre></div></div>
<p>这个函数可以给stream设置一个属性,这里是设置stream不会随着socket的生命周期(close,release)而变化。</p>
<p>接着调用了registerForStreamCallbacksIncludingReadWrite来给stream注册读写回调。</p>
<p>Stream相关方法二 – 读写回调的注册:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//注册Stream的回调
- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite
{
LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO"));
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//判断读写stream是不是都为空
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
//客户端stream上下文对象
streamContext.version = 0;
streamContext.info = (__bridge void *)(self);
streamContext.retain = nil;
streamContext.release = nil;
streamContext.copyDescription = nil;
// The open has completed successfully.
// The stream has bytes to be read.
// The stream can accept bytes for writing.
// An error has occurred on the stream.
// The end of the stream has been reached.
//设置一个CF的flag 两种,一种是错误发生的时候,一种是stream事件结束
CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered ;
//如果包含读写
if (includeReadWrite)
//仍然有Bytes要读的时候 The stream has bytes to be read.
readStreamEvents |= kCFStreamEventHasBytesAvailable;
//给读stream设置客户端,会在之前设置的那些标记下回调函数 CFReadStreamCallback。设置失败的话直接返回NO
if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext))
{
return NO;
}
//写的flag,也一样
CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
if (includeReadWrite)
writeStreamEvents |= kCFStreamEventCanAcceptBytes;
if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext))
{
return NO;
}
//走到最后说明读写都设置回调成功,返回YES
return YES;
}
</code></pre></div></div>
<p>相信用过CFStream的朋友,应该会觉得很简单,这个方法就是调用了一些CFStream相关函数,其中最主要的这个设置读写回调函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Boolean CFReadStreamSetClient(CFReadStreamRef stream, CFOptionFlags streamEvents, CFReadStreamClientCallBack clientCB, CFStreamClientContext *clientContext);
Boolean CFWriteStreamSetClient(CFWriteStreamRef stream, CFOptionFlags streamEvents, CFWriteStreamClientCallBack clientCB, CFStreamClientContext *clientContext);
</code></pre></div></div>
<p>这个函数共4个参数:
第1个为我们需要设置的stream;
第2个为需要监听的事件选项,包括以下事件:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef CF_OPTIONS(CFOptionFlags, CFStreamEventType) {
kCFStreamEventNone = 0, //没有事件发生
kCFStreamEventOpenCompleted = 1, //成功打开流
kCFStreamEventHasBytesAvailable = 2, //流中有数据可读
kCFStreamEventCanAcceptBytes = 4, //流中可以接受数据去写
kCFStreamEventErrorOccurred = 8, //流发生错误
kCFStreamEventEndEncountered = 16 //到达流的结尾
};
</code></pre></div></div>
<p>其中具体用法,大家可以自行去试试,这里作者只监听了了两种事件kCFStreamEventErrorOccurred和kCFStreamEventEndEncountered,再根据传过来的参数去决定是否监听kCFStreamEventCanAcceptBytes:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//如果包含读写
if (includeReadWrite)
//仍然有Bytes要读的时候 The stream has bytes to be read.
readStreamEvents |= kCFStreamEventHasBytesAvailable;
</code></pre></div></div>
<p>而这里我们传过来的参数为NO,导致它并不监听可读数据。显然,我们正常的连接,当有消息发送过来,并不是由stream回调来触发的。这个框架中,如果是TLS传输的socket是用stream来触发的,这个我们后续文章会讲到。</p>
<p>那么有数据的时候,到底是什么来触发我们的读写呢,答案就是读写source,我们接下来就会去创建初始化它。</p>
<p>这里绑定了两个函数,分别对应读和写的回调,分别为:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//读的回调
static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
//写的回调
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
</code></pre></div></div>
<p>关于这两个函数,同样这里暂时不做讨论,等后续文章再来分析。</p>
<p>还有一点需要说一下的是streamContext这个属性,它是一个结构体,包含流的上下文信息,其结构如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef struct {
CFIndex version;
void *info;
void *(*retain)(void *info);
void (*release)(void *info);
CFStringRef (*copyDescription)(void *info);
} CFStreamClientContext;
</code></pre></div></div>
<p>这个流的上下文中info指针,其实就是前面所对应的读写回调函数中的pInfo指针,每次回调都会传过去。其它的version就是流的版本标识,之外的3个都需要的是一个函数指针,对应我们传递的pInfo的持有以及释放还有复制的描述信息,这里我们都赋值给nil。</p>
<p>接着我们来到流处理的第三步:addStreamsToRunLoop-添加到runloop上。</p>
<p>Stream相关方法三 – 加到当前线程的runloop上:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//把stream添加到runloop上
- (BOOL)addStreamsToRunLoop
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
//判断flag里是否包含kAddedStreamsToRunLoop,没添加过则添加。
if (!(flags & kAddedStreamsToRunLoop))
{
LogVerbose(@"Adding streams to runloop...");
[[self class] startCFStreamThreadIfNeeded];
//在开启的线程中去执行,阻塞式的
[[self class] performSelector:@selector(scheduleCFStreams:)
onThread:cfstreamThread
withObject:self
waitUntilDone:YES];
//添加标识
flags |= kAddedStreamsToRunLoop;
}
return YES;
}
</code></pre></div></div>
<p>这里方法做了两件事:</p>
<p>开启了一条用于CFStream读写回调的常驻线程,其中调用了好几个函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+ (void)startCFStreamThreadIfNeeded;
+ (void)cfstreamThread;
</code></pre></div></div>
<p>在这两个函数中,添加了一个runloop,并且绑定了一个定时器事件,让它run起来,使得线程常驻。大家可以结合着github中demo的注释,自行查看这几个方法。如果有任何疑问可以看看楼主这篇文章:基于runloop的线程保活、销毁与通信,或者本文下评论,会一一解答。</p>
<p>在这个常驻线程中去调用注册方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//注册CFStream
+ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
//断言当前线程是cfstreamThread,不是则报错
NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");
//获取到runloop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
//如果有readStream
if (asyncSocket->readStream)
//注册readStream在runloop的kCFRunLoopDefaultMode上
CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
//一样
if (asyncSocket->writeStream)
CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
}
</code></pre></div></div>
<p>这里可以看到,我们流的回调都是在这条流的常驻线程中,至于为什么要这么做,相信大家楼主看过AFNetworking系列文章的会明白。我们之后文章也会就这个框架线程的问题详细讨论的,这里就暂时不详细说明了。
这里主要用了CFReadStreamScheduleWithRunLoop函数完成了runloop的注册:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
</code></pre></div></div>
<p>这样,如果stream中有我们监听的事件发生了,就会在这个runloop中触发我们之前设置的读写回调函数。</p>
<p>我们完成了注册,接下来我们就需要打开stream了:</p>
<p>Stream相关方法四 – 打开stream:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//打开stream
- (BOOL)openStreams
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//断言读写stream都不会空
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
//返回stream的状态
CFStreamStatus readStatus = CFReadStreamGetStatus(readStream);
CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream);
//如果有任意一个没有开启
if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen))
{
LogVerbose(@"Opening read and write stream...");
//开启
BOOL r1 = CFReadStreamOpen(readStream);
BOOL r2 = CFWriteStreamOpen(writeStream);
//有一个开启失败
if (!r1 || !r2)
{
LogError(@"Error in CFStreamOpen");
return NO;
}
}
return YES;
}
</code></pre></div></div>
<p>方法也很简单,通过CFReadStreamGetStatus函数,获取到当前stream的状态,判断没开启则调用CFReadStreamOpen函数去开启,如果开启失败,错误返回。</p>
<p>到这里stream初始化相关的工作就做完了,接着我们还是回到本文方法十一 – 连接成功后的初始化中:</p>
<p>其中第5条,我们谈到了设置socket的I/O模式为非阻塞,相信很多朋友对socket的I/O:同步、异步、阻塞、非阻塞。这四个概念有所混淆。
简单的来说,同步、异步是对于客户端而言的。比如我发起一个调用一个函数,我如果直接去调用,那么就是同步的,否则新开辟一个线程去做,那么对于当前线程而言就是异步的。
而阻塞和非阻塞是对于服务端而言。当服务端被客户端调用后,我如果立刻返回调用的结果(无论数据是否处理完)那么就是非阻塞的,又或者等待数据拿到并且处理完(总之一系列逻辑)再返回,那么这种情况就是阻塞的。</p>
<p>好了,有了这个概念,我们接下来看看Linux下的5种I/O模型:</p>
<p>1)阻塞I/O(blocking I/O)</p>
<p>2)非阻塞I/O (nonblocking I/O)</p>
<p>3) I/O复用(select 和poll) (I/O multiplexing)</p>
<p>4)信号驱动I/O (signal driven I/O (SIGIO))</p>
<p>5)异步I/O (asynchronous I/O (the POSIX aio_functions))</p>
<p>我们来简单谈谈这5种模型:</p>
<p>1)阻塞I/O:
简单举个例子,比如我们调用read()去读取消息,如果是在阻塞模式下,我们会一直等待,直到有消息到来为止。
很多小伙伴可能又要说了,这有什么不可以,我们新开辟一条线程,让它等着不就行了,看起来确实没什么不可以。
那是因为你仅仅是站在客户端的角度上来看。试想如果我们服务端也这么做,那岂不是有多少个socket连接,我们得开辟多少个线程去做阻塞IO?
2)非阻塞I/O
于是就有了非阻塞的概念,当我们去read()的时候,直接返回结果,这样在很大概率下,是并没有消息给我们读的。这时候函数就会错误返回-1,并将errno设置为 EWOULDBLOCK,意为IO并没有数据。
这时候就需要我们自己有一个机制,能知道什么时候有数据,在去调用read()。有一个很傻的方式就是不停的循环去调用这个函数,这样有数据来,我们第一时间就读到了。
3)I/O复用模式
I/O复用模式是阻塞I/O的改进版,它在read之前,会先去调用select去遍历所有的socket,看哪一个有消息。当然这个过程是阻塞的,直到有消息返回为止。然后在去调用read,阻塞的方式去读取从系统内核中去读取这条消息到进程中来。
4)信号驱动I/O
信号驱动I/O是一个半异步的I/O模式,它首先会调用一个系统sginal相关的函数,把socket和信号绑定起来,然后不管有没有消息直接返回(这一步非阻塞)。这时候系统内核会去检查socket是否有可用数据。有的话则发送该信号给进程,然后进程在去调用read阻塞式的从系统内核读取数据到进程中来(这一步阻塞)。
5)可能聪明的你已经想到了更好的解决方式,这就对了,这就是我们第5种IO模式:异步I/O ,它和第4步一样,也是调用sginal相关函数,把socket和信号绑定起来,同时绑定起来的还有一块数据缓冲区buffer。然后无论有没有数据直接返回(非阻塞)。而系统内核会去检查是否有可用数据,一旦有可用数据,则触发信号,并且把数据填充到我们之前提供的数据缓冲区buffer中。这样我们进程被信号触发,并且直接能从buffer中读取到数据,整个过程没有任何阻塞。
很显然,我们CocoaAyncSocket框架用的就是第5种I/O模式。</p>
<p>如果大家对I/O模式仍然感到疑惑,可以看看这篇文章:
<a href="http://blog.csdn.net/hguisu/article/details/7453390">socket阻塞与非阻塞,同步与异步、I/O模型</a></p>
<p>接着我们继续看本文方法十一 – 连接成功后的初始化中第6条,读写source的初始化方法:</p>
<p>本文方法十二 – 初始化读写source:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//初始化读写source
- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD
{
//GCD source DISPATCH_SOURCE_TYPE_READ 会一直监视着 socketFD,直到有数据可读
readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue);
//_dispatch_source_type_write :监视着 socketFD,直到写数据了
writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue);
// Setup event handlers
__weak GCDAsyncSocket *weakSelf = self;
#pragma mark readSource的回调
//GCD事件句柄 读,当socket中有数据流出现,就会触发这个句柄,全自动,不需要手动触发
dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
LogVerbose(@"readEventBlock");
//从readSource中,获取到数据长度,
strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource);
LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable);
//如果长度大于0,开始读数据
if (strongSelf->socketFDBytesAvailable > 0)
[strongSelf doReadData];
else
//因为触发了,但是却没有可读数据,说明读到当前包边界了。做边界处理
[strongSelf doReadEOF];
#pragma clang diagnostic pop
}});
//写事件句柄
dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
LogVerbose(@"writeEventBlock");
//标记为接受数据
strongSelf->flags |= kSocketCanAcceptBytes;
//开始写
[strongSelf doWriteData];
#pragma clang diagnostic pop
}});
// Setup cancel handlers
__block int socketFDRefCount = 2;
#if !OS_OBJECT_USE_OBJC
dispatch_source_t theReadSource = readSource;
dispatch_source_t theWriteSource = writeSource;
#endif
//读写取消的句柄
dispatch_source_set_cancel_handler(readSource, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
LogVerbose(@"readCancelBlock");
#if !OS_OBJECT_USE_OBJC
LogVerbose(@"dispatch_release(readSource)");
dispatch_release(theReadSource);
#endif
if (--socketFDRefCount == 0)
{
LogVerbose(@"close(socketFD)");
//关闭socket
close(socketFD);
}
#pragma clang diagnostic pop
});
dispatch_source_set_cancel_handler(writeSource, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
LogVerbose(@"writeCancelBlock");
#if !OS_OBJECT_USE_OBJC
LogVerbose(@"dispatch_release(writeSource)");
dispatch_release(theWriteSource);
#endif
if (--socketFDRefCount == 0)
{
LogVerbose(@"close(socketFD)");
//关闭socket
close(socketFD);
}
#pragma clang diagnostic pop
});
// We will not be able to read until data arrives.
// But we should be able to write immediately.
//设置未读数量为0
socketFDBytesAvailable = 0;
//把读挂起的状态移除
flags &= ~kReadSourceSuspended;
LogVerbose(@"dispatch_resume(readSource)");
//开启读source
dispatch_resume(readSource);
//标记为当前可接受数据
flags |= kSocketCanAcceptBytes;
//先把写source标记为挂起
flags |= kWriteSourceSuspended;
}
这个方法初始化了读写source,这个方法主要是GCD source运用,如果有对这部分知识有所疑问,可以看看宜龙大神这篇:GCD高级用法。
这里GCD Source相关的主要是下面这3个函数:
//创建source
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t _Nullable queue);
//为source设置事件句柄
dispatch_source_set_event_handler(dispatch_source_t source,
dispatch_block_t _Nullable handler);
//为source设置取消句柄
dispatch_source_set_cancel_handler(dispatch_source_t source,
dispatch_block_t _Nullable handler);
</code></pre></div></div>
<p>相信大家用至少用过GCD定时器,接触过这3个函数,这里创建source的函数,根据参数type的不同,可以处理不同的事件:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao72/1.png" alt="1" /><br /></p>
<p>这里我们用的是DISPATCH_SOURCE_TYPE_READ和DISPATCH_SOURCE_TYPE_WRITE这两个类型。标识如果handle如果有可读或者可写数据时,会触发我们的事件句柄。</p>
<ul>
<li>
<p>而这里初始化的读写事件句柄内容也很简单,就是去读写数据。</p>
</li>
<li>
<p>而取消句柄也就是去关闭socket。</p>
</li>
<li>
<p>初始化完成后,我们开启了readSource,一旦有数据过来就触发了我们readSource事件句柄,就可以去监听的socket所分配的缓冲区中去读取数据了,而wirteSource初始化完是挂起的。</p>
</li>
<li>
<p>除此之外我们还初始化了当前source的状态,用于我们后续的操作。</p>
</li>
</ul>
<p>至此我们客户端的整个Connect流程结束了,用一张图来概括总结一下吧:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao72/2.png" alt="1" /><br /></p>
<p>整个客户端连接的流程大致如上图,当然远不及于此,这里我们对地址做了IPV4和IPV6的兼容处理,对一些使用socket而产生的网络错误导致进程退出的容错处理。以及在这个过程中,socketQueue、代理queue、全局并发queue和stream常驻线程的管理调度等等。</p>
<p>当然其中绝大部分操作都是在socketQueue中进行的。而在socketQueue中,我们也分为两种操作dispatch_sync和dispatch_async。
因为socketQueue本身就是一个串行queue,所以我们所有的操作都在这个queue中进行保证了线程安全,而需要阻塞后续行为的操作,我们用了sync的方式。其实这样使用sync是及其容易死锁的,但是作者每次在调用sync之前都调用了这么一行判断:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
</code></pre></div></div>
<p>判断当前队列是否就是这个socketQueue队列,如果是则直接调用,否则就用sync的方式提交到这个queue中去执行。这种防死锁的方式,你学到了么?</p>
<p>接着我们来讲讲服务端Accept流程:</p>
<p>整个流程还是相对Connect来说还是十分简单的,因为这个方法很长,而且大多数是我们直接连接讲到过得内容,所以我省略了一部分的代码,只把重要的展示出来,大家可以参照着源码看。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//监听端口起点
- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr
{
return [self acceptOnInterface:nil port:port error:errPtr];
}
- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr
{
LogTrace();
// Just in-case interface parameter is immutable.
//防止参数被修改
NSString *interface = [inInterface copy];
__block BOOL result = NO;
__block NSError *err = nil;
// CreateSocket Block
// This block will be invoked within the dispatch block below.
//创建socket的Block
int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {
//创建TCP的socket
int socketFD = socket(domain, SOCK_STREAM, 0);
//一系列错误判断
...
// Bind socket
//用本地地址去绑定
status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]);
//监听这个socket
//第二个参数是这个端口下维护的socket请求队列,最多容纳的用户请求数。
status = listen(socketFD, 1024);
return socketFD;
};
// Create dispatch block and run on socketQueue
dispatch_block_t block = ^{ @autoreleasepool {
//一系列错误判断
...
//判断ipv4 ipv6是否支持
...
//得到本机的IPV4 IPV6的地址
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port];
...
//判断可以用IPV4还是6进行请求
...
// Create accept sources
//创建接受连接被触发的source
if (enableIPv4)
{
//接受连接的source
accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
//事件句柄
dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {
//拿到数据,连接数
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
//循环去接受这些socket的事件(一次触发可能有多个连接)
while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections));
}});
//取消句柄
dispatch_source_set_cancel_handler(accept4Source, ^{
//...
//关闭socket
close(socketFD);
});
//开启source
dispatch_resume(accept4Source);
}
//ipv6一样
...
//在scoketQueue中同步做这些初始化。
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
//...错误判断
//返回结果
return result;
}
</code></pre></div></div>
<p>这个方法省略完仍然有这么长,它主要做了这两件事(篇幅原因,尽量精简):</p>
<ul>
<li>
<p>创建本机地址、创建socket、绑定端口、监听端口。</p>
</li>
<li>
<p>创建了一个GCD Source,来监听这个socket读source,这样连接事件一发生,就会触发我们的事件句柄。接着我们调用了doAccept:方法循环去接受所有的连接。</p>
</li>
</ul>
<p>接着我们来看这个接受连接的方法(同样省略了一部分不那么重要的代码):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//连接接受的方法
- (BOOL)doAccept:(int)parentSocketFD
{
LogTrace();
int socketType;
int childSocketFD;
NSData *childSocketAddress;
//IPV4
if (parentSocketFD == socket4FD)
{
socketType = 0;
struct sockaddr_in addr;
socklen_t addrLen = sizeof(addr);
//调用接受,得到接受的子socket
childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
//NO说明没有连接
if (childSocketFD == -1)
{
LogWarn(@"Accept failed with error: %@", [self errnoError]);
return NO;
}
//子socket的地址数据
childSocketAddress = [NSData dataWithBytes:&addr length:addrLen];
}
//一样
else if (parentSocketFD == socket6FD)
{
...
}
//unix domin socket 一样
else // if (parentSocketFD == socketUN)
{
...
}
//socket 配置项的设置... 和connect一样
//响应代理
if (delegateQueue)
{
__strong id theDelegate = delegate;
//代理队列中调用
dispatch_async(delegateQueue, ^{ @autoreleasepool {
// Query delegate for custom socket queue
dispatch_queue_t childSocketQueue = NULL;
//判断是否实现了为socket 生成一个新的SocketQueue,是的话拿到新queue
if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)])
{
childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress
onSocket:self];
}
// Create GCDAsyncSocket instance for accepted socket
//新创建一个本类实例,给接受的socket
GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate
delegateQueue:delegateQueue
socketQueue:childSocketQueue];
//IPV4 6 un
if (socketType == 0)
acceptedSocket->socket4FD = childSocketFD;
else if (socketType == 1)
acceptedSocket->socket6FD = childSocketFD;
else
acceptedSocket->socketUN = childSocketFD;
//标记开始 并且已经连接
acceptedSocket->flags = (kSocketStarted | kConnected);
// Setup read and write sources for accepted socket
//初始化读写source
dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool {
[acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD];
}});
//判断代理是否实现了didAcceptNewSocket方法,把我们新创建的socket返回出去
if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)])
{
[theDelegate socket:self didAcceptNewSocket:acceptedSocket];
}
}});
}
return YES;
}
</code></pre></div></div>
<p>这个方法很简单,核心就是调用下面这个函数,去接受连接,并且拿到一个新的socket</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
</code></pre></div></div>
<p>然后调用了newSocketQueueForConnectionFromAddress:onSocket:这个代理,可以为新的socket重新设置一个socketQueue。
接着我们用这个Socket重新创建了一个GCDAsyncSocket实例,然后调用我们的代理didAcceptNewSocket方法,把这个实例给传出去了。
这里需要注意的是,我们调用didAcceptNewSocket代理方法传出去的实例我们需要自己保留,不然就会被释放掉,那么这个与客户端的连接也就断开了。
同时我们还初始化了这个新socket的读写source,这一步完全和connect中一样,调用同一个方法,这样如果有读写数据,就会触发这个新的socket的source了。
建立连接之后的无数个新的socket,都是独立的,它们处理读写连接断开的逻辑就和客户端socket完全一样了。
而我们监听本机端口的那个socket始终只有一个,这个用来监听触发socket连接,并返回创建我们这无数个新的socket实例。</p>
<p>作为服务端的Accept流程就这么结束了,因为篇幅原因,所以尽量精简了一些细节的处理,不过这些处理在Connect中也是反复出现的,所以基本无伤大雅。如果大家会感到困惑,建议下载github中的源码注释,对照着再看一遍,相信会有帮助的。</p>
<p>接着我们来讲讲Unix Domin Socket建立本地进程通信流程:</p>
<p>基本上这个流程,比上述任何流程还要简单,简单的到即使不简化代码,也没多少行(当然这是建立在客户端Connect流程已经实现了很多公用方法的基础上)。</p>
<p>接着进入正题,我们来看看它发起连接的方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//连接本机的url上,IPC,进程间通信
- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;
{
LogTrace();
__block BOOL result = NO;
__block NSError *err = nil;
dispatch_block_t block = ^{ @autoreleasepool {
//判断长度
if ([url.path length] == 0)
{
NSString *msg = @"Invalid unix domain socket url.";
err = [self badParamError:msg];
return_from_block;
}
// Run through standard pre-connect checks
//前置的检查
if (![self preConnectWithUrl:url error:&err])
{
return_from_block;
}
// We've made it past all the checks.
// It's time to start the connection process.
flags |= kSocketStarted;
// Start the normal connection process
NSError *connectError = nil;
//调用另一个方法去连接
if (![self connectWithAddressUN:connectInterfaceUN error:&connectError])
{
[self closeWithError:connectError];
return_from_block;
}
[self startConnectTimeout:timeout];
result = YES;
}};
//在socketQueue中同步执行
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
if (result == NO)
{
if (errPtr)
*errPtr = err;
}
return result;
}
</code></pre></div></div>
<p>连接方法非常简单,就只是做了一些错误的处理,然后调用了其他的方法,包括一个前置检查,这检查中会去判断各种参数是否正常,如果正常会返回YES,并且把url转换成Uinix domin socket地址的结构体,赋值给我们的属性connectInterfaceUN。
接着调用了connectWithAddressUN方法去发起连接。</p>
<p>我们接着来看看这个方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//连接Unix域服务器
- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
// Create the socket
int socketFD;
LogVerbose(@"Creating unix domain socket");
//创建本机socket
socketUN = socket(AF_UNIX, SOCK_STREAM, 0);
socketFD = socketUN;
if (socketFD == SOCKET_NULL)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
return NO;
}
// Bind the socket to the desired interface (if needed)
LogVerbose(@"Binding socket...");
int reuseOn = 1;
//设置可复用
setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
// Prevent SIGPIPE signals
int nosigpipe = 1;
//进程终止错误信号禁止
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
// Start the connection process in a background queue
int aStateIndex = stateIndex;
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalConcurrentQueue, ^{
const struct sockaddr *addr = (const struct sockaddr *)[address bytes];
//并行队列调用连接
int result = connect(socketFD, addr, addr->sa_len);
if (result == 0)
{
dispatch_async(socketQueue, ^{ @autoreleasepool {
//连接成功的一些状态初始化
[self didConnect:aStateIndex];
}});
}
else
{
// 失败的处理
perror("connect");
NSError *error = [self errnoErrorWithReason:@"Error in connect() function"];
dispatch_async(socketQueue, ^{ @autoreleasepool {
[self didNotConnect:aStateIndex error:error];
}});
}
});
LogVerbose(@"Connecting...");
return YES;
}
</code></pre></div></div>
<p>主要部分基本和客户端连接相同,并且简化了很多,调用了这一行完成了连接:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int result = connect(socketFD, addr, addr->sa_len);
</code></pre></div></div>
<p>同样也和客户端一样,在连接成功之后去调用下面这个方法完成了一些资源的初始化:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> [self didConnect:aStateIndex];
</code></pre></div></div>
<p>基本上连接就这么两个方法了(当然我们省略了一些细节),看完客户端的连接之后,到这就变得非常简单了。</p>
<p>接着我们来看看uinix domin socket作为服务端Accept。</p>
<p>这个Accpet,基本和我们普通Socket服务端的Accept相同。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//接受一个Url,uniex domin socket 做为服务端
- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr;
{
LogTrace();
__block BOOL result = NO;
__block NSError *err = nil;
//基本和正常的socket accept一模一样
// CreateSocket Block
// This block will be invoked within the dispatch block below.
//生成一个创建socket的block,创建、绑定、监听
int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {
//creat socket
...
// Set socket options
...
// Bind socket
...
// Listen
...
};
// Create dispatch block and run on socketQueue
//错误判断
dispatch_block_t block = ^{ @autoreleasepool {
//错误判断
...
//判断是否有这个url路径是否正确
...
//调用上面的Block创建socket,并且绑定监听。
...
//创建接受连接的source
acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, 0, socketQueue);
int socketFD = socketUN;
dispatch_source_t acceptSource = acceptUNSource;
//事件句柄,和accpept一样
dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool {
//循环去接受所有的每一个连接
...
}});
//取消句柄
dispatch_source_set_cancel_handler(acceptUNSource, ^{
//关闭socket
close(socketFD);
});
LogVerbose(@"dispatch_resume(accept4Source)");
dispatch_resume(acceptUNSource);
flags |= kSocketStarted;
result = YES;
}};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
//填充错误
if (result == NO)
{
LogInfo(@"Error in accept: %@", err);
if (errPtr)
*errPtr = err;
}
return result;
}
</code></pre></div></div>
<p>因为代码基本雷同,所以我们省略了大部分代码,大家可以参照着之前的讲解或者源码去理解。这里和普通服务端socket唯一的区别就是,这里服务端绑定的地址是unix domin socket类型的地址,它是一个结构体,里面包含的是我们进行进程通信的纽带-一个本机文件路径。
所以这里服务端简单来说就是绑定的这个文件路径,当这个文件路径有数据可读(即有客户端连接到达)的时候,会触发初始化的source事件句柄,我们会去循环的接受所有的连接,并且新生成一个socket实例,这里和普通的socket完全一样。</p>
<p>就这样我们所有的连接方式已经讲完了,后面这两种方式,为了节省篇幅,确实讲的比较粗略,但是核心的部分都有提到。
另外如果你有理解客户端的Connect流程,那么理解起来应该没有什么问题,这两个流程比前者可简化太多了。</p>
<h2 id="写在结尾">写在结尾:</h2>
<p>这个框架的Connect篇到此为止了,其实想一篇结束一块内容的,但是代码量实在太多,如果讲的太粗略,大家也很难去学习到真正的内容。但是楼主也不想写的太长,太琐碎,相信大家都很难看下去,不过万幸能两篇内总结完。
之后的内容,等过完年会继续写。包括read篇和write篇等等,希望这个系列能让大家能对Socket编程有个新的认识和理解。以后也可以自己上手Socket,运用于项目中去。</p>
<p>转眼过年了,回想这一年,许多地方都做的差强人意,希望2017有个更好的愿景吧。
纸上学来终觉浅,绝知此事要躬行。</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao72">iOS即时通讯进阶之CocoaAsyncSocket源码解析(Connect终)</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on March 09, 2017.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao71
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao71
2017-02-17T00:00:00+08:00
2017-02-17T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<h2 id="前言">前言:</h2>
<p>CocoaAsyncSocket是谷歌的开发者,基于BSD-Socket写的一个IM框架,它给Mac和iOS提供了易于使用的、强大的异步套接字库,向上封装出简单易用OC接口。省去了我们面向Socket以及数据流Stream等繁琐复杂的编程。
本文为一个系列,旨在让大家了解CocoaAsyncSocket是如何基于底层进行封装、工作的。</p>
<p>注:文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解。这里有楼主标注好注释的源码,有需要的可以作为参照:CocoaAsyncSocket源码注释</p>
<p>如果对该框架用法不熟悉的话,可以参考楼主之前这篇文章:iOS即时通讯,从入门到“放弃”?,或者自行查阅。</p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/22c984eac9b9">涂耀辉</a><br /></p>
<h2 id="正文">正文:</h2>
<p>首先我们来看看框架的结构图:
<img src="//allluckly.cn/images/blog/tuogao/tougao71/1.jpg" alt="1" /><br /></p>
<p>整个库就这么两个类,一个基于TCP,一个基于UDP。其中基于TCP的GCDAsyncSocket,大概8000多行代码。而GCDAsyncUdpSocket稍微少一点,也有5000多行。
所以单纯从代码量上来看,这个库还是做了很多事的。</p>
<p>顺便提一下,之前这个框架还有一个runloop版的,不过因为功能重叠和其它种种原因,后续版本便废弃了,现在仅有GCD版本。</p>
<p>本系列我们将重点来讲GCDAsyncSocket这个类。</p>
<p>我们先来看看这个类的属性:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@implementation GCDAsyncSocket
{
//flags,当前正在做操作的标识符
uint32_t flags;
uint16_t config;
//代理
__weak id<GCDAsyncSocketDelegate> delegate;
//代理回调的queue
dispatch_queue_t delegateQueue;
//本地IPV4Socket
int socket4FD;
//本地IPV6Socket
int socket6FD;
//unix域的套接字
int socketUN;
//unix域 服务端 url
NSURL *socketUrl;
//状态Index
int stateIndex;
//本机的IPV4地址
NSData * connectInterface4;
//本机的IPV6地址
NSData * connectInterface6;
//本机unix域地址
NSData * connectInterfaceUN;
//这个类的对Socket的操作都在这个queue中,串行
dispatch_queue_t socketQueue;
dispatch_source_t accept4Source;
dispatch_source_t accept6Source;
dispatch_source_t acceptUNSource;
//连接timer,GCD定时器
dispatch_source_t connectTimer;
dispatch_source_t readSource;
dispatch_source_t writeSource;
dispatch_source_t readTimer;
dispatch_source_t writeTimer;
//读写数据包数组 类似queue,最大限制为5个包
NSMutableArray *readQueue;
NSMutableArray *writeQueue;
//当前正在读写数据包
GCDAsyncReadPacket *currentRead;
GCDAsyncWritePacket *currentWrite;
//当前socket未获取完的数据大小
unsigned long socketFDBytesAvailable;
//全局公用的提前缓冲区
GCDAsyncSocketPreBuffer *preBuffer;
#if TARGET_OS_IPHONE
CFStreamClientContext streamContext;
//读的数据流
CFReadStreamRef readStream;
//写的数据流
CFWriteStreamRef writeStream;
#endif
//SSL上下文,用来做SSL认证
SSLContextRef sslContext;
//全局公用的SSL的提前缓冲区
GCDAsyncSocketPreBuffer *sslPreBuffer;
size_t sslWriteCachedLength;
//记录SSL读取数据错误
OSStatus sslErrCode;
//记录SSL握手的错误
OSStatus lastSSLHandshakeError;
//socket队列的标识key
void *IsOnSocketQueueOrTargetQueueKey;
id userData;
//连接备选服务端地址的延时 (另一个IPV4或IPV6)
NSTimeInterval alternateAddressDelay;
}
</code></pre></div></div>
<p>这个里定义了一些属性,可以先简单看看注释,这里我们仅仅先暂时列出来,给大家混个眼熟。
在接下来的代码中,会大量穿插着这些属性的使用。所以大家不用觉得困惑,具体作用,我们后面会一一讲清楚的。</p>
<p>接着我们来看看本文方法一–初始化方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//层级调用
- (id)init
{
return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL];
}
- (id)initWithSocketQueue:(dispatch_queue_t)sq
{
return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq];
}
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq
{
return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL];
}
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
{
if((self = [super init]))
{
delegate = aDelegate;
delegateQueue = dq;
//这个宏是在sdk6.0之后才有的,如果是之前的,则OS_OBJECT_USE_OBJC为0,!0即执行if语句
//对6.0的适配,如果是6.0以下,则去retain release,6.0之后ARC也管理了GCD
#if !OS_OBJECT_USE_OBJC
if (dq) dispatch_retain(dq);
#endif
//创建socket,先都置为 -1
//本机的ipv4
socket4FD = SOCKET_NULL;
//ipv6
socket6FD = SOCKET_NULL;
//应该是UnixSocket
socketUN = SOCKET_NULL;
//url
socketUrl = nil;
//状态
stateIndex = 0;
if (sq)
{
//如果scoketQueue是global的,则报错。断言必须要一个非并行queue。
NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
@"The given socketQueue parameter must not be a concurrent queue.");
NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
@"The given socketQueue parameter must not be a concurrent queue.");
NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
@"The given socketQueue parameter must not be a concurrent queue.");
//拿到scoketQueue
socketQueue = sq;
//iOS6之下retain
#if !OS_OBJECT_USE_OBJC
dispatch_retain(sq);
#endif
}
else
{
//没有的话创建一个, 名字为:GCDAsyncSocket,串行
socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL);
}
// The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter.
// From the documentation:
//
// > Keys are only compared as pointers and are never dereferenced.
// > Thus, you can use a pointer to a static variable for a specific subsystem or
// > any other value that allows you to identify the value uniquely.
//
// We're just going to use the memory address of an ivar.
// Specifically an ivar that is explicitly named for our purpose to make the code more readable.
//
// However, it feels tedious (and less readable) to include the "&" all the time:
// dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey)
//
// So we're going to make it so it doesn't matter if we use the '&' or not,
// by assigning the value of the ivar to the address of the ivar.
// Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey;
//比如原来为 0X123 -> NULL 变成 0X222->0X123->NULL
//自己的指针等于自己原来的指针,成二级指针了 看了注释是为了以后省略&,让代码更可读?
IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
void *nonNullUnusedPointer = (__bridge void *)self;
//dispatch_queue_set_specific给当前队里加一个标识 dispatch_get_specific当前线程取出这个标识,判断是不是在这个队列
//这个key的值其实就是一个一级指针的地址 ,第三个参数把自己传过去了,上下文对象?第4个参数,为销毁的时候用的,可以指定一个函数
dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
//读的数组 限制为5
readQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentRead = nil;
//写的数组,限制5
writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentWrite = nil;
//设置大小为 4kb
preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
#pragma mark alternateAddressDelay??
//交替地址延时?? wtf
alternateAddressDelay = 0.3;
}
return self;
}
</code></pre></div></div>
<p>详细的细节可以看看注释,这里初始化了一些属性:</p>
<p>1.代理、以及代理queue的赋值。</p>
<p>2.本机socket的初始化:包括下面3种</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//本机的ipv4
socket4FD = SOCKET_NULL;
//ipv6
socket6FD = SOCKET_NULL;
//UnixSocket
socketUN = SOCKET_NULL;
</code></pre></div></div>
<p>其中值得一提的是第三种:UnixSocket,这个是用于Unix Domin Socket通信用的。
那么什么是Unix Domain Socket呢?
原来它是在socket的框架上发展出一种IPC(进程间通信)机制,虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC 更有效率 :</p>
<ul>
<li>
<p>不需要经过网络协议栈</p>
</li>
<li>
<p>不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。</p>
</li>
</ul>
<p>基本上它是当今应用于IPC最主流的方式。至于它到底和普通的socket通信实现起来有什么区别,别着急,我们接着往下看。</p>
<p>3.生成了一个socketQueue,这个queue是串行的,接下来我们看代码就会知道它贯穿于这个类的所有地方。所有对socket以及一些内部数据的相关操作,都需要在这个串行queue中进行。这样使得整个类没有加一个锁,就保证了整个类的线程安全。</p>
<p>4.创建了两个读写队列(本质数组),接下来我们所有的读写任务,都会先追加在这个队列最后,然后每次取出队列中最前面的任务,进行处理。</p>
<p>5.创建了一个全局的数据缓冲区:preBuffer,我们所操作的数据,大部分都是要先存入这个preBuffer中,然后再从preBuffer取出进行处理的。</p>
<p>6.初始化了一个交替延时变量:alternateAddressDelay,这个变量先简单的理解下:就是进行另一个服务端地址请求的延时。后面我们一讲到,大家就明白了。</p>
<p>初始化方法就到此为止了。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao71/2.jpg" alt="1" /><br /></p>
<p>接着我们有socket了,我们如果是客户端,就需要去connect服务器。</p>
<p>又或者我们是服务端的话,就需要去bind端口,并且accept,等待客户端的连接。(基本上也没有用iOS来做服务端的吧…)</p>
<p>这里我们先作为客户端来看看connect:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao71/3.png" alt="1" /><br /></p>
<p>其中和connect相关的方法就这么多,我们一般这么来连接到服务端:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[socket connectToHost:Khost onPort:Kport error:nil];
</code></pre></div></div>
<p>也就是我们在截图中选中的方法,那我们就从这个方法作为起点,开始讲起吧。</p>
<h3 id="本文方法二connect总方法">本文方法二–connect总方法</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//逐级调用
- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr
{
return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr];
}
- (BOOL)connectToHost:(NSString *)host
onPort:(uint16_t)port
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr];
}
//多一个inInterface,本机地址
- (BOOL)connectToHost:(NSString *)inHost
onPort:(uint16_t)port
viaInterface:(NSString *)inInterface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
//{} 跟踪当前行为
LogTrace();
// Just in case immutable objects were passed
//拿到host ,copy防止值被修改
NSString *host = [inHost copy];
//interface?接口?
NSString *interface = [inInterface copy];
//声明两个__block的
__block BOOL result = NO;
//error信息
__block NSError *preConnectErr = nil;
//gcdBlock ,都包裹在自动释放池中
dispatch_block_t block = ^{ @autoreleasepool {
// Check for problems with host parameter
if ([host length] == 0)
{
NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
preConnectErr = [self badParamError:msg];
//其实就是return,大牛的代码真是充满逼格
return_from_block;
}
// Run through standard pre-connect checks
//一个前置的检查,如果没通过返回,这个检查里,如果interface有值,则会将本机的IPV4 IPV6的 address设置上。
if (![self preConnectWithInterface:interface error:&preConnectErr])
{
return_from_block;
}
// We've made it past all the checks.
// It's time to start the connection process.
//flags 做或等运算。 flags标识为开始Socket连接
flags |= kSocketStarted;
//又是一个{}? 只是为了标记么?
LogVerbose(@"Dispatching DNS lookup...");
// It's possible that the given host parameter is actually a NSMutableString.
//很可能给我们的服务端的参数是一个可变字符串
// So we want to copy it now, within this block that will be executed synchronously.
//所以我们需要copy,在Block里同步的执行
// This way the asynchronous lookup block below doesn't have to worry about it changing.
//这种基于Block的异步查找,不需要担心它被改变
//copy,防止改变
NSString *hostCpy = [host copy];
//拿到状态
int aStateIndex = stateIndex;
__weak GCDAsyncSocket *weakSelf = self;
//全局Queue
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//异步执行
dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
//忽视循环引用
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
//查找错误
NSError *lookupErr = nil;
//server地址数组(包含IPV4 IPV6的地址 sockaddr_in6、sockaddr_in类型)
NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
//strongSelf
__strong GCDAsyncSocket *strongSelf = weakSelf;
//完整Block安全形态,在加个if
if (strongSelf == nil) return_from_block;
//如果有错
if (lookupErr)
{
//用cocketQueue
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
//一些错误处理,清空一些数据等等
[strongSelf lookup:aStateIndex didFail:lookupErr];
}});
}
//正常
else
{
NSData *address4 = nil;
NSData *address6 = nil;
//遍历地址数组
for (NSData *address in addresses)
{
//判断address4为空,且address为IPV4
if (!address4 && [[self class] isIPv4Address:address])
{
address4 = address;
}
//判断address6为空,且address为IPV6
else if (!address6 && [[self class] isIPv6Address:address])
{
address6 = address;
}
}
//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
}
#pragma clang diagnostic pop
}});
//开启连接超时
[self startConnectTimeout:timeout];
result = YES;
}};
//在socketQueue中执行这个Block
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
//否则同步的调起这个queue去执行
else
dispatch_sync(socketQueue, block);
//如果有错误,赋值错误
if (errPtr) *errPtr = preConnectErr;
//把连接是否成功的result返回
return result;
}
</code></pre></div></div>
<p>这个方法非常长,它主要做了以下几件事:</p>
<ul>
<li>首先我们需要说一下的是,整个类大量的会出现LogTrace()类似这样的宏,我们点进去发现它的本质只是一个{},什么事都没做。</li>
</ul>
<p>原来这些宏是为了追踪当前执行的流程用的,它被定义在一个大的#if #else中:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#ifndef GCDAsyncSocketLoggingEnabled
#define GCDAsyncSocketLoggingEnabled 0
#endif
#if GCDAsyncSocketLoggingEnabled
// Logging Enabled - See log level below
// Logging uses the CocoaLumberjack framework (which is also GCD based).
// https://github.com/robbiehanson/CocoaLumberjack
//
// It allows us to do a lot of logging without significantly slowing down the code.
#import "DDLog.h"
#define LogAsync YES
#define LogContext GCDAsyncSocketLoggingContext
#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)
#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__)
#ifndef GCDAsyncSocketLogLevel
#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE
#endif
// Log levels : off, error, warn, info, verbose
static const int logLevel = GCDAsyncSocketLogLevel;
#else
// Logging Disabled
#define LogError(frmt, ...) {}
#define LogWarn(frmt, ...) {}
#define LogInfo(frmt, ...) {}
#define LogVerbose(frmt, ...) {}
#define LogCError(frmt, ...) {}
#define LogCWarn(frmt, ...) {}
#define LogCInfo(frmt, ...) {}
#define LogCVerbose(frmt, ...) {}
#define LogTrace() {}
#define LogCTrace(frmt, ...) {}
#endif
</code></pre></div></div>
<p>而此时因为GCDAsyncSocketLoggingEnabled默认为0,所以仅仅是一个{}。当标记为1时,这些宏就可以用来输出我们当前的业务流程,极大的方便了我们的调试过程。</p>
<ul>
<li>
<p>接着我们回到正题上,我们定义了一个Block,所有的连接操作都被包裹在这个Block中。我们做了如下判断:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//在socketQueue中执行这个Block
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
//否则同步的调起这个queue去执行
else
dispatch_sync(socketQueue, block);
</code></pre></div> </div>
</li>
</ul>
<p>保证这个连接操作一定是在我们的socketQueue中,而且还是以串行同步的形式去执行,规避了线程安全的问题。</p>
<ul>
<li>
<p>接着把Block中连接过程产生的错误进行赋值,并且把连接的结果返回出去</p>
<p>//如果有错误,赋值错误
if (errPtr) *errPtr = preConnectErr;
//把连接是否成功的result返回
return result;</p>
</li>
</ul>
<p>接着来看这个方法声明的Block内部,也就是进行连接的真正主题操作,这个连接过程将会调用许多函数,一环扣一环,我会尽可能用最清晰、详尽的语言来描述…</p>
<p>1.这个Block首先做了一些错误的判断,并调用了一些错误生成的方法。类似:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if ([host length] == 0)
{
NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
preConnectErr = [self badParamError:msg];
//其实就是return,大牛的代码真是充满逼格
return_from_block;
}
//用该字符串生成一个错误,错误的域名,错误的参数
- (NSError *)badParamError:(NSString *)errMsg
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo];
}
</code></pre></div></div>
<p>2.接着做了一个前置的错误检查:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (![self preConnectWithInterface:interface error:&preConnectErr])
{
return_from_block;
}
</code></pre></div></div>
<p>这个检查方法,如果没通过返回NO。并且如果interface有值,则会将本机的IPV4 IPV6的 address设置上。即我们之前提到的这两个属性:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> //本机的IPV4地址
NSData * connectInterface4;
//本机的IPV6地址
NSData * connectInterface6;
</code></pre></div></div>
<p>我们来看看这个前置检查方法:</p>
<h3 id="本文方法三前置检查方法">本文方法三–前置检查方法</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//在连接之前的接口检查,一般我们传nil interface本机的IP 端口等等
- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr
{
//先断言,如果当前的queue不是初始化quueue,直接报错
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//无代理
if (delegate == nil) // Must have delegate set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate. Set a delegate first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//没有代理queue
if (delegateQueue == NULL) // Must have delegate queue set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//当前不是非连接状态
if (![self isDisconnected]) // Must be disconnected
{
if (errPtr)
{
NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//判断是否支持IPV4 IPV6 &位与运算,因为枚举是用 左位移<<运算定义的,所以可以用来判断 config包不包含某个枚举。因为一个值可能包含好几个枚举值,所以这时候不能用==来判断,只能用&来判断
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
//是否都不支持
if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
{
if (errPtr)
{
NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
//如果有interface,本机地址
if (interface)
{
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;
//得到本机的IPV4 IPV6地址
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
//如果两者都为nil
if ((interface4 == nil) && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv4Disabled && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv6Disabled && (interface4 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
*errPtr = [self badParamError:msg];
}
return NO;
}
//如果都没问题,则赋值
connectInterface4 = interface4;
connectInterface6 = interface6;
}
// Clear queues (spurious read/write requests post disconnect)
//清除queue(假的读写请求 ,提交断开连接)
//读写Queue清除
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
return YES;
}
</code></pre></div></div>
<p>又是非常长的一个方法,但是这个方法还是非常好读的。</p>
<ul>
<li>主要是对连接前的一个属性参数的判断,如果不齐全的话,则填充错误指针,并且返回NO。</li>
<li>
<p>在这里如果我们interface这个参数不为空话,我们会额外多执行一些操作。
首先来讲讲这个参数是什么,简单来讲,这个就是我们设置的本机IP+端口号。照理来说我们是不需要去设置这个参数的,默认的为localhost(127.0.0.1)本机地址。而端口号会在本机中取一个空闲可用的端口。
而我们一旦设置了这个参数,就会强制本地IP和端口为我们指定的。其实这样设置反而不好,其实大家也能想明白,这里端口号如果我们写死,万一被其他进程给占用了。那么肯定是无法连接成功的。
所以就有了我们做IM的时候,一般是不会去指定客户端bind某一个端口。而是用系统自动去选择。</p>
</li>
<li>我们最后清空了当前读写queue中,所有的任务。</li>
</ul>
<p>至于有interface,我们所做的额外操作是什么呢,我们接下来看看这个方法:</p>
<h3 id="本文方法四本地地址绑定方法">本文方法四–本地地址绑定方法</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr
address6:(NSMutableData **)interfaceAddr6Ptr
fromDescription:(NSString *)interfaceDescription
port:(uint16_t)port
{
NSMutableData *addr4 = nil;
NSMutableData *addr6 = nil;
NSString *interface = nil;
//先用:分割
NSArray *components = [interfaceDescription componentsSeparatedByString:@":"];
if ([components count] > 0)
{
NSString *temp = [components objectAtIndex:0];
if ([temp length] > 0)
{
interface = temp;
}
}
if ([components count] > 1 && port == 0)
{
//拿到port strtol函数,将一个字符串,根据base参数转成长整型,如base值为10则采用10进制,若base值为16则采用16进制
long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10);
//UINT16_MAX,65535最大端口号
if (portL > 0 && portL <= UINT16_MAX)
{
port = (uint16_t)portL;
}
}
//为空则自己创建一个 0x00000000 ,全是0 ,为线路地址
//如果端口为0 通常用于分析操作系统。这一方法能够工作是因为在一些系统中“0”是无效端口,当你试图使用通常的闭合端口连接它时将产生不同的结果。一种典型的扫描,使用IP地址为0.0.0.0,设置ACK位并在以太网层广播。
if (interface == nil)
{
struct sockaddr_in sockaddr4;
//memset作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
//memset(void *s,int ch,size_t n);函数,第一个参数为指针地址,第二个为设置值,第三个为连续设置的长度(大小)
memset(&sockaddr4, 0, sizeof(sockaddr4));
//结构体长度
sockaddr4.sin_len = sizeof(sockaddr4);
//addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
sockaddr4.sin_family = AF_INET;
//端口号 htons将主机字节顺序转换成网络字节顺序 16位
sockaddr4.sin_port = htons(port);
//htonl ,将INADDR_ANY:0.0.0.0,不确定地址,或者任意地址 htonl 32位。 也是转为网络字节序
//ipv4 32位 4个字节 INADDR_ANY,0x00000000 (16进制,一个0代表4位,8个0就是32位) = 4个字节的
sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
struct sockaddr_in6 sockaddr6;
memset(&sockaddr6, 0, sizeof(sockaddr6));
sockaddr6.sin6_len = sizeof(sockaddr6);
//ipv6
sockaddr6.sin6_family = AF_INET6;
//port
sockaddr6.sin6_port = htons(port);
//共128位
sockaddr6.sin6_addr = in6addr_any;
//把这两个结构体转成data
addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
}
//如果localhost、loopback 回环地址,虚拟地址,路由器工作它就存在。一般用来标识路由器
//这两种的话就赋值为127.0.0.1,端口为port
else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"])
{
// LOOPBACK address
//ipv4
struct sockaddr_in sockaddr4;
memset(&sockaddr4, 0, sizeof(sockaddr4));
sockaddr4.sin_len = sizeof(sockaddr4);
sockaddr4.sin_family = AF_INET;
sockaddr4.sin_port = htons(port);
//#define INADDR_LOOPBACK (u_int32_t)0x7f000001
//7f000001->1111111 00000000 00000000 00000001->127.0.0.1
sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
//ipv6
struct sockaddr_in6 sockaddr6;
memset(&sockaddr6, 0, sizeof(sockaddr6));
sockaddr6.sin6_len = sizeof(sockaddr6);
sockaddr6.sin6_family = AF_INET6;
sockaddr6.sin6_port = htons(port);
sockaddr6.sin6_addr = in6addr_loopback;
//赋值
addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
}
//非localhost、loopback,去获取本机IP,看和传进来Interface是同名或者同IP,相同才给赋端口号,把数据封装进Data。否则为nil
else
{
//转成cString
const char *iface = [interface UTF8String];
//定义结构体指针,这个指针是本地IP
struct ifaddrs *addrs;
const struct ifaddrs *cursor;
//获取到本机IP,为0说明成功了
if ((getifaddrs(&addrs) == 0))
{
//赋值
cursor = addrs;
//如果IP不为空,则循环链表去设置
while (cursor != NULL)
{
//如果 addr4 IPV4地址为空,而且地址类型为IPV4
if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET))
{
// IPv4
struct sockaddr_in nativeAddr4;
//memcpy内存copy函数,把src开始到size的字节数copy到 dest中
memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4));
//比较两个字符串是否相同,本机的IP名,和接口interface是否相同
if (strcmp(cursor->ifa_name, iface) == 0)
{
// Name match
//相同则赋值 port
nativeAddr4.sin_port = htons(port);
//用data封号IPV4地址
addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
//本机IP名和interface不相同
else
{
//声明一个IP 16位的数组
char ip[INET_ADDRSTRLEN];
//这里是转成了10进制。。(因为获取到的是二进制IP)
const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip));
//如果conversion不为空,说明转换成功而且 ,比较转换后的IP,和interface是否相同
if ((conversion != NULL) && (strcmp(ip, iface) == 0))
{
// IP match
//相同则赋值 port
nativeAddr4.sin_port = htons(port);
addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
}
}
//IPV6 一样
else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6))
{
// IPv6
struct sockaddr_in6 nativeAddr6;
memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6));
if (strcmp(cursor->ifa_name, iface) == 0)
{
// Name match
nativeAddr6.sin6_port = htons(port);
addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
}
else
{
char ip[INET6_ADDRSTRLEN];
const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip));
if ((conversion != NULL) && (strcmp(ip, iface) == 0))
{
// IP match
nativeAddr6.sin6_port = htons(port);
addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
}
}
}
//指向链表下一个addr
cursor = cursor->ifa_next;
}
//和getifaddrs对应,释放这部分内存
freeifaddrs(addrs);
}
}
//如果这两个二级指针存在,则取成一级指针,把addr4赋值给它
if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4;
if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6;
</code></pre></div></div>
<p>这个方法中,主要是大量的socket相关的函数的调用,会显得比较难读一点,其实简单来讲就做了这么一件事:
把interface变成进行socket操作所需要的地址结构体,然后把地址结构体包裹在NSMutableData中。</p>
<p>这里,为了让大家能更容易理解,我把这个方法涉及到的socket相关函数以及宏(按照调用顺序)都列出来:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//拿到port strtol函数,将一个字符串,根据base参数转成长整型,
//如base值为10则采用10进制,若base值为16则采用16进制
long strtol(const char *__str, char **__endptr, int __base);
//作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
//第一个参数为指针地址,第二个为设置值,第三个为连续设置的长度(大小)
memset(void *s,int ch,size_t n);
//最大端口号
#define UINT16_MAX 65535
//作用是把主机字节序转化为网络字节序
htons() //参数16位
htonl() //参数32位
//获取占用内存大小
sizeof()
//比较两个指针,是否相同 相同返回0
int strcmp(const char *__s1, const char *__s2)
//内存copu函数,把src开始到len的字节数copy到 dest中
memcpy(dest, src, len)
//inet_pton和inet_ntop这2个IP地址转换函数,可以在将IP地址在“点分十进制”和“二进制整数”之间转换
//参数socklen_t cnt,他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
//得到本机地址
extern int getifaddrs(struct ifaddrs **);
//释放本机地址
extern void freeifaddrs(struct ifaddrs *);
</code></pre></div></div>
<p>还有一些用到的作为参数的结构体:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//socket通信用的 IPV4地址结构体
struct sockaddr_in {
__uint8_t sin_len; //整个结构体大小
sa_family_t sin_family; //协议族,IPV4?IPV6
in_port_t sin_port; //端口
struct in_addr sin_addr; //IP地址
char sin_zero[8]; //空的占位符,为了和其他地址结构体保持一致大小,方便转化
};
//IPV6地址结构体,和上面的类似
struct sockaddr_in6 {
__uint8_t sin6_len; /* length of this struct(sa_family_t) */
sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */
in_port_t sin6_port; /* Transport layer port # (in_port_t) */
__uint32_t sin6_flowinfo; /* IP6 flow information */
struct in6_addr sin6_addr; /* IP6 address */
__uint32_t sin6_scope_id; /* scope zone index */
};
//用来获取本机IP的参数结构体
struct ifaddrs {
//指向链表的下一个成员
struct ifaddrs *ifa_next;
//接口名称
char *ifa_name;
//接口标识位(比如当IFF_BROADCAST或IFF_POINTOPOINT设置到此标识位时,影响联合体变量ifu_broadaddr存储广播地址或ifu_dstaddr记录点对点地址)
unsigned int ifa_flags;
//接口地址
struct sockaddr *ifa_addr;
//存储该接口的子网掩码;
struct sockaddr *ifa_netmask;
//点对点的地址
struct sockaddr *ifa_dstaddr;
//ifa_data存储了该接口协议族的特殊信息,它通常是NULL(一般不关注他)。
void *ifa_data;
};
</code></pre></div></div>
<p>这一段内容算是比较枯涩了,但是也是了解socket编程必经之路。</p>
<p>这里提到了网络字节序和主机字节序。我们创建socket之前,必须把port和host这些参数转化为网络字节序。那么为什么要这么做呢?</p>
<blockquote>
<p>不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序
最常见的有两种
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址</p>
</blockquote>
<p>这样如果我们到网络中,就无法得知互相的字节序是什么了,所以我们就必须统一一套排序,这样网络字节序就有它存在的必要了。</p>
<blockquote>
<p>网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关。从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。</p>
</blockquote>
<p>大家感兴趣可以到这篇文章中去看看:<a href="http://blog.csdn.net/houwei544/article/details/8592996">网络字节序与主机字节序</a>。</p>
<p>除此之外比较重要的就是这几个地址结构体了。它定义了我们当前socket的地址信息。包括IP、Port、长度、协议族等等。当然socket中标识为地址的结构体不止这3种,等我们后续代码来补充。</p>
<p>大家了解了我们上述说的知识点,这个方法也就不难度了。这个方法主要是做了本机IPV4和IPV6地址的创建和绑定。当然这里分了几种情况:</p>
<p>1.interface为空的,我们作为客户端不会出现这种情况。注意之前我们是这个参数不为空才会调入这个方法的。
而这个一般是用于做服务端监听用的,这里的处理是给本机地址绑定0地址(任意地址)。那么这里这么做作用是什么呢?引用一个应用场景来说明:</p>
<blockquote>
<p>如果你的服务器有多个网卡(每个网卡上有不同的IP地址),而你的服务(不管是在udp端口上侦听,还是在tcp端口上侦听),出于某种原因:可能是你的服务器操作系统可能随时增减IP地址,也有可能是为了省去确定服务器上有什么<a href="http://baike.baidu.com/view/43200.htm">网络端口</a>(网卡)的麻烦 —— 可以要在调用<a href="http://baike.baidu.com/view/569184.htm">bind()</a>的时候,告诉操作系统:“我需要在 yyyy 端口上侦听,所有发送到<a href="http://baike.baidu.com/view/899.htm">服务器</a>的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都是我处理的。”这时候,服务器程序则在0.0.0.0这个地址上进行侦听。</p>
</blockquote>
<p>2.如果interface为localhost或者loopback则把IP设置为127.0.0.1,这里localhost我们大家都知道。那么什么是loopback呢?
loopback地址叫做回环地址,他不是一个物理接口上的地址,他是一个虚拟的一个地址,只要路由器在工作,这个地址就存在.它是路由器的唯一标识。
更详细的内容可以看看百科:<a href="http://baike.baidu.com/link?url=Xte8hyBPtSWdJGAZd8A7hPR3p_oMHsDHE5sZUbezw3EH4tIVmgKKcCtIcBSA8Ah6hBqd9hsPMBLgPjbegr1g4gJnqgpDclP6rhc000P6lTy">loopback</a></p>
<p>3.如果是一个其他的地址,我们会去使用getifaddrs()函数得到本机地址。然后去对比本机名或者本机IP。有一个能相同,我们就认为该地址有效,就进行IPV4和IPV6绑定。否则什么都不做。</p>
<p>至此这个本机地址绑定我们就做完了,我们前面也说过,一般我们作为客户端,是不需要做这一步的。如果我们不绑定,系统会自己绑定本机IP,并且选择一个空闲可用的端口。所以这个方法是iOS用来作为服务端调用的。</p>
<h3 id="方法三前置检查方法四本机地址绑定都说完了我们继续接着之前的方法二往下看">方法三–前置检查、方法四–本机地址绑定都说完了,我们继续接着之前的方法二往下看:</h3>
<p>之前讲到第3点了:</p>
<p>3.这里把flag标记为kSocketStarted:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flags |= kSocketStarted;
</code></pre></div></div>
<table>
<tbody>
<tr>
<td>源码中大量的运用了3个位运算符:分别是或(</td>
<td>)、与(&)、取反(~)、运算符。 运用这个标记的好处也很明显,可以很简单的标记当前的状态,并且因为flags所指向的枚举值是用左位移的方式:</td>
</tr>
</tbody>
</table>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>enum GCDAsyncSocketFlags
{
kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting)
kConnected = 1 << 1, // If set, the socket is connected
kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed
kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout
kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout
kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued
kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued
kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown.
kReadSourceSuspended = 1 << 8, // If set, the read source is suspended
kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended
kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS
kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete
kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete
kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS
kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket
kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained
kDealloc = 1 << 16, // If set, the socket is being deallocated
#if TARGET_OS_IPHONE
kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread
kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport
kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available
#endif
};
</code></pre></div></div>
<table>
<tbody>
<tr>
<td>所以flags可以通过</td>
<td>的方式复合横跨多个状态,并且运算也非常轻量级,好处很多,所有的状态标记的意义可以在注释中清晰的看出,这里把状态标记为socket已经开始连接了。</td>
</tr>
</tbody>
</table>
<p>4.然后我们调用了一个全局queue,异步的调用连接,这里又做了两件事:</p>
<p>第一步是拿到我们需要连接的服务端server的地址数组:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//server地址数组(包含IPV4 IPV6的地址 sockaddr_in6、sockaddr_in类型)
NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
</code></pre></div></div>
<p>第二步是做一些错误判断,并且把地址信息赋值到address4和address6中去,然后异步调用回socketQueue去用另一个方法去发起连接:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
</code></pre></div></div>
<p>在这个方法中我们可以看到作者这里把创建server地址这些费时的逻辑操作放在了异步线程中并发进行。然后得到数据之后又回到了我们的socketQueue发起下一步的连接。</p>
<p>然后这里又是两个很大块的分支,首先我们来看看server地址的获取:</p>
<p>本文方法五–创建服务端server地址数据:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//根据host、port
+ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr
{
LogTrace();
NSMutableArray *addresses = nil;
NSError *error = nil;
//如果Host是这localhost或者loopback
if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"])
{
// Use LOOPBACK address
struct sockaddr_in nativeAddr4;
nativeAddr4.sin_len = sizeof(struct sockaddr_in);
nativeAddr4.sin_family = AF_INET;
nativeAddr4.sin_port = htons(port);
nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
//占位置0
memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero));
//ipv6
struct sockaddr_in6 nativeAddr6;
nativeAddr6.sin6_len = sizeof(struct sockaddr_in6);
nativeAddr6.sin6_family = AF_INET6;
nativeAddr6.sin6_port = htons(port);
nativeAddr6.sin6_flowinfo = 0;
nativeAddr6.sin6_addr = in6addr_loopback;
nativeAddr6.sin6_scope_id = 0;
// Wrap the native address structures
NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
//两个添加进数组
addresses = [NSMutableArray arrayWithCapacity:2];
[addresses addObject:address4];
[addresses addObject:address6];
}
else
{
//拿到port String
NSString *portStr = [NSString stringWithFormat:@"%hu", port];
//定义三个addrInfo 是一个sockaddr结构的链表而不是一个地址清单
struct addrinfo hints, *res, *res0;
//初始化为0
memset(&hints, 0, sizeof(hints));
//相当于 AF_UNSPEC ,返回的是适用于指定主机名和服务名且适合任何协议族的地址。
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
//根据host port,去获取地址信息。
int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);
//出错
if (gai_error)
{ //获取到错误
error = [self gaiError:gai_error];
}
//正确获取到addrInfo
else
{
//
NSUInteger capacity = 0;
//遍历 res0
for (res = res0; res; res = res->ai_next)
{
//如果有IPV4 IPV6的,capacity+1
if (res->ai_family == AF_INET || res->ai_family == AF_INET6) {
capacity++;
}
}
//生成一个地址数组,数组为capacity大小
addresses = [NSMutableArray arrayWithCapacity:capacity];
//再去遍历,为什么不一次遍历完,仅仅是为了限制数组的大小?
for (res = res0; res; res = res->ai_next)
{
//IPV4
if (res->ai_family == AF_INET)
{
// Found IPv4 address.
// Wrap the native address structure, and add to results.
//加到数组中
NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
[addresses addObject:address4];
}
else if (res->ai_family == AF_INET6)
{
// Fixes connection issues with IPv6
// https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158
// Found IPv6 address.
// Wrap the native address structure, and add to results.
//强转
struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)res->ai_addr;
//拿到port
in_port_t *portPtr = &sockaddr->sin6_port;
//如果Port为0
if ((portPtr != NULL) && (*portPtr == 0)) {
//赋值,用传进来的port
*portPtr = htons(port);
}
//添加到数组
NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
[addresses addObject:address6];
}
}
//对应getaddrinfo 释放内存
freeaddrinfo(res0);
//如果地址里一个没有,报错 EAI_FAIL:名字解析中不可恢复的失败
if ([addresses count] == 0)
{
error = [self gaiError:EAI_FAIL];
}
}
}
//赋值错误
if (errPtr) *errPtr = error;
//返回地址
return addresses;
}
</code></pre></div></div>
<p>这个方法根据host进行了划分:</p>
<p>如果host为localhost或者loopback,则按照我们之前绑定本机地址那一套生成地址的方式,去生成IPV4和IPV6的地址,并且用NSData包裹住这个地址结构体,装在NSMutableArray中。
不是本机地址,那么我们就需要根据host和port去创建地址了,这里用到的是这么一个函数:</p>
<p>int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );
这个函数主要的作用是:根据hostname(IP),service(port),去获取地址信息,并且把地址信息传递到result中。
而hints这个参数可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,如果填了,其实它就是一个配置参数,返回的地址信息会和这个配置参数的内容有关,如下例:</p>
<p>举例来说:指定的服务既可支持TCP也可支持UDP,所以调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
这里我们可以看到result和hints这两个参数指针指向的都是一个addrinfo的结构体,这是我们继上面以来看到的第4种地址结构体了。它的定义如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
socklen_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for hostname */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
</code></pre></div></div>
<p>我们可以看到它其中包括了一个IPV4的结构体地址ai_addr,还有一个指向下一个同类型数据节点的指针ai_next。
其他参数和之前的地址结构体一些参数作用类似,大家可以对着注释很好理解,或者仍有疑惑可以看看这篇:
socket编程之addrinfo结构体与getaddrinfo函数
这里讲讲ai_next这个指针,因为我们是去获取server端的地址,所以很可能有不止一个地址,比如IPV4、IPV6,又或者我们之前所说的一个服务器有多个网卡,这时候可能就会有多个地址。这些地址就会用ai_next指针串联起来,形成一个单链表。</p>
<p>然后我们拿到这个地址链表,去遍历它,对应取出IPV4、IPV6的地址,封装成NSData并装到数组中去。</p>
<p>如果中间有错误,赋值错误,返回地址数组,理清楚这几个结构体与函数,这个方法还是相当容易读的,具体的细节可以看看注释。</p>
<p>接着我们回到本文方法二,就要用这个地址数组去做连接了。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//异步去发起连接
dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
}});
</code></pre></div></div>
<p>这里调用了我们本文方法六–开始连接的方法1</p>
<p>//连接的最终方法 1</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//至少有一个server地址
NSAssert(address4 || address6, @"Expected at least one valid address");
//如果状态不一致,说明断开连接
if (aStateIndex != stateIndex)
{
LogInfo(@"Ignoring lookupDidSucceed, already disconnected");
// The connect operation has been cancelled.
// That is, socket was disconnected, or connection has already timed out.
return;
}
// Check for problems
//分开判断。
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
if (isIPv4Disabled && (address6 == nil))
{
NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address.";
[self closeWithError:[self otherError:msg]];
return;
}
if (isIPv6Disabled && (address4 == nil))
{
NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address.";
[self closeWithError:[self otherError:msg]];
return;
}
// Start the normal connection process
NSError *err = nil;
//调用连接方法,如果失败,则错误返回
if (![self connectWithAddress4:address4 address6:address6 error:&err])
{
[self closeWithError:err];
}
}
</code></pre></div></div>
<p>这个方法也比较简单,基本上就是做了一些错误的判断。比如:</p>
<ol>
<li>
<p>判断在不在这个socket队列。</p>
</li>
<li>
<p>判断传过来的aStateIndex和属性stateIndex是不是同一个值。说到这个值,不得不提的是大神用的框架,在容错处理上,做的真不是一般的严谨。从这个stateIndex上就能略见一二。
这个aStateIndex是我们之前调用方法,用属性传过来的,所以按道理说,是肯定一样的。但是就怕在调用过程中,这个值发生了改变,这时候整个socket配置也就完全不一样了,有可能我们已经置空地址、销毁socket、断开连接等等…等我们后面再来看这个属性stateIndex在什么地方会发生改变。</p>
</li>
<li>
<p>判断config中是需要哪种配置,它的参数对应了一个枚举:</p>
<p>enum GCDAsyncSocketConfig
{
kIPv4Disabled = 1 « 0, // If set, IPv4 is disabled
kIPv6Disabled = 1 « 1, // If set, IPv6 is disabled
kPreferIPv6 = 1 « 2, // If set, IPv6 is preferred over IPv4
kAllowHalfDuplexConnection = 1 « 3, // If set, the socket will stay open even if the read stream closes
};</p>
</li>
</ol>
<p>前3个大家很好理解,无非就是用IPV4还是IPV6。
而第4个官方注释意思是,我们即使关闭读的流,也会保持Socket开启。至于具体是什么意思,我们先不在这里讨论,等后文再说。
这里调用了我们本文方法七–开始连接的方法2</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//连接最终方法 2。用两个Server地址去连接,失败返回NO,并填充error
- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
//输出一些东西?
LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]);
LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]);
// Determine socket type
//判断是否倾向于IPV6
BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO;
// Create and bind the sockets
//如果有IPV4地址,创建IPV4 Socket
if (address4)
{
LogVerbose(@"Creating IPv4 socket");
socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr];
}
//如果有IPV6地址,创建IPV6 Socket
if (address6)
{
LogVerbose(@"Creating IPv6 socket");
socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr];
}
//如果都为空,直接返回
if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL)
{
return NO;
}
//主选socketFD,备选alternateSocketFD
int socketFD, alternateSocketFD;
//主选地址和备选地址
NSData *address, *alternateAddress;
//IPV6
if ((preferIPv6 && socket6FD) || socket4FD == SOCKET_NULL)
{
socketFD = socket6FD;
alternateSocketFD = socket4FD;
address = address6;
alternateAddress = address4;
}
//主选IPV4
else
{
socketFD = socket4FD;
alternateSocketFD = socket6FD;
address = address4;
alternateAddress = address6;
}
//拿到当前状态
int aStateIndex = stateIndex;
//用socket和address去连接
[self connectSocket:socketFD address:address stateIndex:aStateIndex];
//如果有备选地址
if (alternateAddress)
{
//延迟去连接备选的地址
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{
[self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex];
});
}
return YES;
}
</code></pre></div></div>
<p>这个方法也仅仅是连接中过渡的一个方法,做的事也非常简单:</p>
<ol>
<li>就是拿到IPV4和IPV6地址,先去创建对应的socket,注意这个socket是本机客户端的,和server端没有关系。这里服务端的IPV4和IPV6地址仅仅是用来判断是否需要去创建对应的本机Socket。这里去创建socket会带上我们之前生成的本地地址信息connectInterface4或者connectInterface6。</li>
</ol>
<p>2.根据我们的config配置,得到主选连接和备选连接。 然后先去连接主选连接地址,在用我们一开始初始化中设置的属性alternateAddressDelay,就是这个备选连接延时的属性,去延时连接备选地址(当然如果主选地址在此时已经连接成功,会再次连接导致socket错误,并且关闭)。</p>
<p>这两步分别调用了各自的方法去实现,接下来我们先来看创建本机Socket的方法:</p>
<p>本文方法八–创建Socket:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//创建Socket
- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr
{
//创建socket,用的SOCK_STREAM TCP流
int socketFD = socket(family, SOCK_STREAM, 0);
//如果创建失败
if (socketFD == SOCKET_NULL)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
return socketFD;
}
//和connectInterface绑定
if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr])
{
//绑定失败,直接关闭返回
[self closeSocket:socketFD];
return SOCKET_NULL;
}
// Prevent SIGPIPE signals
//防止终止进程的信号?
int nosigpipe = 1;
//SO_NOSIGPIPE是为了避免网络错误,而导致进程退出。用这个来避免系统发送signal
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
return socketFD;
}
</code></pre></div></div>
<p>这个方法做了这么几件事:</p>
<p>创建了一个socket:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//创建一个socket,返回值为Int。(注scoket其实就是Int类型)
//第一个参数addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
//第二个参数 type 表示 socket 的类型,通常是流stream(SOCK_STREAM) 或数据报文datagram(SOCK_DGRAM)
//第三个参数 protocol 参数通常设置为0,以便让系统自动为选择我们合适的协议,对于 stream socket 来说会是 TCP 协议(IPPROTO_TCP),而对于 datagram来说会是 UDP 协议(IPPROTO_UDP)。
int socketFD = socket(family, SOCK_STREAM, 0);
</code></pre></div></div>
<p>其实这个函数在之前那篇IM文章中也讲过了,大家参考参考注释看看就可以了,这里如果返回值为-1,说明创建失败。</p>
<p>去绑定我们之前创建的本地地址,它调用了另外一个方法来实现。</p>
<p>最后我们调用了如下函数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
</code></pre></div></div>
<p>那么这个函数是做什么用的呢?简单来说,它就是给我们的socket加一些额外的设置项,来配置socket的一些行为。它还有许多的用法,具体可以参考这篇文章:<a href="http://baike.baidu.com/link?url=qZ3TqqbYLcudk5OsauA4aiIFeD7mLfGdSE0ttP_ALK5_VX1yhcwhhuv1Ba7s4XF_3975YY9unFO_dtEYQTIUEFq0LfQvvMNMBJjh8mHPehK">setsockopt函数</a></p>
<p>而这里的目的是为了来避免网络错误而出现的进程退出的情况,调用了这行函数,网络错误后,系统不再发送进程退出的信号。
关于这个进程退出的错误可以参考这篇文章:<a href="http://www.sinohandset.com/mac-osx下so_nosigpipe的怪异表现.html">Mac OSX下SO_NOSIGPIPE的怪异表现</a></p>
<p>未完总结:</p>
<p>connect篇还没有完结,奈何篇幅问题,只能断在这里。下一个方法将是socket本地绑定的方法。再下面就是我们最终的连接方法了,历经九九八十一难,马上就要取到真经了…(然而这仅仅是一个开始…)
下一篇将会承接这一篇的内容继续讲,包括最终连接、连接完成后的source和流的处理。
我们还会去讲讲iOS作为服务端的accpet建立连接的流程。
除此之外还有 unix domin socket(进程间通信)的连接。</p>
<p>最近总感觉很浮躁,贴一句一直都很喜欢的话:
上善若水。水善利万物而不争</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao71">iOS即时通讯进阶 - CocoaAsyncSocket源码解析(Connect篇)</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on February 17, 2017.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao70
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao70
2016-11-14T00:00:00+08:00
2016-11-14T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="//allluckly.cn/images/blog/tuogao/tougao70/1.gif" alt="1" /><br /></p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/f99d72bf8452">逆流丶而上</a><br /></p>
<p>iOS7推出了新的转场动画API,以协id<UIViewControllerInterativeTransition>、id<UIViewAnimatedTransitioning>方式开放给开发者。但是由于其复杂的API及繁琐的实现方式,使众多的开发者望而止步。</UIViewAnimatedTransitioning></UIViewControllerInterativeTransition></p>
<p>这里我封装了几种常见的转场动画,简化的使用方式,可以直接用cocoapod搜索WTKTransitionAnimate,导入即可.</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao70/2.png" alt="1" /><br /></p>
<h2 id="使用方法">使用方法</h2>
<p>导入#import <WTKTransition.h></WTKTransition.h></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>self.navigationController.delegate = [WTKTransition shareManager];
[WTKTransition shareManager].animationType = WTKAnimateTypeKuGou;
</code></pre></div></div>
<p>只需要把navigationController的代理设置为WTKTransition的单例对象即可,animationType为动画类型,如下</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef NS_ENUM(NSInteger,WTKAnimateType)
{
WTKAnimateTypeDefault = 0,
/// 两个导航栏不一致
WTKAnimateTypeDiffNavi,
///仿酷狗
WTKAnimateTypeKuGou,
///圆形遮罩
WTKAnimateTypeRound,
///椭圆遮罩
WTKAnimateTypeOval,
///仿斗鱼
WTKAnimateTypeDouYu,
};
</code></pre></div></div>
<h2 id="实现过程">实现过程</h2>
<p>在单例对象实现了转场动画的两个代理方法,使用工厂模式根据animateType创建子类,在子类中实现具体的动画。动画的可交互属性则是通过UIPercentDrivenInteractiveTransition来实现,需要在viewController.view上面添加拖动手势,根据手势来改变UIPercentDrivenInteractiveTransition的动画状态,一般的都是在父类的viewController添加手势,这里为了简化使用,使用类别实现,利用runtime给viewController关联属性,并且拦截viewDidload方法,在viewDidLoad中添加返回手势</p>
<p>WTKAnimateTypeRound这个动画,圆心每次都在点击的坐标,实现方式为获取每次点击的坐标,然后把这个坐标设置为下次push的圆心。获取坐标方式有两种</p>
<p>1、继承Appdelegate,然后实现Appdelegate的sendEvent方法,通过Event获取坐标。</p>
<p>2、使用类别,通过runtime拦截sendEvent方法,然后通过Event获取坐标。
为了简化WTKTransitionAnimation的使用方法,这里通过类别来实现。
另外,当侧滑返回取消时,会发送一个通知WTK_CANCEL_POP</p>
<p><a href="https://github.com/wangtongke/WTKTransitionAnimate">demo下载地址</a></p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="http://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="http://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao70">iOS 两行代码实现自定义转场动画</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on November 14, 2016.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao69
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao69
2016-11-07T00:00:00+08:00
2016-11-07T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/36ba5d65804f">Sindri的小巢</a><br /></p>
<h2 id="前言">前言</h2>
<p>从现代计算机电路来说,只有<code class="language-plaintext highlighter-rouge">通电/没电</code>两种状态,即为<code class="language-plaintext highlighter-rouge">0/1</code>状态,计算机中所有的数据按照具体的编码格式以二进制的形式存储在设备中。</p>
<p>直接操作这些二进制数据的位数据就是位运算,在iOS中基本所有的位运算都通过枚举声明传值的方式将位运算的实现细节隐藏了起来:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef NS_OPTIONS(NSUInteger, UIRectEdge) {
UIRectEdgeNone = 0,
UIRectEdgeTop = 1 << 0,
UIRectEdgeLeft = 1 << 1,
UIRectEdgeBottom = 1 << 2,
UIRectEdgeRight = 1 << 3,
UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight
} NS_ENUM_AVAILABLE_IOS(7_0);
</code></pre></div></div>
<p>位运算是一种极为高效乃至可以说最为高效的计算方式,虽然现代程序开发中编译器已经为我们做了大量的优化,但是合理的使用位运算可以提高代码的可读性以及执行效率。</p>
<h2 id="基础计算">基础计算</h2>
<p>在了解怎么使用位运算之前,笔者简单说一下CPU处理计算的过程。如果你对<code class="language-plaintext highlighter-rouge">CPU</code>的计算方式有所了解,可以跳过这一节。</p>
<p>当代码<code class="language-plaintext highlighter-rouge">int sum = 11 + 79</code>被执行的时候,计算机直接将两个数的二进制位进行相加和进位操作:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>11: 0 0 0 0 1 0 1 1
79: 0 1 0 0 1 1 1 1
————————————————————
90: 0 1 0 1 1 0 1 0
</code></pre></div></div>
<p>通常来说CPU执行两个数相加操作所花费的时间被我们称作一个时钟周期,而2.0GHz频率的CPU表示可以在一秒执行运算<code class="language-plaintext highlighter-rouge">2.0*1024*1024*1024</code>个时钟周期。相较于加法运算,下面看一下<code class="language-plaintext highlighter-rouge">11*2</code>、<code class="language-plaintext highlighter-rouge">11*4</code>的二进制结果:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>11: 0 0 0 0 1 0 1 1 * 2
————————————————————
22: 0 0 0 1 0 1 1 0
11: 0 0 0 0 1 0 1 1 * 4
————————————————————
44: 0 0 1 0 1 1 0 0
</code></pre></div></div>
<p>简单来说,不难发现当某个数乘以<code class="language-plaintext highlighter-rouge">2的N次幂</code>的时候,结果等同于将这个数的二进制位置向左移动<code class="language-plaintext highlighter-rouge">N</code>位,在代码中我们使用<code class="language-plaintext highlighter-rouge">num << N</code>表示将<code class="language-plaintext highlighter-rouge">num</code>的二进制数据左移<code class="language-plaintext highlighter-rouge">N</code>个位置,其效果等同于下面这段代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for (int idx = 0; idx < N; idx++) {
num *= 2;
}
</code></pre></div></div>
<p>假如相乘的两个数都不是<code class="language-plaintext highlighter-rouge">2的N次幂</code>,这时候编译器会将其中某个值分解成多个<code class="language-plaintext highlighter-rouge">2的N次幂</code>相加的结果进行运算。比如<code class="language-plaintext highlighter-rouge">37 * 69</code>,这时候CPU会将<code class="language-plaintext highlighter-rouge">37</code>分解成<code class="language-plaintext highlighter-rouge">32+4+1</code>,然后换算成<code class="language-plaintext highlighter-rouge">(69<<5) + (69<<2) + (69<<0)</code>的方式计算出结果。因此,计算两个数相乘通常需要十个左右的时钟周期。 同理,代码<code class="language-plaintext highlighter-rouge">num >> N</code>的作用等效于:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for (int idx = 0; idx < N; idx++) {
num /= 2;
}
</code></pre></div></div>
<p>但是两个数相除花费的时钟周期要比乘法还要多得多,其大部分消耗在将数值分解成多个<code class="language-plaintext highlighter-rouge">2的N次幂</code>上。除此之外,浮点数涉及到的计算更为复杂,这里也简单聊聊浮点数的准确度问题。拿<code class="language-plaintext highlighter-rouge">float</code>类型来说,总共使用了<code class="language-plaintext highlighter-rouge">32bit</code>的存储空间,其中第一位表示正负,<code class="language-plaintext highlighter-rouge">2~13位</code>表示整数部分的值,<code class="language-plaintext highlighter-rouge">14~32位</code>之中分别存储了小数位以及科学计数的标识值(这里可能并不那么准确,主要是为了给读者一个大概的介绍)。由于小数位的二进制数据依旧保持<code class="language-plaintext highlighter-rouge">2的N次幂</code>特性,假如下面的二进制属于小数位:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 0 1 1 1 0 0 1
</code></pre></div></div>
<p>那么这部分小数位的值等于:<code class="language-plaintext highlighter-rouge">1/2 + 1/4 + 1/8 + 1/16 + 1/128 = 0.9453125</code>。因此,当你把一个没有任何规律的小数例如<code class="language-plaintext highlighter-rouge">3.1415926535898</code>存入计算机的时候,小数点后面会被拆解成很多的<code class="language-plaintext highlighter-rouge">2的N次幂</code>进行保存。由于小数位总是有限的,因此当分解的<code class="language-plaintext highlighter-rouge">N</code>超出这些位数时导致存储不下,就会出现精度偏差。另一方面,这样的分解计算势必要消耗大量的时钟周期,这也是大量的浮点数运算<code class="language-plaintext highlighter-rouge">(cell动态计算)</code>容易引发卡顿的原因。所以,当小数位过多时,改用字符串存储是一个更优的选择。</p>
<h2 id="位运算符">位运算符</h2>
<p>使用的运算符包括下面:</p>
<table>
<thead>
<tr>
<th>含义</th>
<th style="text-align: center">运算符</th>
</tr>
</thead>
<tbody>
<tr>
<td>左移</td>
<td style="text-align: center">«</td>
</tr>
<tr>
<td>右移</td>
<td style="text-align: center">»</td>
</tr>
<tr>
<td>按位或</td>
<td style="text-align: center">︳</td>
</tr>
<tr>
<td>按位并</td>
<td style="text-align: center">&</td>
</tr>
<tr>
<td>按位取反</td>
<td style="text-align: center">~</td>
</tr>
<tr>
<td>按位异或</td>
<td style="text-align: center">^</td>
</tr>
</tbody>
</table>
<ul>
<li>
<p>& 操作</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 0 0 1 0 1 1 1 0 46
1 0 0 1 1 1 0 1 157
———————————————
0 0 0 0 1 1 0 0 12
</code></pre></div> </div>
</li>
<li>
<table>
<tbody>
<tr>
<td>操作</td>
</tr>
</tbody>
</table>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 0 0 1 0 1 1 1 0 46
1 0 0 1 1 1 0 1 157
———————————————
1 0 1 1 1 1 1 1 191
</code></pre></div> </div>
</li>
<li>
<p>~ 操作</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 0 0 1 0 1 1 1 0 46
———————————————
1 1 0 1 0 0 0 1 225
</code></pre></div> </div>
</li>
<li>
<p>^ 操作</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 0 0 1 0 1 1 1 0 46
1 0 0 1 1 1 0 1 157
———————————————
1 0 1 1 0 0 1 1 179
</code></pre></div> </div>
</li>
</ul>
<h2 id="色彩存储">色彩存储</h2>
<p>使用位运算包括下面几个原因:
1、代码更简洁
2、更高的效率
3、更少的内存</p>
<p>简单来说,我们如何单纯的保存一张<code class="language-plaintext highlighter-rouge">RGB</code>色彩空间下的图片?由于图片由一系列的像素组成,每个像素有着自己表达的颜色,因此需要这么一个类用来表示图片的单个像素:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface Pixel
@property (nonatomic, assign) CGFloat red;
@property (nonatomic, assign) CGFloat green;
@property (nonatomic, assign) CGFloat blue;
@property (nonatomic, assign) CGFloat alpha;
@end
</code></pre></div></div>
<p>那么在4.7寸的屏幕上,启动图需要<code class="language-plaintext highlighter-rouge">750*1334</code>个这样的类,不计算其他数据,单单是变量的存储需要<code class="language-plaintext highlighter-rouge">750*1334*4*8</code> = <code class="language-plaintext highlighter-rouge">32016000</code>个字节的占用内存。但实际上我们使用到的图片总是将<code class="language-plaintext highlighter-rouge">RGBA</code>这四个属性保存在一个<code class="language-plaintext highlighter-rouge">int</code>类型或者其它相似的少字节变量中。</p>
<p>由于色彩取值范围为<code class="language-plaintext highlighter-rouge">0~255</code>,即<code class="language-plaintext highlighter-rouge">2^1 ~ 2^8-1</code>不超过一个字节的整数占用内存。因此可以通过左移运算保证每一个字节只存储了一个决定色彩的值:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (int)rgbNumberWithRed: (int)red green: (int)green blue: (int)blue alpha: (float)alpha {
int bitPerByte = 8;
int maxNumber = 255;
int alphaInt = alpha * maxNumber;
int rgbNumber = (red << (bitPerByte*3)) + (green << (bitPerByte*2)) + (blue << bitPerByte) + alphaInt;
}
</code></pre></div></div>
<p>同理,通过右移操作保证数值的最后一个字节存储着需要的数据,并用<code class="language-plaintext highlighter-rouge">0xff</code>将值取出来:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)obtainRGBA: (int)rgbNumber {
int mask = 0xff;
int bitPerByte = 8;
double alphaInt = (rgbNumber & mask) / 255.0;
int blue = ((rgbNumber >> bitPerByte) & mask);
int green = ((rgbNumber >> (bitPerByte*2)) & mask);
int red = ((rgbNumber >> (bitPerByte*3)) & mask);
}
</code></pre></div></div>
<p>对比使用类和位运算存储,效率跟内存占用上可以说是完败。</p>
<h2 id="位运算应用">位运算应用</h2>
<p>苹果在类对象的结构中使用了位运算这一设计:每个对象都有一个整型类型的标识符<code class="language-plaintext highlighter-rouge">flags</code>,其中多个不同的位表示了是否存在弱引用、是否被初始化等信息,对于这些存储的数据通过<code class="language-plaintext highlighter-rouge">&</code>、<code class="language-plaintext highlighter-rouge">|</code>等运算符获取出来。这些在<a href="http://opensource.apple.com">runtime源码</a>中都能看到,下面是一段伪代码(参数请勿对号入座)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define IS_TAGGED_POINTER (1 << 12);
#define HAS_WEAK_REFERENCE (1 << 13);
inline void objc_object::free() {
if (this->flags | HAS_WEAK_REFERENCE) {
/// set all weak reference point to nil
}
}
inline int objc_object::retainCount() {
if (this.flags | IS_TAGGED_POINTER) {
return (int)INT_MAX;
} else {
return this->retainCount;
}
}
......
</code></pre></div></div>
<p>借鉴苹果的运算操作,可以声明一个应用常用权限的枚举,来获取我们的应用权限:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef NS_ENUM(NSInteger, LXDAuthorizationType)
{
LXDAuthorizationTypeNone = 0,
LXDAuthorizationTypePush = 1 << 0, ///< 推送授权
LXDAuthorizationTypeLocation = 1 << 1, ///< 定位授权
LXDAuthorizationTypeCamera = 1 << 2, ///< 相机授权
LXDAuthorizationTypePhoto = 1 << 3, ///< 相册授权
LXDAuthorizationTypeAudio = 1 << 4, ///< 麦克风授权
LXDAuthorizationTypeContacts = 1 << 5, ///< 通讯录授权
};
</code></pre></div></div>
<p>通过声明一个全局的权限变量来保存不同的授权信息。当应用拥有对应的授权时,通过<code class="language-plaintext highlighter-rouge">|</code>操作符保证对应的二进制位的值被修改成<code class="language-plaintext highlighter-rouge">1</code>。否则对对应授权枚举进行<code class="language-plaintext highlighter-rouge">~</code>取反后再<code class="language-plaintext highlighter-rouge">&</code>操作消除二进制位的授权表达。为了完成这些工作,建立一个工具类来获取以及更新授权的状态:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/*!
* @brief 获取应用授权信息工具,最低使用版本:iOS8.0
*/
NS_CLASS_AVAILABLE_IOS(8_0) @interface LXDAuthObtainTool : NSObject
/// 获取当前应用权限
+ (LXDAuthorizationType)obtainAuthorization;
/// 更新应用权限
+ (void)updateAuthorization;
@end
#pragma mark - LXDAuthObtainTool.m
static LXDAuthorizationType kAuthorization;
@implementation LXDAuthObtainTool
+ (void)initialize
{
kAuthorization = LXDAuthorizationTypeNone;
[self updateAuthorization];
}
/// 获取当前应用权限
+ (LXDAuthorizationType)obtainAuthorization
{
return kAuthorization;
}
/// 更新应用权限
+ (void)updateAuthorization
{
/// 推送
if ([UIApplication sharedApplication].currentUserNotificationSettings.types == UIUserNotificationTypeNone) {
kAuthorization &= (~LXDAuthorizationTypePush);
} else {
kAuthorization |= LXDAuthorizationTypePush;
}
/// 定位
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse) {
kAuthorization |= LXDAuthorizationTypeLocation;
} else {
kAuthorization &= (~LXDAuthorizationTypeLocation);
}
/// 相机
if ([AVCaptureDevice authorizationStatusForMediaType: AVMediaTypeVideo] == AVAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypeCamera;
} else {
kAuthorization &= (~LXDAuthorizationTypeCamera);
}
/// 相册
if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypePhoto;
} else {
kAuthorization &= (~LXDAuthorizationTypePhoto);
}
/// 麦克风
[[AVAudioSession sharedInstance] requestRecordPermission: ^(BOOL granted) {
if (granted) {
kAuthorization |= LXDAuthorizationTypeAudio;
} else {
kAuthorization &= (~LXDAuthorizationTypeAudio);
}
}];
/// 通讯录
if ([UIDevice currentDevice].systemVersion.doubleValue >= 9) {
if ([CNContactStore authorizationStatusForEntityType: CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypeContacts;
} else {
kAuthorization &= (~LXDAuthorizationTypeContacts);
}
} else {
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypeContacts;
} else {
kAuthorization &= (~LXDAuthorizationTypeContacts);
}
}
}
@end
</code></pre></div></div>
<p>在我们需要使用某些授权的时候,例如打开相册时,直接使用<code class="language-plaintext highlighter-rouge">&</code>运算符判断权限即可:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)openCamera {
LXDAuthorizationType type = [LXDAuthObtainTool obtainAuthorization];
if (type & LXDAuthorizationTypeCamera) {
/// open camera
} else {
/// alert
}
}
</code></pre></div></div>
<p>在数据存储的方面位运算拥有着占用内存少,高效率的优点,当然位运算能做的不仅仅是这些,比如笔者项目有这样的一个需求:用户登录成功之后在首页界面请求服务器下载所有金额相关的数据。这个需求最大的问题是:</p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">AFN2.3+</code>版本的请求库不支持同步请求,当需要多个请求任务一次性执行时,判断请求任务完成是很麻烦的一件事情。</p>
</blockquote>
<p>由于<code class="language-plaintext highlighter-rouge">NSInteger</code>拥有8个字节64位的二进制位,因此笔者将每一个二进制位用来表示单个任务请求的完成状态。已知登陆后需要同步数据的接口为<code class="language-plaintext highlighter-rouge">N(<64)</code>个,因此可以声明一个全部请求任务完成后的状态变量:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NSInteger complete = 0;
for (int idx = 0; idx < N; idx++) {
complete |= (1 << idx);
}
</code></pre></div></div>
<p>然后使用一个标志变量<code class="language-plaintext highlighter-rouge">flags</code>用来记录当前任务请求的完成情况,每一个数据同步的任务完成之后对应的二进制位就置为<code class="language-plaintext highlighter-rouge">1</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>__block NSInteger flags = 0;
NSArray<NSString *> * urls = @[......];
NSArray<NSDictionary *> * params = @[......];
for (NSInteger idx = 0; idx < urls.count; idx++) {
NSString * url = urls[idx];
NSDictionary * param = params[idx];
[LXDDataSyncTool syncWithUrl: url params: param complete: ^{
flags |= (1 << idx);
if ( (flags ^ complete) == 0 ) {
[self completeDataSync];
}
}];
}
</code></pre></div></div>
<h2 id="位运算与算法">位运算与算法</h2>
<p>在普遍使用高级语言开发的大环境下,位运算的实现更多的被封装起来,因此大多数开发者在项目开发中不见得会使用这一机制。在上面<code class="language-plaintext highlighter-rouge">基础计算</code>一节中笔者说过两个数相加只需要一个时钟周期(虽然<code class="language-plaintext highlighter-rouge">CPU</code>从寄存器读取存放数据也需要额外的时钟周期,但通常这部分的花销总是常量级,可以忽略不计)</p>
<p>由于位运算的处理基本也在一个时钟周期完成,位运算这一操作备受算法封装者的喜爱。比如交换两个变量的值一般情况下代码是:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int sum = a;
a = b;
b = sum;
</code></pre></div></div>
<p>又或者:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>a = a + b;
b = a - b;
a = a - b;
</code></pre></div></div>
<p>如果通过位运算的方式则不需要任何加减操作或者临时变量:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>a ^= b;
b = a ^ b;
a = a ^ b;
</code></pre></div></div>
<p>上面的代码和第二种方式的实现思路类似,都是将<code class="language-plaintext highlighter-rouge">a</code>和<code class="language-plaintext highlighter-rouge">b</code>合并成单个变量,再分别消除变量中的<code class="language-plaintext highlighter-rouge">a</code>和<code class="language-plaintext highlighter-rouge">b</code>的值(<code class="language-plaintext highlighter-rouge">^</code>运算会对相同二进制位的值置0,意味着<code class="language-plaintext highlighter-rouge">b^b</code>的结果等于0)</p>
<blockquote>
<p>进阶题:找出整型数组中唯一的单独数字,数组中的其他数字的个数为2个</p>
</blockquote>
<p>通过上面不用中间变量交换<code class="language-plaintext highlighter-rouge">a</code>和<code class="language-plaintext highlighter-rouge">b</code>的值可以得出下面的最简代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (int)singleDog(int * nums) {
int singleDog = 0;
for (int idx = 0; idx < sizeof(nums)/sizeof(int); idx++) {
singleDog ^= nums[idx];
}
return singleDog;
}
</code></pre></div></div>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao69">iOS开发之位运算</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on November 07, 2016.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao68
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao68
2016-10-27T00:00:00+08:00
2016-10-27T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p>MVC是软件工程中的一种软件架构模式,它把软件系统分为三个基本的部分:模型Model、视图View以及控制器Controller。这种模式的目的是为了实现一种动态的程序设计,简化后续对软件系统的修改和扩展,并使得程序的某一部分的复用成为可能。</p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/92d086b8efd8">Sindri的小巢</a><br /></p>
<p>文章的标题有点绕口,不过想了半天,想不到更好的标题了。本文的诞生有一部分功劳要归于<a href="http://www.jianshu.com/p/a51d66383eb9">iOS应用现状分析</a>,标题也是来源于原文中的“能把代码职责均衡的划分到不同的功能类里”。如果你看过我的文章,就会发现我是一个<code class="language-plaintext highlighter-rouge">MVC</code>主导开发的人。这是因为开发的项目总是算不上大项目,在合理的代码职责分工后项目能保持良好的状态,就没有使用到其他架构开发过项目(如果你的状态跟笔者差不多,就算不适用其他架构模式,你也应该自己学习)</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao68/1.jpg" alt="1" /><br /></p>
<p>OK,简短来说,在很早之前我就有写这么一篇文章的想法,大致是在当初面试很多iOS开发者的时候这样的对话萌生的念头,下面的对话是经过笔者总结的,切勿对号入座:</p>
<blockquote>
<p>Q: 你在项目中使用了MVVM的架构结构,能说说为什么采用的是这种结构吗?</p>
</blockquote>
<blockquote>
<p>A: 这是因为我们的项目在开发中控制器的代码越来越多,超过了一千行,然后觉得这样控制器的职责太多,就采用一个个ViewModel把这些职责分离出来</p>
</blockquote>
<blockquote>
<p>Q: 能说说你们控制器的职责吗?或者有源码可以参考一下吗?</p>
</blockquote>
<blockquote>
<p>面试者拿出电脑展示源码</p>
</blockquote>
<p>最后的结果就是,笔者不认为面试者需要使用到<code class="language-plaintext highlighter-rouge">MVVM</code>来改进他们的架构,这里当然是见仁见智了。由于对方代码职责的不合理分工导致了<code class="language-plaintext highlighter-rouge">View</code>和<code class="language-plaintext highlighter-rouge">Model</code>层几乎没有业务逻辑,从而导致了控制器的失衡,变得笨重。在这种情况下即便他使用了<code class="language-plaintext highlighter-rouge">ViewModel</code>将控制器的代码分离了出来,充其量只是<code class="language-plaintext highlighter-rouge">将垃圾挪到另一个地方罢了</code>。我在<a href="http://www.jianshu.com/p/4847c9a1e19b">MVC架构杂谈</a>中提到过自身对<code class="language-plaintext highlighter-rouge">MVC</code>三个模块的职责认识,当你想将<code class="language-plaintext highlighter-rouge">MVC</code>改进成<code class="language-plaintext highlighter-rouge">MVX</code>的其他结构时,应当先思考自己的代码职责是不是已经均衡了。</p>
<h2 id="码农小明的项目">码农小明的项目</h2>
<p>在开始之前,还是强烈推荐推荐<code class="language-plaintext highlighter-rouge">《重构-改善既有代码的设计》</code>这本书,一本好书或者好文章应该让你每次观赏时都能产生不同的感觉。</p>
<p>正常来说,造成你代码笨重的最大凶手是重复的代码,例如曾经笔者看过这样一张界面图以及逻辑代码:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao68/2.jpg" alt="1" /><br /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface XXXViewController
@property (weak, nonatomic) IBOutlet UIButton * rule1;
@property (weak, nonatomic) IBOutlet UIButton * rule2;
@property (weak, nonatomic) IBOutlet UIButton * rule3;
@property (weak, nonatomic) IBOutlet UIButton * rule4;
@end
@implementation XXXViewController
- (IBAction)actionToClickRule1: (id)sender {
[_rule1 setSelected: YES];
[_rule2 setSelected: NO];
[_rule3 setSelected: NO];
[_rule4 setSelected: NO];
}
- (IBAction)actionToClickRule2: (id)sender {
[_rule1 setSelected: NO];
[_rule2 setSelected: YES];
[_rule3 setSelected: NO];
[_rule4 setSelected: NO];
}
- (IBAction)actionToClickRule1: (id)sender {
[_rule1 setSelected: NO];
[_rule2 setSelected: NO];
[_rule3 setSelected: YES];
[_rule4 setSelected: NO];
}
- (IBAction)actionToClickRule1: (id)sender {
[_rule1 setSelected: NO];
[_rule2 setSelected: NO];
[_rule3 setSelected: NO];
[_rule4 setSelected: YES];
}
@end
</code></pre></div></div>
<p>别急着嘲笑这样的代码,曾经的我们也写过类似的代码。这就是最直接粗浅的重复代码,所有的重复代码都和上面存在一样的毛病:亢长、无意义、占用了大量的空间。实际上,这些重复的代码总是分散在多个类当中,积少成多让我们的代码变得笨重。因此,在讨论你的项目是否需要改进架构之前,先弄清楚你是否需要消除这些垃圾。</p>
<p>举个例子,小明开发的一款面向B端的应用中允许商户添加优惠活动,包括开始日期和结束日期:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface Promotion: NSObject
+ (instancetype)currentPromotion;
@property (readonly, nonatomic) CGFloat discount;
@property (readonly, nonatomic) NSDate * start;
@property (readonly, nonatomic) NSDate * end;
@end
</code></pre></div></div>
<p>由于商户同一时间只会存在一个优惠活动,小明把活动写成了单例,然后其他模块通过获取活动单例来计算折后价格:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// module A
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
CGFloat discountAmount = _order.amount;
if ([now timeIntervalSinceDate: promotion.start] > 0 && [now timeIntervalSinceDate: promotion.end] < 0) {
discountAmount *= promotion.discount;
}
// module B
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
if ([now timeIntervalSinceDate: promotion.start] > 0 && [now timeIntervalSinceDate: promotion.end] < 0) {
[_cycleDisplayView display: @"全场限时%g折", promotion.discount*10];
}
// module C
...
</code></pre></div></div>
<p>小明在开发完成后优化代码时发现了多个模块存在这样的重复代码,于是他写了一个<code class="language-plaintext highlighter-rouge">NSDate</code>的扩展来简化了这段代码,顺便还添加了一个安全监测:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@implementation NSDate (convenience)
- (BOOL)betweenFront: (NSDate *)front andBehind: (NSDate *)behind {
if (!front || !behind) { return NO; }
return ([self timeIntervalSinceDate: front] > 0 && [self timeIntervalSinceDate: behind] < 0);
}
@end
// module A
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
CGFloat discountAmount = _order.amount;
if ([now betweenFront: promotion.start andBehind: promotion.end]) {
discountAmount *= promotion.discount;
}
// module B
Promotion * promotion = [Promotion currentPromotion];
NSDate * now = [NSDate date];
if ([now betweenFront: promotion.start andBehind: promotion.end]) {
[_cycleDisplayView display: @"全场限时%g折", promotion.discount*10];
}
</code></pre></div></div>
<p>过了一段时间,产品找到小明说:小明啊,商户反映说只有一个优惠活动是不够的,他们需要存在多个不同的活动。小明一想,那么就取消<code class="language-plaintext highlighter-rouge">Promotion</code>的单例属性,增加一个管理单例:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface PromotionManager: NSObject
@property (readonly, nonatomic) NSArray<Promotion *> * promotions
+ (instancetype)sharedManager;
- (void)requestPromotionsWithComplete: (void(^)(PromotionManager * manager))complete;
@end
// module A
- (void)viewDidLoad {
PromotionManager * manager = [PromotionManager sharedManager];
if (manager.promotions) {
[manager requestPromotionsWithComplete: ^(PromotionManager * manager) {
_promotions = manager.promotions;
[self calculateOrder];
}
} else {
_promotions = manager.promotions;
[self calculateOrder];
}
}
- (void)calculateOrder {
CGFloat orderAmount = _order.amount;
for (Promotion * promotion in _promotions) {
if ([[NSDate date] betweenFront: promotion.start andBehind: promotion.end]) {
orderAmount *= promotion.discount;
}
}
}
</code></pre></div></div>
<p>随着日子一天天过去,产品提出的需求也越来越多。有一天,产品说应该让商户可以自由开关优惠活动,于是<code class="language-plaintext highlighter-rouge">Promotion</code>多了一个<code class="language-plaintext highlighter-rouge">isActived</code>是否激活的属性。其他模块的判断除了判断时间还多了判断是否启动了活动。再后来,还添加了一个<code class="language-plaintext highlighter-rouge">synchronize</code>属性判断是否可以与其他活动同时计算判断。最近产品告诉小明活动现在不仅局限于折扣,还新增了固定优惠,以及满额优惠,于是代码变成了下面这样:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface Promotion: NSObject
@property (assign, nonatomic) BOOL isActived;
@property (assign, nonatomic) BOOL synchronize;
@property (assign, nonatomic) CGFloat discount;
@property (assign, nonatomic) CGFloat discountCondition;
@property (assign, nonatomic) DiscountType discountType;
@property (assign, nonatomic) PromotionType promotionType;
@property (readonly, nonatomic) NSDate * start;
@property (readonly, nonatomic) NSDate * end;
@end
// module A
- (void)viewDidLoad {
PromotionManager * manager = [PromotionManager sharedManager];
if (manager.promotions) {
[manager requestPromotionsWithComplete: ^(PromotionManager * manager) {
_promotions = manager.promotions;
[self calculateOrder];
}
} else {
_promotions = manager.promotions;
[self calculateOrder];
}
}
- (void)calculateOrder {
CGFloat orderAmount = _order.amount;
NSMutableArray * fullPromotions = @[].mutableCopy;
NSMutableArray * discountPromotions = @[].mutableCopy;
for (Promotion p in _promotions) {
if (p.isActived && [[NSDate date] betweenFront: p.start andBehind: p.end]) {
if (p.promotionType == PromotionTypeFullPromotion) {
[fullPromotions addObject: p];
} else if (p.promotionType == PromotionTypeDiscount) {
[discountPromotions addObject: p];
}
}
}
Promotion * syncPromotion = nil;
Promotion * singlePromotion = nil;
for (Promotion * p in fullPromotions) {
if (p.synchronize) {
if (p.discountCondition != 0) {
if (p.discountCondition > syncPromotion.discountCondition) {
syncPromotion = p;
}
} else {
if (p.discount > syncPromotion.discount) {
syncPromotion = p;
}
}
} else {
if (p.discountCondition != 0) {
if (p.discountCondition > singlePromotion.discountCondition) {
singlePromotion = p;
}
} else {
if (p.discount > singlePromotion.discount) {
singlePromotion = p;
}
}
}
}
// find discount promotions
......
}
</code></pre></div></div>
<p>这时候模块获取优惠活动信息的代价已经变得十分的昂贵,一堆亢长的代码,重复度高。这时候小明的同事对他说,我们改进一下架构吧,通过<code class="language-plaintext highlighter-rouge">ViewModel</code>把这部分的代码从控制器分离出去。其实这时候<code class="language-plaintext highlighter-rouge">ViewModel</code>的做法跟上面小明直接扩展<code class="language-plaintext highlighter-rouge">NSDate</code>的目的是一样的,在这个时候<code class="language-plaintext highlighter-rouge">View</code>和<code class="language-plaintext highlighter-rouge">Model</code>几乎无作为,基本所有逻辑都在控制器中不断地撑胖它。小明认真思考,完完全全将代码阅览后,告诉同事现在最大的原因在于代码职责混乱,并不能很好的分离到<code class="language-plaintext highlighter-rouge">VC</code>的模块中,解决的方式应该是从逻辑分工下手。</p>
<p>首先,小明发现<code class="language-plaintext highlighter-rouge">Promotion</code>本身除了存储活动信息,没有进行任何的逻辑操作。而控制器中判断活动是否有效以及折扣金额计算的业务理可以由<code class="language-plaintext highlighter-rouge">Promotion</code>来完成:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface Promotion: NSObject
- (BOOL)isEffective;
- (BOOL)isWorking;
- (CGFloat)discountAmount: (CGFloat)amount;
@end
@implementation Promotion
- (BOOL)isEffective {
return [[NSDate date] betweenFront: _start andBehind: _end];
}
- (BOOL)isWorking {
return ( [self isEffective] && _isActived );
}
- (CGFloat)discountAmount: (CGFloat)amount {
if ([self isWorking]) {
if (_promotionType == PromotionTypeDiscount) {
return [self calculateDiscount: amount];
} else {
if (amount < _discountCondition) { return amount; }
return [self calculateDiscount: amount];
}
}
return amount;
}
#pragma mark - Private
- (CGFloat)calculateDiscount: (CGFloat)amount {
if (_discountType == DiscountTypeCoupon) {
return amount - _discount;
} else {
return amount * _discount;
}
}
@end
</code></pre></div></div>
<p>除此之外,小明发现先前封装的活动管理类<code class="language-plaintext highlighter-rouge">PromotionManager</code>本身涉及了网络请求和数据管理两个业务,因此需要将其中一个业务分离出来。于是网络请求封装成<code class="language-plaintext highlighter-rouge">PromotionRequest</code>,另一方面原有的数据管理只有获取数据的功能,因此增加增删改以及对活动进行初步筛选的功能:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#pragma mark - PromotionManager.h
@class PromotionManager;
typeof void(^PromotionRequestComplete)(PromotionManager * manager);
@interface PromotionRequest: NSObject
+ (void)requestPromotionsWithComplete: (PromotionRequestComplete)complete;
+ (void)insertPromotion: (Promotion *)promotion withComplete: (PromotionRequestComplete)complete;
+ (void)updatePromotion: (Promotion *)promotion withComplete: (PromotionRequestComplete)complete;
+ (void)deletePromotion: (Promotion *)promotion withComplete: (PromotionRequestComplete)complete;
@end
@interface PromotionManager: NSObject
+ (instancetype)sharedManager;
- (NSArray<Promotion *> *)workingPromotions;
- (NSArray<Promotion *> *)effectivePromotions;
- (NSArray<Promotion *> *)fullPromotions;
- (NSArray<Promotion *> *)discountPromotions;
- (void)insertPromotion: (Promotion *)promotion;
- (void)updatePromotion: (Promotion *)promotion;
- (void)deletePromotion: (Promotion *)promotion;
@end
#pragma mark - PromotionManager.m
@interface PromotionManager ()
@property (nonatomic, strong) NSArray<Promotion *> * promotions;
@end
@implementation PromotionManager
+ (instancetype)sharedManager { ... }
- (NSArray<Promotion *> *)fullPromotions {
return [self filterPromotionsWithType: PromotionTypeFullPromote];
}
- (NSArray<Promotion *> *)discountPromotions {
return [self filterPromotionsWithType: PromotionDiscountPromote];
}
- (NSArray<Promotion *> *)workingPromotions {
return _promotions.filter(^BOOL(Promotion * p) {
return (p.isWorking);
});
}
- (NSArray<Promotion *> *)effectivePromotions {
return _promotions.filter(^BOOL(Promotion * p) {
return (p.isEffective);
});
}
- (NSArray<Promotion *> *)filterPromotionsWithType: (PromotionType)type {
return [self workingPromotions].filter(^BOOL(Promotion * p) {
return (p.promotionType == type);
});
}
- (void)insertPromotion: (Promotion *)promotion {
if ([_promotions containsObject: promotion]) {
[PromotionRequest updatePromotion: promotion withComplete: nil];
} else {
[PromotionRequest insertPromotion: promotion withComplete: nil];
}
}
- (void)updatePromotion: (Promotion *)promotion {
if ([_promotions containsObject: promotion]) {
[PromotionRequest updatePromotion: promotion withComplete: nil];
}
}
- (void)deletePromotion: (Promotion *)promotion {
if ([_promotions containsObject: promotion]) {
[PromotionRequest deletePromotion: promotion withComplete: nil];
}
}
- (void)obtainPromotionsFromJSON: (id)JSON { ... }
@end
</code></pre></div></div>
<p>最后,小明发现其他模块在寻找最优惠活动的逻辑代码非常的多,另外由于存在满额优惠和普通优惠两种活动,进一步加大了代码量。因此小明新建了一个计算类<code class="language-plaintext highlighter-rouge">PromotionCalculator</code>用来完成查找最优活动和计算最优价格的接口:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface PromotionCalculator: NSObject
+ (CGFloat)calculateAmount: (CGFloat)amount;
+ (Promotion *)bestFullPromotion: (CGFloat)amount;
+ (Promotion *)bestDiscountPromotion: (CGFloat)amount;
@end
@implementation PromotionCalculator
+ (CGFloat)calculateAmount: (CGFloat)amount {
Promotion * bestFullPromotion = [self bestFullPromotion: amount];
Promotion * bestDiscountPromotion = [self bestDiscountPromotion: amount];
if (bestFullPromotion.synchronize && bestDiscountPromotion.synchronize) {
return [bestFullPromotion discountAmount: [bestDiscountPromotion discountAmount: amount]];
} else {
return MAX([bestDiscountPromotion discountAmount: amount], [bestFullPromotion discountAmount: amount]);
}
}
+ (Promotion *)bestFullPromotion: (CGFloat)amount {
PromotionManager * manager = [PromotionManager sharedManager];
return [self bestPromotionInPromotions: [manager fullPromotions] amount: amount];
}
+ (Promotion *)bestDiscountPromotion: (CGFloat)amount {
PromotionManager * manager = [PromotionManager sharedManager];
return [self bestPromotionInPromotions: [manager discountPromotions] amount: amount];
}
+ (Promotion *)bestPromotionInPromotions: (NSArray *)promotions amount: (CGFloat)amount {
CGFloat discount = amount;
Promotion * best = nil;
for (Promotion * promotion in promotions) {
CGFloat tmp = [promotion discountAmount: amount];
if (tmp < discount) {
discount = tmp;
best = promotion;
}
}
return best;
}
@end
</code></pre></div></div>
<p>当这些代码逻辑被小明分散到各处之后,小明惊讶的发现其他模块在进行计算时剩下几行代码而已:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)viewDidLoad {
[PromotionRequest requestPromotionsWithComplete: ^(PromotionManager * manager) {
_discountAmount = [PromotionCalculator calculateAmount: _order.amount];
}];
} 这时候代码职责的结构图,小明成功的均衡了不同组件之间的代码职责,避免了改变项目原架构带来的风险以及不必要的工作:
</code></pre></div></div>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao68/3.jpg" alt="1" /><br /></p>
<h2 id="尾语">尾语</h2>
<p>这是第二篇讲<code class="language-plaintext highlighter-rouge">MVC</code>的文章,仍然要告诉大家的是<code class="language-plaintext highlighter-rouge">MVC</code>确确实实存在着缺陷,这个缺陷会在项目变得很大的时候暴露出来(笔者没有开发过大型项目的弱鸡),如果你的项目结构分层做的足够完善的话,那么该改进更换架构的时候就不要犹豫。但千万要记住,如果仅仅是因为重复了太多的无用代码,又或者是逻辑全部塞到控制器中,那么更换架构无非是将垃圾再次分散罢了。</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao68">iOS开发之均衡代码职责浅谈</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 27, 2016.</p>
//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu02
//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu02
2016-10-19T00:00:00+08:00
2016-10-19T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/1.png" alt="1" /><br /></p>
<p><a href="http://www.jianshu.com/p/ccf40205300f">微信小程序开发教程-从零开始(1)</a></p>
<p><a href="http://www.jianshu.com/p/bfd8c8d91c7d">微信小程序开发教程-从零开始(2)</a></p>
<p>前俩章中我们学会了怎么搭建一个微信小程序的框架以及显示一个文章列表,这篇文章我将讲解列表的网络请求以及网络数据的对接。</p>
<p>首先找到我们的index.js文件,然后看看<a href="https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-request.html?t=1476197482156">微信小程序的网络请求文档</a>很轻松的就可以找到我们的示例代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wx.request({
url: 'test.php',
data: {
x: '' ,
y: ''
},
header: {
'Content-Type': 'application/json'
},
success: function(res) {
console.log(res.data)
}
})
</code></pre></div></div>
<blockquote>
<p>url为我们需要请求的接口</p>
</blockquote>
<blockquote>
<p>data为我们的请求参数</p>
</blockquote>
<blockquote>
<p>header为设置请求的 header , header 中不能设置 Referer</p>
</blockquote>
<blockquote>
<p>success收到开发者服务成功返回的回调函数,res = {data: ‘开发者服务器返回的内容’}</p>
</blockquote>
<blockquote>
<p>console.log( res.data )为打印请求下来的数据</p>
</blockquote>
<p>默认为get请求,在此我们就用默认的请求方式,具体的代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>onLoad: function () {
console.log('onLoad')
var that = this
wx.request( {
url: 'http://sep9.cn/qt5wix',
data: {},
header: {
'Content-Type': 'application/json'
},
success: function( res ) {
console.log( res.data )
}
})
}
</code></pre></div></div>
<p>运行一下看看我们的请求是否有数据,结果如下图:</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161019/1.png" alt="1" /><br /></p>
<p>可以看出我们的数据请求已经是成功的,是不是非常的简单啊?😄下面我们再来看看怎么给相应的UI赋值吧。</p>
<p>首先在我们网络成功的地方加上以下代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>success: function( res ) {
console.log( res.data )
that.setData( {
})
}
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">that.setData( { })</code>这个方法主要是用来赋值的</p>
<p>然后我们得到的数据为<code class="language-plaintext highlighter-rouge">res.data</code>通过打印我们可以看出我们的数据结构和原来写死的数据结构是一样的,但是里面的字段确不一样,因此,我们需要把请求下来的值赋值给我们原来的数据源,然后把原有的数据源的字段改成网络请求下来的字段最终的代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//index.js
//获取应用实例
var app = getApp()
Page({
data: {
newList:[
]
},
//事件处理函数
bindViewTap: function() {
wx.navigateTo({
url: '../logs/logs'
})
},
onLoad: function () {
console.log('onLoad')
var that = this
wx.request( {
url: 'http://sep9.cn/qt5wix',
data: {},
header: {
'Content-Type': 'application/json'
},
success: function( res ) {
console.log( res.data )
that.setData( {
newList: res.data
})
}
})
}
})
</code></pre></div></div>
<p>再把index.wxml中赋值的字段改成服务器返回相应的字段,运行结果如下图:</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161019/2.gif" alt="1" /><br /></p>
<p>不知道什么原因,我这接口返回的图片url在微信小程序中无法显示,为了让效果更加的接近我们的效果图,在本地给我们的数据源加了些网络上的图片,代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
data: {
newList:[{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"} ,
{fistImg:"http://a.hiphotos.baidu.com/image/pic/item/c8ea15ce36d3d539be4d77b83f87e950352ab05c.jpg"} ,
{fistImg:"http://h.hiphotos.baidu.com/image/pic/item/8d5494eef01f3a2922e765c99b25bc315c607c8d.jpg"} ,
{fistImg:"http://c.hiphotos.baidu.com/image/pic/item/3b292df5e0fe9925ae23d95736a85edf8db1718d.jpg"} ,
{fistImg:"http://g.hiphotos.baidu.com/image/pic/item/faedab64034f78f099a529f47b310a55b3191c0e.jpg"} ,
{fistImg:"http://g.hiphotos.baidu.com/image/pic/item/bd315c6034a85edf9ba34e244b540923dd54758d.jpg"} ,
{fistImg:"http://f.hiphotos.baidu.com/image/pic/item/00e93901213fb80e0ee553d034d12f2eb9389484.jpg"} ,
{fistImg:"http://img1.imgtn.bdimg.com/it/u=2955244448,132069077&fm=21&gp=0.jpg"} ,
{fistImg:"http://image.tianjimedia.com/uploadImages/2014/127/32/VP974HZ0AXL2.jpg"} ,
{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"} ,
{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"} ,
{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"} ,
{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"} ,
{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"} ,
{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"} ,
{fistImg:"http://img0.imgtn.bdimg.com/it/u=1640246403,1832676351&fm=21&gp=0.jpg"}
]
}
</code></pre></div></div>
<p>随便弄几张图了,看看效果如何,😄</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161019/3.gif" alt="1" /><br /></p>
<p>本来还想做下详情页的,由于接口的详情是H5 ,貌似微信小程序不能直接加载H5,如有知道的朋友也可以给我留言告诉我,本人对于H5也是一窍不通😄。</p>
<p><a href="https://github.com/AllLuckly/IT-Blog_Wechat">demo下载</a></p>
<p>本文为<a href="blog.allluckly.cn">Bison</a>原创,转载请注明出处,否则将追究法律责任</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu02">微信小程序开发教程-从零开始(3)</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 19, 2016.</p>
//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu01
//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu01
2016-10-18T00:00:00+08:00
2016-10-18T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/1.png" alt="1" /><br /></p>
<p>从<a href="http://www.jianshu.com/p/ccf40205300f">微信小程序开发教程-从零开始(1)</a></p>
<p>中我们学会了怎么搭建一个微信小程序的框架以及显示一个文章列表,这篇文章我将讲解列表的点击以及UI的优化,达到一个我们预期的一种效果。</p>
<p>首先我们创建一个详情的界面所需的文件,如下图所示:</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161018/1.png" alt="1" /><br /></p>
<p>详情页的话,我们暂时随便搭建一下,主要是看下怎么做跳转。</p>
<p>首先我们在详情页面随便写点东西,代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><!--detail.wxml-->
<view class="container">
666666
</view>
</code></pre></div></div>
<p>这样的话在外面的详情页会显示666666这些个字样,然后我们在index.wxml中写跳转的代码,主要代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><navigator url="navigate?title=navigate" hover-class="navigator-hover">
跳转到新页面
</navigator>
</code></pre></div></div>
<p>其中<code class="language-plaintext highlighter-rouge">url</code>为应用内的跳转链接<code class="language-plaintext highlighter-rouge">title=navigate</code>为传过去的字段<code class="language-plaintext highlighter-rouge">hover-class</code>指定点击时的样式类,当hover-class=”none”时,没有点击态效果.完整的代码如下图框起来的地方</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161018/2.png" alt="1" /><br /></p>
<p>运行一下,可以瞧瞧效果如下。</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161018/3.gif" alt="1" /><br /></p>
<p>由图可以看出跳转的功能已经做好了,下面我们开始优化一下首页的UI
优化UI 的话主要是在<code class="language-plaintext highlighter-rouge">index.wxss</code>中,根据每个控件的class名来写相应的设置。
首先我们把整个页面做一下设置,代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/**index.wxss**/
.list {
height: 100%;
display: flex;
flex-direction: column;
padding: 20rpx;
}
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">padding</code>为内边框,首页整个的class名为list,所以用.list{}来设置。然后设置一下navigator块,再然后来设置我们的每一个列表,在这里我把它命名为cell,看上去对于iOS开发来说亲切一点。具体代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>navigator{ overflow: hidden;}
.cell{
margin-bottom: 20rpx;
height: 200rpx;
position: relative;
}
</code></pre></div></div>
<p>再然后我们设置cell中图片的位置,具体代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.imgs{
float: right;
}
.imgs image {
display: block;
width: 200rpx;
height: 200rpx;
}
</code></pre></div></div>
<p>由代码可以看出,我们的图片设置了向右对齐,宽和高分别设置了200rpx,我们运行一下看看效果图片是否已经改变了。</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161018/4.png" alt="1" /><br /></p>
<p>相对于我们的效果图的图片部分,应该已经差不多就是这个样子了,下面让我们再调一调标题以及其他部分UI 的调试。
我们把其他部分的UI都放在class=”infos”; 首先我们先调这一大块的布局,代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.infos {
float: left;
width: 480rpx;
height: 200rpx;
padding: 20rpx 0 0 20rpx;
}
</code></pre></div></div>
<p>然后设置里面的每一个小部件,代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.title {font-size: 20px;}
.date {
padding-top: 50rpx;
float: right;
font-size: 16px;
color: #aaa;
position: relative;
}
.classification{
padding-top: 50rpx;
font-size: 16px;
color: #aaa;
position: relative;
}
</code></pre></div></div>
<p>最后我们运行一下看下结果如何:</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161018/5.gif" alt="1" /><br /></p>
<p>有空的话再写写网络请求方面的,结合这个<a href="https://github.com/AllLuckly/IT-Blog_Wechat">demo</a>写</p>
<p>本文为<a href="blog.allluckly.cn">Bison</a>原创,转载请注明出处,否则将追究法律责任</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu01">微信小程序开发教程-从零开始(2)</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 18, 2016.</p>
//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu
//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu
2016-10-17T00:00:00+08:00
2016-10-17T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/1.png" alt="1" /><br /></p>
<h2 id="前言">前言</h2>
<p>微信小程序暂时处于内测期间,公司大的版本刚好上线了,闲来无事,看看微信小程序的文档,顺便学习学习,在此希望和大家一起共勉,发现自己越来越懒惰了,越活越没上进心了,有点危险,给自己敲下警钟吧。废话不多说,开始记录下这些天学习到的一些知识,希望对正在阅读的你有所帮助!
本文为iOS开发者<a href="blog.allluckly.cn">Bison</a>自学微信小程序所写,所以很多东西都和iOS进行了一下对比。</p>
<h2 id="开搞">开搞</h2>
<blockquote>
<p>创建项目在此滤过,相信大家看着官方文档就可以搞定</p>
</blockquote>
<h3 id="首先我们先把整个app的架构搭起来">首先我们先把整个app的架构搭起来</h3>
<p>一般市面上的app都已tabbar展示的方式为主,今天我就仿原生的<a href="https://itunes.apple.com/cn/app/it-blog-ios-kai-fa-zhe-wen/id1067787090?mt=8">IT Blog</a>下面让我们看下<a href="https://itunes.apple.com/cn/app/it-blog-ios-kai-fa-zhe-wen/id1067787090?mt=8">IT blog</a>长什么样吧!<br /></p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/2.jpg" alt="1" /><br /></p>
<h4 id="首先是tabbar">首先是tabbar</h4>
<p>下面我将简单的介绍一下微信小程序一些不可缺的目录结构。
<img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/3.png" alt="1" /><br /></p>
<ul>
<li>下面借用官方的解释</li>
</ul>
<h5 id="wxssweixin-style-sheets是一套样式语言用于描述-wxml-的组件样式">WXSS(WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式。</h5>
<p>WXSS 用来决定 WXML 的组件应该怎么显示。为了适应广大的前端开发者,我们的 WXSS 具有 CSS 大部分特性。 同时为了更适合开发微信小程序,我们对 CSS 进行了扩充以及修改。</p>
<h4 id="appjson-文件来对微信小程序进行全局配置决定页面文件的路径窗口表现设置网络超时时间设置多-tab-等">app.json 文件来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。</h4>
<p>文件来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。相当于iOS开发中的AppDelegate</p>
<h4 id="appjs-是小程序逻辑部分">app.js 是小程序逻辑部分</h4>
<p>根据上面的目录结构的解释不难看出,我们的tabbar是写在哪的,没错就是app.json,下面让我们看下代码</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"pages":[
"pages/index/index",
"pages/logs/logs"
],
"window":{
"backgroundTextStyle":"light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle":"black"
},
"tabBar": {
"backgroundColor":"#000000",
"list": [{
"iconPath":"image/icon_API.png",
"selectedIconPath":"image/icon_API_HL.png",
"pagePath": "pages/index/index",
"text": "首页"
}, {
"iconPath":"image/icon_component.png",
"selectedIconPath":"image/icon_component_HL.png",
"pagePath": "pages/about/about",
"text": "我的"
}]
}
}
</code></pre></div></div>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/4.png" alt="1" /><br /></p>
<p>上图框出来的地方就是我们的tabbar,tabbar里面可以传一个数组list,想显示多少个tab就到这里加就行,当然个数是有限制的最多5个,一个的话就没必要了。这点和iOS开发里面颇为相似。
下面我们按下com + s 再 编译一下,就可以看到如下结果了</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/5.png" alt="1" /><br /></p>
<p>iconPath为默认图片路径,selectedIconPath为点击时的图片路径,text的话我想不说大家也已经猜到了,对,没错就是图片下面显示的title了。</p>
<p>我们这暂时只写了俩个Tab,在此主要也就是实现我们的首页效果。
由我们的效果图可以看出,iOS开发中我们的布局主要用的是tabview,而在微信小程序中类似tabviewCell的布局又是怎么写的呢?下面我们先写贴下代码再做下解说。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><!--文章列表模板 begin-->
<template name="itmes">
<view class="imgs"><image src="" class="in-img" background-size="cover" model="scaleToFill"></image></view>
<view class="infos">
<view class="title"></view>
<view class="date"></view>
<view class="classification"></view>
</view>
</template>
<!--文章列表模板 begin-->
</code></pre></div></div>
<p>在这段代码中<code class="language-plaintext highlighter-rouge"><template name="items"></code>…<code class="language-plaintext highlighter-rouge"></template></code>是微信小程序中的模板,那什么是模板呢?官方文档是这样解释的。</p>
<h4 id="模板">模板</h4>
<p>WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用。</p>
<p>我的理解这个相当于iOS开发中的cell,cell有了的话, 那就只缺少一个数据源了,下面我们暂时做一个本地的数据源。</p>
<p>数据主要是写在js代码当中的,下面来看下代码</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//index.js
//获取应用实例
var app = getApp()
Page({
data: {
newList:[{url:"baidu.com",title:"sdsadsadasdas",classification:"ss",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdassss",classification:"ss",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"12",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"333",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"44",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"44",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"32",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"123",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"456",time:"2016-10-17",imgURL:"../../image/wechatHL.png"},
{url:"baidu.com",title:"sdsadsadasdas",classification:"454",time:"2016-10-17",imgURL:"../../image/wechatHL.png"}
]
},
//事件处理函数
bindViewTap: function() {
wx.navigateTo({
url: '../logs/logs'
})
}
})
</code></pre></div></div>
<p>其中的<code class="language-plaintext highlighter-rouge">newList</code>为我们的数据源,数组里面包含多个字典,字典里面有我们所需要的5个字段。
cell 和数据源都有了,那就只差一个显示了, 显示在微信小程序当中用的是<code class="language-plaintext highlighter-rouge">列表渲染</code></p>
<h4 id="列表渲染">列表渲染</h4>
<p>在组件上使用wx:for控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。
默认数组的当前项的下标变量名默认为index,数组当前项的变量名默认为item</p>
<p>下面我们来看看这个列表渲染是怎么做的,首先切换到index.wxml中,代码如下。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><!--循环渲染列表 begin-->
<block wx:for="">
<template is="itmes" data="" />
</block>
<!--循环渲染列表 end-->
</code></pre></div></div>
<p>com + s 再 编译一下可以看到如下的效果</p>
<p><img src="//allluckly.cn/images/blog/weixinxiaochengxu/20161017/6.gif" alt="1" /><br /></p>
<p>到此微信小程序的列表功能已经做完了,当然我们看到的布局都是很乱的,下一篇我们将优化UI让他和我们的效果图一样。</p>
<p>本文为<a href="blog.allluckly.cn">Bison</a>原创,转载请注明出处,否则将追究法律责任</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/weixinxiaochengxu">微信小程序开发教程-从零开始(1)</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 17, 2016.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao67
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao67
2016-10-13T00:00:00+08:00
2016-10-13T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="//allluckly.cn/images/blog/tuogao/tougao66/1.png" alt="1" /><br /></p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/6c9423672d54">Sindri的小巢</a><br /></p>
<p>设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。GoF提出了23种设计模式,本系列将使用Swift语言来实现这些设计模式</p>
<h2 id="概述">概述</h2>
<p>整个应用生命周期中,只存在唯一一个实例对象的类被称作单例,所以的模块共同使用这一个对象的设计叫做单例模式</p>
<p>单例模式Singleton具有如下优点:</p>
<p>多个模块共用同一个对象,保证了数据的唯一性
统一逻辑功能,具有较高的灵活性
在iOS开发中,本身也会接触到不少的系统单例,例如NSNotificaitonCenter通知中心、UIApplication应用单例等类,在swift中主要使用两种方式进行单例的创建,通常我将用户数据存储为单例方便不同模块访问:</p>
<ul>
<li>方式1,类内部声明静态常量并私有化构造器方法</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
class UserInfo {
static let sharedInfo = UserInfo()
private init() {}
}
</code></pre></div></div>
<ul>
<li>方式2,使用全局常量对象</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
let singleton = UserInfo()
class UserInfo {
class var sharedInfo : UserInfo {
return singleton
}
}
</code></pre></div></div>
<p>对于从OC转过来的开发者而言,dispatch_once创建的单例更符合他们的习惯,但在Swift3.0后,这个方法本身已经无法使用了,苹果在static let修饰变量的实现中已经使用了dispatch_once的方式保证了变量只存在一份。</p>
<h2 id="总结">总结</h2>
<p>单例保证了数据在应用运行期间的唯一性,减少了重复内存的损耗,但如果单例本身内存占用过大时,又是一种负担。另一方面,单例的访问也存在着多线程安全的问题,这需要我们合理的使用线程锁来保证单例的稳定性。</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao67">Swift3.0学习系列之Swift实战-单例模式</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 13, 2016.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao66
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao66
2016-10-13T00:00:00+08:00
2016-10-13T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><img src="//allluckly.cn/images/blog/tuogao/tougao66/1.png" alt="1" /><br /></p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/25543ae95293">Sindri的小巢</a><br /></p>
<p>设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。GoF提出了23种设计模式,本系列将使用Swift语言来实现这些设计模式</p>
<h2 id="概述">概述</h2>
<p>通过共享已存在的对象,减少创建对象内存开销的设计模式被称作享元模式</p>
<p>享元模式Flyweight和单例模式Singleton像是一对孪生兄弟,二者的表现方式非常相似,但二者的存在目的却不一样:</p>
<ul>
<li>单例模式</li>
</ul>
<p>保证了整个应用声明周期内,同一个对象只会存在一份内存,并且任何时间都能被访问使用。
<img src="//allluckly.cn/images/blog/tuogao/tougao66/2.jpg" alt="1" /><br /></p>
<ul>
<li>享元模式</li>
</ul>
<p>如果存在可以复用的对象,那么对象将被共享而不是创建新的对象</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao66/3.jpg" alt="1" /><br /></p>
<p>在iOS开发中,享元模式的最佳实践就是UITableView的复用机制——超出屏幕外的单元格统一被回收放到一个复用队列之中,等待着需要新的单元格时进行复用。</p>
<h2 id="实战">实战</h2>
<p>笔者最近项目有一个需求,几乎所有的数据都要保存在本地。由于应用的特殊性,模块之间需要用到彼此的数据,如果使用单例模式来做,那么同一时间占用的内存是非常的大的,因此以享元模式的思想封装了一个数据管理类:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class DataManager {
//MARK: - Variable
private static var shareStorage = [String: AnyObject]()
private var storeKey = "DefaultKey"
private var storeData = [AnyObject]()
var data: [AnyObject] {
get {
return storeData
}
}
//MARK: - Operate
func insert(data: AnyObject) { }
func delete(at index: Int) { }
func save() { }
}
</code></pre></div></div>
<p>笔者以数据模型的类名作为数据管理的关键字,因此创建一个私有的静态字典用来保存当前正在使用的数据。由于数据以加密的方式存储在沙盒目录下,在数据量足够大的时候,从本地读取这些数据会占用大量的花销,因此在数据管理对象被创建的时候需要判断是否存在可复用的数据,如果不存在再从本地加载:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class DataManager {
init() {
initalizeData()
}
init(storeKey: String) {
self.storeKey = storeKey
initalizeData()
}
private func initalizeData() {
if let data = DataManager.shareStorage[storeKey] {
storeData = data as! [AnyObject]
} else {
loadData()
DataManager.shareStorage[storeKey] = storeData as AnyObject?
}
}
private func loadData() {
// load data from local path
}
}
</code></pre></div></div>
<p>ok,对于数据的复用已经完成了,剩下的问题是不可能让字典一直存储这些数据,否则直接使用单例要更加方便的多。对此,笔者使用了计数功能,保证数据可以在没有使用的时候进行本地存储然后释放:</p>
<p>class DataManager {</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>deinit {
let count = DataManager.shareStorage[countKey()] as! Int
if count == 1 {
save()
DataManager.shareStorage[storeKey] = nil
} else {
DataManager.shareStorage[countKey()] = (count - 1) as AnyObject?
}
}
private func initalizeData() {
if let data = DataManager.shareStorage[storeKey] {
let count = DataManager.shareStorage[countKey()] as! Int
DataManager.shareStorage[countKey()] = (count + 1) as AnyObject?
storeData = data as! [AnyObject]
} else {
loadData()
DataManager.shareStorage[countKey()] = 1 as AnyObject
DataManager.shareStorage[storeKey] = storeData as AnyObject?
}
}
private func countKey() -> String {
return "\(storeKey)Count"
} }
</code></pre></div></div>
<p>上面的代码是初步的逻辑搭建,下一步还需要考虑线程安全等其他问题,这里就不再写出来了</p>
<h2 id="总结">总结</h2>
<p>最开始接触享元模式概念的时候,笔者是有些混乱的,也不清楚它和单例的区别。简单来说,这是一种提供了一种拥有单例优点、以及改善了一部分单例缺点的设计模式,但是享元模式更加的复杂,在考虑到多线程的环境下,数据竞争要比单例激烈的多,也危险的多。</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao66">Swift3.0学习系列之Swift实战-享元模式</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 13, 2016.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao65
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao65
2016-10-12T00:00:00+08:00
2016-10-12T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<blockquote>
<p>更新晚啦,我要先跟大家说声抱歉哈~</p>
</blockquote>
<p>如果大家还没有看我的这两篇文章,建议还是先阅读一下,循序渐进么~文章链接如下:
<a href="http://www.jianshu.com/p/f5337e8f336d">iOS开发 iOS10推送必看(基础篇)</a><br />
<a href="http://www.jianshu.com/p/3d602a60ca4f">iOS开发 iOS10推送必看(高阶1)</a><br />
这次的最后,终于有demo咯~。</p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/f77d070a8812">徐不同</a><br /></p>
<blockquote>
<p>在这篇文章,我会给大家讲一讲更高级一点的,定制化更高的远程通知。其中会补充我之前没讲的远程推送(多媒体)通知,以及UNNotificationServiceExtension,UNNotificationContentExtension,UNNotificationAction的相关类。相信大家看了这篇文章,虽不能说对苹果的远程推送了如指掌,但是也可以做一些基本的扩展咯~</p>
</blockquote>
<h2 id="1unnotificationserviceextension">1、UNNotificationServiceExtension</h2>
<h3 id="11unnotificationserviceextension简介">1.1、UNNotificationServiceExtension简介</h3>
<p>UNNotificationServiceExtension是iOS10推出后的一个新特性,先看这张图:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/1.png" alt="1" /><br /></p>
<p>从这张图上,我们可以看到,原先直接从APNs推送到用户手机的消息,中间添加了ServiceExtension处理的一个步骤(当然你也可以不处理),通过这个ServiceExtension,我们可以把即将给用户展示的通知内容,做各种自定义的处理,最终,给用户呈现一个更为丰富的通知,当然,如果网络不好,或者附件过大,可能在给定的时间内,我们没能将通知重新编辑,那么,则会显示发过来的原版通知内容。</p>
<p>那么ServiceExtension可以做什么呢?它的意义是什么呢?我总结了几点:</p>
<p>1、 安全</p>
<p>安全总是摆在第一位的,从iOS9开始,苹果鼓励我们使用更为安全的https协议可以看的出来,苹果公司是对安全很重视的一家公司,为什么在这里我会提到安全呢?是因为之前我们的推送内容,不管是通过第三方,还是通过苹果自带的通知处理,如果让有心人对数据做一次拦截,抓个包啥的,我们推送的内容就会完全暴露,当然有的同学说,我可以加密啊~但是不知道大家有没有想过,如果数据加密,那通知栏会怎么展示呢?(你千万别跟我说你把所有的远程推送变成本地通知。。)通过此次这次增加的UNNotificationServiceExtension的类,便可以更好的帮助我们实现数据的加密。
它的原理便是在收到通知后的最多30s内,你可以把你的通知内容,解密后,在重新展示在用户的通知拦上。</p>
<p>2、 内容的丰富</p>
<p>之前的通知展示内容比较少,以至于被各种广告提醒占据了。这次苹果新添加的附件通知,结合上通知拓展类,便可以给用户展现出一个有着丰富内容的通知。比如,一个小短片的某一秒的画面啊(这里要强烈鄙视各大平台的某些电影预览图),又或者是配上一些小图片啊(通过服务器传来的imaUrl)等方式来吸引用户,诱导用户点开你的通知,促使用户会使用你的App。其实推送这个功能,虽然有的人会关闭,但是大部分的人还是开启的,所以说推送这个市场还是很大的哟~灵活利用推送,会让你的程序拥有无限的可能。</p>
<h3 id="12如何新建一个unnotificationserviceextension">1.2、如何新建一个UNNotificationServiceExtension</h3>
<p>首先,我们不能通过创建UNNotificationServiceExtension的类来使用服务扩展,我们应当创建一个Target,这个Target自带一个模板,其中有2个方法是系统会自己调用的,如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 你需要通过重写这个方法,来重写你的通知内容,也可以在这里下载附件内容
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *contentToDeliver))contentHandler;
// 如果处理时间太长,等不及处理了,就会把收到的apns直接展示出来
- (void)serviceExtensionTimeWillExpire;
</code></pre></div></div>
<p>开始跟着我创建一个UNNotificationServiceExtension吧。</p>
<h3 id="新建target">新建Target</h3>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/2.png" alt="1" /><br /></p>
<p>选择如图所示:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/3.png" alt="1" /><br /></p>
<p>然后写名字,下一步,即可</p>
<p>此时我们的目录结构里面,已经多出了一个文件夹了。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/4.png" alt="1" /><br /></p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/5.png" alt="1" /><br /></p>
<p>都多了一个myTest。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/6.jpg" alt="1" /><br /></p>
<p>注意看上图,这里的bundleID是你的工程名字的bundleID加上.名称。
不要修改,系统创建的时候就创建好了,不过我还是给大家说一下这个格式
如果你的工程的BundleID是coderxu.pushDemo,则这个扩展的BundleID就是coderxu.pushDemo.mytest,最后的后缀,是看咱们创建服务扩展时候的名字。其他的小细节,大家可以看看。
到这一步,我们就新建了一个服务通知类的扩展。</p>
<h3 id="13如何使用以及相关demo">1.3、如何使用以及相关Demo</h3>
<p>在使用这个类的时候,我重写了以下代码,大家可以先看下:
(1). 这是处理通知内容重写的方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
// copy发来的通知,开始做一些处理
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
// 重写一些东西
self.bestAttemptContent.title = @"我是标题";
self.bestAttemptContent.subtitle = @"我是子标题";
self.bestAttemptContent.body = @"来自徐不同";
// 附件
NSDictionary *dict = self.bestAttemptContent.userInfo;
NSDictionary *notiDict = dict[@"aps"];
NSString *imgUrl = [NSString stringWithFormat:@"%@",notiDict[@"imageAbsoluteString"]];
if (!imgUrl.length) {
self.contentHandler(self.bestAttemptContent);
}
[self loadAttachmentForUrlString:imgUrl withType:@"png" completionHandle:^(UNNotificationAttachment *attach) {
if (attach) {
self.bestAttemptContent.attachments = [NSArray arrayWithObject:attach];
}
self.contentHandler(self.bestAttemptContent);
}];
}
</code></pre></div></div>
<p>(2). 这是下载附件通知的方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> - (void)loadAttachmentForUrlString:(NSString *)urlStr
withType:(NSString *)type
completionHandle:(void(^)(UNNotificationAttachment *attach))completionHandler{
__block UNNotificationAttachment *attachment = nil;
NSURL *attachmentURL = [NSURL URLWithString:urlStr];
NSString *fileExt = [self fileExtensionForMediaType:type];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session downloadTaskWithURL:attachmentURL
completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) {
if (error != nil) {
NSLog(@"%@", error.localizedDescription);
} else {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *localURL = [NSURL fileURLWithPath:[temporaryFileLocation.path stringByAppendingString:fileExt]];
[fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];
NSError *attachmentError = nil;
attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:localURL options:nil error:&attachmentError];
if (attachmentError) {
NSLog(@"%@", attachmentError.localizedDescription);
}
}
completionHandler(attachment);
}] resume];
}
</code></pre></div></div>
<p>(3)判断文件类型的方法</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (NSString *)fileExtensionForMediaType:(NSString *)type {
NSString *ext = type;
if ([type isEqualToString:@"image"]) {
ext = @"jpg";
}
if ([type isEqualToString:@"video"]) {
ext = @"mp4";
}
if ([type isEqualToString:@"audio"]) {
ext = @"mp3";
}
return [@"." stringByAppendingString:ext];
}
</code></pre></div></div>
<p>第一段代码主要讲通知内容的重组,逻辑就是有附件的url,我就下载,如果没有url我就直接展示通知。
第二段代码主要讲的是,用系统自带类,下载图,存图,找到filePath,创建通知的附件内容。(创建附件的url,必须是一个文件路径,也就是说,必须下载下,才能获取文件路径,开头是file://)
第三段的代码主要讲判断文件的后缀类型,然后前端好处理。这里我的代码是写死了,因为我就测试一张图。最好的方式是服务器返回的 推送内容中,带有附件的类型。我的iOS开发 iOS10推送必看(高阶1)一文中,有讲多媒体附件的类型,以及相关的大小限制。</p>
<p>这里强烈建议,处理推送内容的时候,让服务器带上文件类型。重要的事情说三遍
这里强烈建议,处理推送内容的时候,让服务器带上文件类型。重要的事情说三遍
这里强烈建议,处理推送内容的时候,让服务器带上文件类型。重要的事情说三遍</p>
<p>在补充一些问题</p>
<p>1.在这个推送服务的扩展类中,为什么我使用了系统自带类下载,没有使用AFN?
答:不知道为什么,导入AFN后总是出现各种编译报错啊。主要涉及这几个情况
1.用cocoapods导入AFN,在类引入AFN,编译报错!!
2.手动拖拽AFN进工程(不勾选),在类引入AFN,编译报错!!
3.手动拖拽AFN进工程(勾选),直接编译报错!!
4.下载用通知回调的方式(慢,线程不确定)
最后我就选择这个block回调,系统类下载的方式,来下载通知中的附件。
以下是一些错误的截图:(可以不看)</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/7.png" alt="1" /><br /></p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/8.png" alt="1" /><br /></p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/9.png" alt="1" /><br /></p>
<p>2.如果调试这个通知扩展类,为什么我跑程序的时候,打断点无反应?</p>
<p>答:这是因为你跑的target不对,正确的做法是,跑正确的target,具体如下图:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/10.png" alt="1" /><br /></p>
<p>选择你的程序Target</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/11.png" alt="1" /><br /></p>
<p>这个时候,大家在打断点,就可以啦~</p>
<p>最后,在附上推送的内容格式。
推送内容格式如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"aps": {
"alert": "This is some fancy message.",
"badge": 1,
"sound": "default",
"mutable-content": "1",
"imageAbsoluteString": "http://upload.univs.cn/2012/0104/1325645511371.jpg"
}
}
</code></pre></div></div>
<p>这里我们要注意一定要有”mutable-content”: “1”,以及一定要有Alert的字段,否则可能会拦截通知失败。(苹果文档说的)。除此之外,我们还可以添加自定义字段,比如,图片地址,图片类型,大家慢慢摸索下吧~有问题可以留言哟~</p>
<p>这一章的最后,附上成功推送的展示图:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao65/12.png" alt="1" /><br /></p>
<p>稍后补充以下内容~</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 2、UNNotificationContentExtension
## 2.1、UNNotificationContentExtension简介
## 2.2、如何新建一个UNNotificationContentExtension
## 2.3、如何使用以及相关Demo
# 3、UNNotificationAction
## 3.1、UNNotificationAction简介
## 3.2、如何新建一个UNNotificationAction
## 3.3、如何使用以及相关Demo
</code></pre></div></div>
<p>如果你喜欢我的文章,不要忘记关注我,谢谢大家了~
另外如果你要转载,希望可以注明出处,我会写出更多更好的文章,来回馈大家~</p>
<p>重要的事情说三遍,<a href="https://github.com/CoderWQ/XZPushTest/tree/master">demo地址</a>
重要的事情说三遍,<a href="https://github.com/CoderWQ/XZPushTest/tree/master">demo地址</a>
重要的事情说三遍,<a href="https://github.com/CoderWQ/XZPushTest/tree/master">demo地址</a></p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao65">iOS开发之iOS10推送必看(高阶2)</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 12, 2016.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao64
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao64
2016-10-09T00:00:00+08:00
2016-10-09T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<p><code class="language-plaintext highlighter-rouge">UITextField</code>被用作项目中获取用户信息的重要控件,但是在实际应用中存在的不少的坑:修改<code class="language-plaintext highlighter-rouge">keyboardType</code>来限制键盘的类型,却难以限制第三方键盘的输入类型;在代理中限制了输入长度以及输入的文本类型,但是却抵不住中文输入的联想;键盘弹起时遮住输入框,需要接收键盘弹起收回的通知,然后计算坐标实现移动动画。</p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/4b9957f2afc2">Sindri的小巢</a><br /></p>
<p>对于上面这些问题,苹果提供给我们文本输入框的同时并不提供解决方案,因此本文将使用<code class="language-plaintext highlighter-rouge">category+runtime</code>的方式解决上面提到的这些问题,本文假设读者已经清楚从<code class="language-plaintext highlighter-rouge">UITextField</code>成为第一响应者到结束编辑过程中的事件调用流程。</p>
<p>##输入限制
最常见的输入限制是手机号码以及金额,前者文本中只能存在纯数字,后者文本中还能包括小数。笔者暂时定义了三种枚举状态用来表示三种文本限制:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef NS_ENUM(NSInteger, LXDRestrictType)
{
LXDRestrictTypeOnlyNumber = 1, ///< 只允许输入数字
LXDRestrictTypeOnlyDecimal = 2, ///< 只允许输入实数,包括.
LXDRestrictTypeOnlyCharacter = 3, ///< 只允许非中文输入
};
</code></pre></div></div>
<p>在文本输入的时候会有两次回调,一次是代理的<code class="language-plaintext highlighter-rouge">replace</code>的替换文本方法,另一个需要我们手动添加的<code class="language-plaintext highlighter-rouge">EditingChanged</code>编辑改变事件。前者在中文联想输入的时候无法准确获取文本内容,而当确认好输入的文本之后才会调用后面一个事件,因此回调后一个事件才能准确的筛选文本。下面的代码会筛选掉文本中所有的非数字:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)viewDidLoad
{
[textField addTarget: self action: @selector(textDidChanged:) forControlEvents: UIControlEventEditingChanged];
}
- (void)textDidChanged: (UITextField *)textField
{
NSMutableString * modifyText = textField.text.mutableCopy;
for (NSInteger idx = 0; idx < modifyText.length; idx++) {
NSString * subString = [modifyText substringWithRange: NSMakeRange(idx, 1)];
// 使用正则表达式筛选
NSString * matchExp = @"^\\d$";
NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", matchExp];
if ([predicate evaluateWithObject: subString]) {
idx++;
} else {
[modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
}
}
}
</code></pre></div></div>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao64/1.gif" alt="1" /><br /></p>
<h2 id="限制扩展">限制扩展</h2>
<p>如果说我们每次需要限制输入的时候都加上这么一段代码也是有够糟的,那么如何将这个功能给封装出来并且实现自定义的限制扩展呢?笔者通过工厂来完成这一个功能,每一种文本的限制对应一个单独的类。抽象提取出一个父类,只提供一个文本变化的实现接口和一个限制最长输入的<code class="language-plaintext highlighter-rouge">NSUInteger</code>整型属性:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#pragma mark - h文件
@interface LXDTextRestrict : NSObject
@property (nonatomic, assign) NSUInteger maxLength;
@property (nonatomic, readonly) LXDRestrictType restrictType;
// 工厂
+ (instancetype)textRestrictWithRestrictType: (LXDRestrictType)restrictType;
// 子类实现来限制文本内容
- (void)textDidChanged: (UITextField *)textField;
@end
#pragma mark - 继承关系
@interface LXDTextRestrict ()
@property (nonatomic, readwrite) LXDRestrictType restrictType;
@end
@interface LXDNumberTextRestrict : LXDTextRestrict
@end
@interface LXDDecimalTextRestrict : LXDTextRestrict
@end
@interface LXDCharacterTextRestrict : LXDTextRestrict
@end
#pragma mark - 父类实现
@implementation LXDTextRestrict
+ (instancetype)textRestrictWithRestrictType: (LXDRestrictType)restrictType
{
LXDTextRestrict * textRestrict;
switch (restrictType) {
case LXDRestrictTypeOnlyNumber:
textRestrict = [[LXDNumberTextRestrict alloc] init];
break;
case LXDRestrictTypeOnlyDecimal:
textRestrict = [[LXDDecimalTextRestrict alloc] init];
break;
case LXDRestrictTypeOnlyCharacter:
textRestrict = [[LXDCharacterTextRestrict alloc] init];
break;
default:
break;
}
textRestrict.maxLength = NSUIntegerMax;
textRestrict.restrictType = restrictType;
return textRestrict;
}
- (void)textDidChanged: (UITextField *)textField
{
}
@end
</code></pre></div></div>
<p>由于子类在筛选的过程中都存在遍历字符串以及正则表达式验证的流程,把这一部分代码逻辑给封装起来。根据<code class="language-plaintext highlighter-rouge">EOC</code>的原则优先使用<code class="language-plaintext highlighter-rouge">static inline</code>的内联函数而非宏定义:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef BOOL(^LXDStringFilter)(NSString * aString);
static inline NSString * kFilterString(NSString * handleString, LXDStringFilter subStringFilter)
{
NSMutableString * modifyString = handleString.mutableCopy;
for (NSInteger idx = 0; idx < modifyString.length;) {
NSString * subString = [modifyString substringWithRange: NSMakeRange(idx, 1)];
if (subStringFilter(subString)) {
idx++;
} else {
[modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
}
}
return modifyString;
}
static inline BOOL kMatchStringFormat(NSString * aString, NSString * matchFormat)
{
NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", matchFormat];
return [predicate evaluateWithObject: aString];
}
#pragma mark - 子类实现
@implementation LXDNumberTextRestrict
- (void)textDidChanged: (UITextField *)textField
{
textField.text = kFilterString(textField.text, ^BOOL(NSString *aString) {
return kMatchStringFormat(aString, @"^\\d$");
});
}
@end
@implementation LXDDecimalTextRestrict
- (void)textDidChanged: (UITextField *)textField
{
textField.text = kFilterString(textField.text, ^BOOL(NSString *aString) {
return kMatchStringFormat(aString, @"^[0-9.]$");
});
}
@end
@implementation LXDCharacterTextRestrict
- (void)textDidChanged: (UITextField *)textField
{
textField.text = kFilterString(textField.text, ^BOOL(NSString *aString) {
return kMatchStringFormat(aString, @"^[^[\\u4e00-\\u9fa5]]$");
});
}
@end
</code></pre></div></div>
<p>有了文本限制的类,那么接下来我们需要新建一个<code class="language-plaintext highlighter-rouge">UITextField</code>的分类来添加输入限制的功能,主要新增三个属性:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface UITextField (LXDRestrict)
/// 设置后生效
@property (nonatomic, assign) LXDRestrictType restrictType;
/// 文本最长长度
@property (nonatomic, assign) NSUInteger maxTextLength;
/// 设置自定义的文本限制
@property (nonatomic, strong) LXDTextRestrict * textRestrict;
@end
</code></pre></div></div>
<p>由于这些属性是<code class="language-plaintext highlighter-rouge">category</code>中添加的,我们需要手动生成<code class="language-plaintext highlighter-rouge">getter</code>和<code class="language-plaintext highlighter-rouge">setter</code>方法,这里使用<code class="language-plaintext highlighter-rouge">objc_associate</code>的动态绑定机制来实现。其中核心的方法实现如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)setRestrictType: (LXDRestrictType)restrictType
{
objc_setAssociatedObject(self, LXDRestrictTypeKey, @(restrictType), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
self.textRestrict = [LXDTextRestrict textRestrictWithRestrictType: restrictType];
}
- (void)setTextRestrict: (LXDTextRestrict *)textRestrict
{
if (self.textRestrict) {
[self removeTarget: self.text action: @selector(textDidChanged:) forControlEvents: UIControlEventEditingChanged];
}
textRestrict.maxLength = self.maxTextLength;
[self addTarget: textRestrict action: @selector(textDidChanged:) forControlEvents: UIControlEventEditingChanged];
objc_setAssociatedObject(self, LXDTextRestrictKey, textRestrict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
</code></pre></div></div>
<p>完成这些工作之后,只需要一句代码就可以完成对<code class="language-plaintext highlighter-rouge">UITextField</code>的输入限制:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>self.textField.restrictType = LXDRestrictTypeOnlyDecimal;
</code></pre></div></div>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao64/2.gif" alt="1" /><br /></p>
<h2 id="自定义的限制">自定义的限制</h2>
<p>假如现在文本框限制只允许输入<code class="language-plaintext highlighter-rouge">emoji</code>表情,上面三种枚举都不存在我们的需求,这时候自定义一个子类来实现这个需求。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface LXDEmojiTextRestrict : LXDTextRestrict
@end
@implementation LXDEmojiTextRestrict
- (void)textDidChanged: (UITextField *)textField
{
NSMutableString * modifyString = textField.text.mutableCopy;
for (NSInteger idx = 0; idx < modifyString.length;) {
NSString * subString = [modifyString substringWithRange: NSMakeRange(idx, 1)];
NSString * emojiExp = @"^[\\ud83c\\udc00-\\ud83c\\udfff]|[\\ud83d\\udc00-\\ud83d\\udfff]|[\\u2600-\\u27ff]$";
NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", emojiExp];
if ([predicate evaluateWithObject: subString]) {
idx++;
} else {
[modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
}
}
textField.text = modifyString;
}
@end
</code></pre></div></div>
<p>代码中的<code class="language-plaintext highlighter-rouge">emoji</code>的正则表达式还不全,因此在实践中很多的<code class="language-plaintext highlighter-rouge">emoji</code>点击会被筛选掉。效果如下:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao64/3.gif" alt="1" /><br /></p>
<h2 id="键盘遮盖">键盘遮盖</h2>
<p>另一个让人头疼的问题就是输入框被键盘遮挡。这里通过在<code class="language-plaintext highlighter-rouge">category</code>中添加键盘相关通知来完成移动整个<code class="language-plaintext highlighter-rouge">window</code>。其中通过下面这个方法获取输入框在<code class="language-plaintext highlighter-rouge">keyWindow</code>中的相对坐标:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view
</code></pre></div></div>
<p>我们给输入框提供一个设置自动适应的接口:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@interface UITextField (LXDAdjust)
/// 自动适应
- (void)setAutoAdjust: (BOOL)autoAdjust;
@end
@implementation UITextField (LXDAdjust)
- (void)setAutoAdjust: (BOOL)autoAdjust
{
if (autoAdjust) {
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillShow:) name: UIKeyboardWillShowNotification object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillHide:) name: UIKeyboardWillHideNotification object: nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
}
- (void)keyboardWillShow: (NSNotification *)notification
{
if (self.isFirstResponder) {
CGPoint relativePoint = [self convertPoint: CGPointZero toView: [UIApplication sharedApplication].keyWindow];
CGFloat keyboardHeight = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
CGFloat actualHeight = CGRectGetHeight(self.frame) + relativePoint.y + keyboardHeight;
CGFloat overstep = actualHeight - CGRectGetHeight([UIScreen mainScreen].bounds) + 5;
if (overstep > 0) {
CGFloat duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect frame = [UIScreen mainScreen].bounds;
frame.origin.y -= overstep;
[UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
[UIApplication sharedApplication].keyWindow.frame = frame;
} completion: nil];
}
}
}
- (void)keyboardWillHide: (NSNotification *)notification
{
if (self.isFirstResponder) {
CGFloat duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect frame = [UIScreen mainScreen].bounds;
[UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
[UIApplication sharedApplication].keyWindow.frame = frame;
} completion: nil];
}
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
@end
</code></pre></div></div>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao64/4.gif" alt="1" /><br /></p>
<p>如果项目中存在自定义的<code class="language-plaintext highlighter-rouge">UITextField</code>子类,那么上面代码中的<code class="language-plaintext highlighter-rouge">dealloc</code>你应该使用<code class="language-plaintext highlighter-rouge">method_swillzing</code>来实现释放通知的作用</p>
<h2 id="尾语">尾语</h2>
<p>其实大多数时候,实现某些小细节功能只是很简单的一些代码,但是需要我们去了解事件响应的整套逻辑来更好的完成它。另外,昨天给微信<code class="language-plaintext highlighter-rouge">小程序</code>刷屏了,我想对各位iOS开发者说与其当心自己的饭碗是不是能保住,不如干好自己的活,顺带学点js适应一下潮流才是王道。<a href="https://github.com/JustKeepRunning/LXDTextFieldAdjust">本文demo</a></p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao64">iOS开发之UITextField的那点事</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on October 09, 2016.</p>
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao63
//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao63
2016-09-28T00:00:00+08:00
2016-09-28T00:00:00+08:00
Bison
//allluckly.cn
lbjobvip@163.com
<blockquote>
<p>虽然这篇文章比较长,也不好理解,但是还是建议大家收藏,以后用到的时候,可以看看,有耐心的还是读一读。</p>
</blockquote>
<p>这篇文章开始,我会跟大家好好讲讲,苹果新发布的iOS10的所有通知类。</p>
<p>编辑:<a href="http://allluckly.cn">Bison</a><br />
来源:<a href="http://www.jianshu.com/p/3d602a60ca4f">徐不同</a><br /></p>
<h2 id="一创建本地通知事例详解">一、创建本地通知事例详解:</h2>
<p>注意啊,小伙伴们,本地通知也必须在appdelegate中注册中心,通知的开关打不打开无所谓的,毕竟是本地通知,但是通知的接收的代理,以及通知点击的代理,苹果给合二为一了。所以大家还是需要在appdelegate中写上这2个方法,还有不要忘记在</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
</code></pre></div></div>
<p>注册通知中心。如果使用极光推送的小伙伴,写看一下我的基础篇,辛苦大家啦</p>
<p>创建一个UNNotificationRequest类的实例,一定要为它设置identifier, 在后面的查找,更新, 删除通知,这个标识是可以用来区分这个通知与其他通知
把request加到UNUserNotificationCenter, 并设置触发器,等待触发
如果另一个request具有和之前request相同的标识,不同的内容, 可以达到更新通知的目的
创建一个本地通知我们应该先创建一个UNNotificationRequest类,并且将这个类添加到UNUserNotificationCenter才可以。代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 1.创建一个UNNotificationRequest
NSString *requestIdentifer = @"TestRequest";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:trigger];
// 2.将UNNotificationRequest类,添加进当前通知中心中
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
}];
</code></pre></div></div>
<p>在创建UNNotificationRequest类时,官方的解释是说,一个通知请求可以在预定通过时间和位置,来通知用户。触发的方式见UNNotificationTrigger的相关说明。调用该方法,在通知触发的时候。会取代具有相同标识符的通知请求,此外,消息个数受系统限制。</p>
<p>上面的翻译,看上去可能有些拗口,简单来说,就是我们需要为UNNotificationRequest设置一个标识符,通过标识符,我们可以对该通知进行添加,删除,更新等操作。</p>
<p>以下是完整的创建通知的代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> // 1.创建通知内容
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"徐不同测试通知";
content.subtitle = @"测试通知";
content.body = @"来自徐不同的简书";
content.badge = @1;
NSError *error = nil;
NSString *path = [[NSBundle mainBundle] pathForResource:@"icon_certification_status1@2x" ofType:@"png"];
// 2.设置通知附件内容
UNNotificationAttachment *att = [UNNotificationAttachment attachmentWithIdentifier:@"att1" URL:[NSURL fileURLWithPath:path] options:nil error:&error];
if (error) {
NSLog(@"attachment error %@", error);
}
content.attachments = @[att];
content.launchImageName = @"icon_certification_status1@2x";
// 2.设置声音
UNNotificationSound *sound = [UNNotificationSound defaultSound];
content.sound = sound;
// 3.触发模式
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];
// 4.设置UNNotificationRequest
NSString *requestIdentifer = @"TestRequest";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:trigger1];
//5.把通知加到UNUserNotificationCenter, 到指定触发点会被触发
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
}];
</code></pre></div></div>
<p>通过以上代码,我们就可以创建一个5秒触发本地通知,具体样式可以看下图</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao63/1.png" alt="1" /><br /></p>
<p>下拉放大content.launchImageName = @”icon_certification_status1@2x”;显示的图片是这行代码的效果,如图</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao63/2.png" alt="1" /><br /></p>
<p>根据上面内容,大家会发现在创建UNNotificationRequest的时候,会需要UNMutableNotificationContent以及UNTimeIntervalNotificationTrigger这两个类。下面我就对相关的类,以及类扩展,做相应的说明。</p>
<p>1.UNNotificationContent以及UNMutableNotificationContent(通知内容和可变通知内容)</p>
<p>通知内容分为可变的以及不可变的两种类型,类似于可变数组跟不可变数组。后续我们通过某一特定标识符更新通知,便是用可变通知了。
不管是可变通知还是不可变通知,都有以下的几个属性:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> // 1.附件数组,存放UNNotificationAttachment类
@property (NS_NONATOMIC_IOSONLY, copy) NSArray <UNNotificationAttachment *> *attachments ;
// 2.应用程序角标,0或者不传,意味着角标消失
@property (NS_NONATOMIC_IOSONLY, copy, nullable) NSNumber *badge;
// 3.主体内容
@property (NS_NONATOMIC_IOSONLY, copy) NSString *body ;
// 4.app通知下拉预览时候展示的图
@property (NS_NONATOMIC_IOSONLY, copy) NSString *launchImageName;
// 5.UNNotificationSound类,可以设置默认声音,或者指定名称的声音
@property (NS_NONATOMIC_IOSONLY, copy, nullable) UNNotificationSound *sound ;
// 6.推送内容的子标题
@property (NS_NONATOMIC_IOSONLY, copy) NSString *subtitle ;
// 7.通知线程的标识
@property (NS_NONATOMIC_IOSONLY, copy) NSString *threadIdentifier;
// 8.推送内容的标题
@property (NS_NONATOMIC_IOSONLY, copy) NSString *title ;
// 9.远程通知推送内容
@property (NS_NONATOMIC_IOSONLY, copy) NSDictionary *userInfo;
// 10.category标识
@property (NS_NONATOMIC_IOSONLY, copy) NSString *categoryIdentifier;
</code></pre></div></div>
<p>以上的的属性,我都增加了相应的说明,大家可以对照我的注释来使用。</p>
<p>2.UNNotificationAttachment (附件内容通知)</p>
<p>在UNNotificationContent类中,有个附件数组的属性,这就是包含UNNotificationAttachment类的数组了。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> @property (NS_NONATOMIC_IOSONLY, copy) NSArray <UNNotificationAttachment *> *attachments ;
</code></pre></div></div>
<p>苹果的解释说,UNNotificationAttachment(附件通知)是指可以包含音频,图像或视频内容,并且可以将其内容显示出来的通知。使用本地通知时,可以在通知创建时,将附件加入即可。对于远程通知,则必须实现使用UNNotificationServiceExtension类通知服务扩展。</p>
<p>创建附件的方法是attachmentWithIdentifier:URL:options:error:。在使用时,必须指定使用文件附件的内容,并且文件格式必须是支持的类型之一。创建附件后,将其分配给内容对象的附件属性。 (对于远程通知,您必须从您的服务扩展做到这一点。)
附件通知支持的类型如下图:</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao63/3.png" alt="1" /><br /></p>
<p>下面是创建UNNotificationAttachment的方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> + (nullable instancetype)attachmentWithIdentifier:(NSString *)identifier URL:(NSURL *)URL options:(nullable NSDictionary *)options error:(NSError *__nullable *__nullable)error;
</code></pre></div></div>
<h3 id="注意url必须是一个有效的文件路径不然会报错">注意:URL必须是一个有效的文件路径,不然会报错</h3>
<p>这里我再在说下options的属性,一共有4种选项(这几个属性可研究死我了)</p>
<ul>
<li>1UNNotificationAttachmentOptionsTypeHintKey此键的值是一个包含描述文件的类型统一类型标识符(UTI)一个NSString。如果不提供该键,附件的文件扩展名来确定其类型,常用的类型标识符有kUTTypeImage,kUTTypeJPEG2000,kUTTypeTIFF,kUTTypePICT,kUTTypeGIF ,kUTTypePNG,kUTTypeQuickTimeImage等。看到这里你一定有疑问,这些类型导入报错了啊!!我研究了苹果文档,发现大家需要添加以下框架才可以,具体大家可以通过以下类型来处理。</li>
</ul>
<p>注意:</p>
<p>框架就是#import<MobileCoreServices/MobileCoreServices.h></p>
<p>使用方法如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> dict[UNNotificationAttachmentOptionsTypeHintKey] = (__bridge id _Nullable)(kUTTypeImage);
</code></pre></div></div>
<ul>
<li>2UNNotificationAttachmentOptionsThumbnailHiddenKey,是一个BOOL值,为YES时候,缩略图将隐藏,默认为YES。如图:</li>
</ul>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao63/4.png" alt="1" /><br /></p>
<p>大家可以对照上面的图来看,就明白是哪里的图消失了。</p>
<p>使用方法如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> dict[UNNotificationAttachmentOptionsThumbnailHiddenKey] = @YES;
</code></pre></div></div>
<ul>
<li>3UNNotificationAttachmentOptionsThumbnailClippingRectKey剪贴矩形的缩略图。这个密钥的值是包含一个归一化的CGRect - 也就是说,一个单元的矩形,其值是在以1.0〜 0.0 ,表示要显示的原始图像的所述部分的字典。例如,指定的(0.25 , 0.25)的原点和大小(0.5 ,0.5 )定义了剪辑矩形,只显示图像的中心部分。使用CGRectCreateDictionaryRepresentation函数来创建字典的矩形。</li>
</ul>
<p>上面这句话是苹果的翻译,太绕口了。我简单说,就是我下面这幅图。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao63/5.png" alt="1" /><br />B</p>
<p>整张图被分割了,整体比例为1,如果想得到图中阴影面积,就需要写的CGRect(0.5,0.5,0.25,0.25),意思是,从(0.5,0.5)为原点,面积为(0.25,0.25),大家可以理解成,即下面的方法。</p>
<p>使用方法如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> dict[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = (__bridge id _Nullable)((CGRectCreateDictionaryRepresentation(CGRectMake(0.5, 0.5, 0.25 ,0.25))));;
</code></pre></div></div>
<p>使用上面的方法,可以得到一张图的阴影部分的图像,这张图像会是通知的缩略图。比如我下面的这个图,缩略图大家应该可以发现变了吧。</p>
<p><img src="//allluckly.cn/images/blog/tuogao/tougao63/6.png" alt="1" /><br /></p>
<p>这里为了理解,在给大家说几个”坐标点”:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> (0,0,0.25,0.25)左上角的最小正方形
(0,0,0.5,0.5) 四分之一的正方形,左上角
(0.5,0.5,0.5,0.5)四分之一的正方形,右下角
(0.5,0,0.5,0.5)四分之一的正方形,左下角
(0.25,0.25,0.5,0.5)最中心的正方形
</code></pre></div></div>
<h2 id="特别注意">特别注意:</h2>
<p>调试到这里的时候,我感觉苹果应该是有个bug,就是我在来回变化这个显示缩略图的frame的时候,来回改,永远显示为第一次写的frame。我在修改UNNotificationRequest的requestIdentifer属性后,可以变换属性。所以我猜测可能相同requestIdentifer的通知,算一个通知,所以只能调用更新的方法,来变化缩略图的吃不腻吧,或许也不是bug。</p>
<ul>
<li>4UNNotificationAttachmentOptionsThumbnailTimeKey,一般影片附件会用到,指的是用影片中的某一秒来做这个缩略图;</li>
</ul>
<p>使用方法如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> dict[UNNotificationAttachmentOptionsThumbnailTimeKey] =@10;
</code></pre></div></div>
<p>这里我们可以直接传递一个NSNumber的数值,比如使用影片第10s的画面来做缩略图就按照上面的来写。此外,要注意的是,这个秒数必须是这个影片长度范围内的,不然报错。</p>
<p>3.UNTimeIntervalNotificationTrigger (通知触发模式)</p>
<p>这个我在!(这篇文章中已经初步介绍了,现在我在详细介绍下)[www.baidu.com]这篇文章中已经初步介绍了,现在我在详细介绍下。</p>
<p>1.UNPushNotificationTrigger (远程通知触发)一般我们不会使用的</p>
<p>2.UNTimeIntervalNotificationTrigger (本地通知) 一定时间之后,重复或者不重复推送通知。我们可以设置timeInterval(时间间隔)和repeats(是否重复)。</p>
<p>使用方法:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> UNTimeIntervalNotificationTrigger *triggerOne = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];
</code></pre></div></div>
<p>解释:上面的方法是指5秒钟之后执行。repeats这个属性,如果需要为重复执行的,则TimeInterval必须大于60s,否则会报<code class="language-plaintext highlighter-rouge">* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'time interval must be at least 60 if repeating'</code>的错误!**</p>
<p>3.UNCalendarNotificationTrigger(本地通知) 一定日期之后,重复或者不重复推送通知 例如,你每天8点推送一个通知,只需要dateComponents为8。如果你想每天8点都推送这个通知,只要repeats为YES就可以了。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> // 周一早上 8:00 上班
NSDateComponents *components = [[NSDateComponents alloc] init];
// 注意,weekday是从周日开始的,如果想设置为从周一开始,大家可以自己想想~
components.weekday = 2;
components.hour = 8;
UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];
</code></pre></div></div>
<p>4.UNLocationNotificationTrigger (本地通知)地理位置的一种通知,使用这个通知,你需要导入</p>
<p>#import<CoreLocation/CoreLocation.h>这个系统类库。示例代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> //1、如果用户进入或者走出某个区域会调用下面两个方法
- (void)locationManager:(CLLocationManager *)manager
didEnterRegion:(CLRegion *)region
- (void)locationManager:(CLLocationManager *)manager
didExitRegion:(CLRegion *)region代理方法反馈相关信息
//2、一到某个经纬度就通知,判断包含某一点么
// 不建议使用!!!!!!CLRegion *region = [[CLRegion alloc] init];// 不建议使用!!!!!!
CLCircularRegion *circlarRegin = [[CLCircularRegion alloc] init];
[circlarRegin containsCoordinate:(CLLocationCoordinate2D)];
UNLocationNotificationTrigger *trigger4 = [UNLocationNotificationTrigger triggerWithRegion:circlarRegin repeats:NO];
</code></pre></div></div>
<p>注意,这里建议使用CLCircularRegion这个继承自CLRegion的类,因为我看到苹果已经飞起了CLRegion里面是否包含这一点的方法,并且推荐我们使用CLCircularRegion这个类型</p>
<p>如果你喜欢我的文章,不要忘记关注我,谢谢大家了~
另外如果你要转载,希望可以注明出处,我会写出更多更好的文章,来回馈大家~</p>
<p>稍后我会补充更多内容,敬请期待!!!</p>
<hr />
<blockquote>
<p><a href="https://itunes.apple.com/us/app/it-blog-zi-xueios-kai-fa-jin/id1067787090?l=zh&ls=1&mt=8">学习iOS开发的app上线啦,点此来围观吧</a><br /></p>
</blockquote>
<blockquote>
<p><a href="https://allluckly.cn">更多经验请点击</a><br /></p>
</blockquote>
<p>好文推荐:<a href="https://allluckly.cn/lblaunchimagead/LBLaunchImageAd">分分钟解决iOS开发中App启动动画广告的功能</a><br /></p>
<p><a href="//allluckly.cn/%E6%8A%95%E7%A8%BF/tougao63">iOS开发之iOS10推送必看(高阶1)</a> was originally published by Bison at <a href="//allluckly.cn">iOS开发</a> on September 28, 2016.</p>