iOS直播(四)对视频进行压缩编码
1.为什么要进行编码?
不经过压缩编码的原视频,所占空间大,不便于保存和网络传输,所以视频录制完后,需要先编码,再传输,解码后再播放。
2.视频为什么可以被压缩?
视频存在冗余信息,主要为数据冗余和视觉冗余
1.数据冗余:图像的各像素之间存在着很强的相关性。消除这些冗余并不会导致信息损失,属于无损压缩。可以细分为:
- 空间冗余:同一帧图像像素之间有较强的相关性,可以进行帧内预测编码去除冗余。
- 时间冗余:相邻帧的图像具有相似性,可以通过帧间预测编码去除冗余。
2.视觉冗余:人眼的一些特性比如亮度辨别阈值,视觉阈值,对亮度和色度的敏感度不同,使得在编码的时候引入适量的误差,也不会被察觉出来。可以利用人眼的视觉特性,以一定的客观失真换取数据压缩。这种压缩属于有损压缩。
3.压缩编码的标准
目前主要主要使用ITU国际电传视频联盟主导的H.26x系列标准,目前应用最广泛的为H.264,随着4k、8k等超高清时代的来临,H.265也逐渐开始普及。
4.H.264压缩方式
(1)H264中图像以序列(GOP)为单位进行组织,把几帧图像分为一个GOP,也就是一个GOP为一段图像编码后的数据流。
(2)一个GOP内的各帧图像被划分为I帧、B帧、P帧。
I帧:帧内编码帧(intra picture):为每个GOP的第一帧,通过去除空间冗余进行压缩,每个GOP有且仅有这一个I帧。
P帧:预测编码帧(predictive-frame):通过去除GOP中前面已编码的帧(I帧或P帧)的时间冗余信息来编码图像。每个GOP中有一个或多个P帧。
一个序列(GOP)的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。
B帧:双向预测帧(bi-directional interpolated prediction frame):根据相邻的前一帧、本帧以及后一帧数据的不同点来压缩本帧,也即仅记录本帧与前后帧的差值。I帧和P帧间或两个P帧间有一个或多个B帧。
I帧为基础帧,以I帧预测P帧,再由I帧和P帧一起预测B帧,一般地,I帧压缩效率最低,P帧较高,B帧最高。
(3)最后将I帧数据与预测的差值信息进行存储和传输。
5.H.264分层结构
H.264的功能分为两层
- 视频编码层(VCL:Video Coding Layer):即被压缩编码后的视频数据序列,我们前面介绍的内容均为VCL层
- 网络提取层(NAL:Network Abstraction Layer):在VCL数据封装到NAL单元中之后,才可以用来传输或存储。
6.NAL封装
(1)封装方式:
NAL是将每一帧数据写入到一个NAL单元(NALU)中,进行传输或存储的
NALU分为NAL头和NAL体
NALU头通常为00 00 00 01,作为一个新的NALU的起始标识
NALU体封装着VCL编码后的信息或者其他信息
(2)封装过程:
I帧、P帧、B帧都是被封装成一个或者多个NALU进行传输或者存储的
I帧开始之前也有非VCL的NAL单元,用于保存其他信息,比如:PPS、SPS
- PPS(Picture Parameter Sets):图像参数集
- SPS(Sequence Parameter Set):序列参数集
在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符,一般来说编码器编出的首帧数据为PPS与SPS,接着为I帧,后续是B帧、P帧等数据
7.编码方式
(1)硬编码:使用非CPU进行编码,例如使用GPU、专用DSP、FPGA、ASIC芯片等。用此方式对CPU负载小,但对GPU等硬件要求高,iOS8中苹果已经为我们封装了VideoToolBox和AudioToolBox两个框架进行硬编码。
(2)软编码:使用CPU进行编码,通常使用开源的ffmpeg+x264
8.代码Demo
下面以代码演示整个采集和编码的流程:采集–>获取视频帧–>对视频帧进行编码–>获取视频帧信息–>将编码后的数据以NALU方式写入文件
既然iPhone拥有强大的GPU硬件,也提供了VideoToolBox和AudioToolBox两个优秀的框架,那demo当然选择硬编码啦~
(1)将前文中利用AVFoundation进行视频采集的代码进行封住,创建VideoCapture类,实现开始采集方法startCapture:和停止采集方法stopCapture。
在获取到sampleBuffer后,使用下一步创建的VideoEncoder类进行编码。
VideoCapture.h
@interface VideoCapture : NSObject- (void)startCapture:(UIView *)preview;- (void)stopCapture;@end
VideoCapture.m
#import "VideoCapture.h"
#import "VideoEncoder.h"
#import <AVFoundation/AVFoundation.h>@interface VideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate>/** 编码对象 */
@property (nonatomic, strong) VideoEncoder *encoder;/** 捕捉会话*/
@property (nonatomic, weak) AVCaptureSession *captureSession;/** 预览图层 */
@property (nonatomic, weak) AVCaptureVideoPreviewLayer *previewLayer;/** 捕捉画面执行的线程队列 */
@property (nonatomic, strong) dispatch_queue_t captureQueue;@end@implementation VideoCapture- (void)startCapture:(UIView *)preview
{// 0.初始化编码对象self.encoder = [[VideoEncoder alloc] init];// 1.创建捕捉会话AVCaptureSession *session = [[AVCaptureSession alloc] init];session.sessionPreset = AVCaptureSessionPreset1280x720;self.captureSession = session;// 2.设置输入设备AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];NSError *error = nil;AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];[session addInput:input];// 3.添加输出设备AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];self.captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);[output setSampleBufferDelegate:self queue:self.captureQueue];[session addOutput:output];// 设置录制视频的方向AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];// 4.添加预览图层AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];previewLayer.frame = preview.bounds;[preview.layer insertSublayer:previewLayer atIndex:0];self.previewLayer = previewLayer;// 5.开始捕捉[self.captureSession startRunning];
}- (void)stopCapture {[self.captureSession stopRunning];[self.previewLayer removeFromSuperlayer];[self.encoder endEncode];
}#pragma mark - 获取到数据
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {[self.encoder encodeSampleBuffer:sampleBuffer];
}
(2)创建VideoEncoder类,实现编码功能。
- setupFileHandle方法创建了保存编码后视频的文件路径。
- setupVideoSession创建并初始化了编码会话compressionSession,其中创建方法VTCompressionSessionCreate()中,第8个参数为指定编码回调的c语言方法为didCompressH264。
- 在上一步采集到SampleBuffer后,调用encodeSampleBuffer:方法进行编码,回调上一步的didCompressH264。
- 在didCompressH264中判断若为关键帧,则增加sps和pps,并转换为NSData,拼接为NALU单元后写入文件,其他帧也拼接为NALU单元后写入文件。
- 视频采集结束后调用endEncode方法销毁对话。
VideoEncoder.h
#import <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>@interface VideoEncoder : NSObject- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;@end
VideoEncoder.m
#import "VideoEncoder.h"@interface VideoEncoder()/** 记录当前的帧数 */
@property (nonatomic, assign) NSInteger frameID;/** 编码会话 */
@property (nonatomic, assign) VTCompressionSessionRef compressionSession;/** 文件写入对象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;@end@implementation VideoEncoder-(instancetype)init{if(self = [super init]){// 1.初始化写入文件的对象(NSFileHandle用于写入二进制文件)[self setUpFileHandle];// 2.初始化压缩编码的会话[self setUpVideoSession];}return self;
}-(void)setUpFileHandle{//1.获取沙盒路径NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"abc.h264"];//2.如果已有文件则删除后再创建[[NSFileManager defaultManager] removeItemAtPath:file error:nil];[[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];//3.创建对象self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}-(void)setUpVideoSession{//1.用于记录当前是第几帧数据self.frameID = 0;//2.录制视频的宽高int width = [UIScreen mainScreen].bounds.size.width;int height = [UIScreen mainScreen].bounds.size.height;//3.创建CompressionSession对象,用于对画面进行编码VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &_compressionSession);//4.设置实时编码输出(直播需要实时输出)VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);//5.设置期望帧率为每秒30帧int fps = 30;CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);//6.设置码率int biteRate = 800*1024;CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &biteRate);VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);NSArray *limit = @[@(biteRate*1.5/8),@(1)];VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);//7.设置关键帧间隔为30(GOP长度)int frameInterval = 30;CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);//8.准备编码VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
}- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer{//1.将sampleBuffer转成imageBufferCVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);//2.根据当前帧数,创建CMTimeCMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);VTEncodeInfoFlags flags;//3.开始编码该帧数据OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,imageBuffer,presentationTimeStamp,kCMTimeInvalid,NULL, (__bridge void * _Nullable)(self), &flags);if (statusCode == noErr) {NSLog(@"H264: VTCompressionSessionEncodeFrame Success");}
}// 编码完成回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {//1.判断状态是否是没有报错if (status != noErr) {return;}//2.根据传入的参数获取对象VideoEncoder* encoder = (__bridge VideoEncoder*)outputCallbackRefCon;//3.判断是否是关键帧bool isKeyframe = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true),0), kCMSampleAttachmentKey_NotSync);//判断当前帧是否为关键帧//获取sps & pps数据if(isKeyframe){//获取编码后的信息CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);//获取SPS信息size_t sparameterSetSize, sparameterSetCount;const uint8_t *sparameterSet;CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );// 获取PPS信息size_t pparameterSetSize, pparameterSetCount;const uint8_t *pparameterSet;CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );//装sps/pps转成NSData,以便写入文件NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];//写入文件[encoder gotSpsPps:sps pps:pps];}// 获取数据块CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);size_t length, totalLength;char *dataPointer;OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);if (statusCodeRet == noErr) {size_t bufferOffset = 0;static const int AVCCHeaderLength = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length// 循环获取nalu数据while (bufferOffset < totalLength - AVCCHeaderLength) {uint32_t NALUnitLength = 0;// Read the NAL unit lengthmemcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);// 从大端转系统端NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];[encoder gotEncodedData:data isKeyFrame:isKeyframe];// 移动到写一个块,转成NALU单元bufferOffset += AVCCHeaderLength + NALUnitLength;}}
}- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{// 1.拼接NALU的headerconst char bytes[] = "\x00\x00\x00\x01";size_t length = (sizeof bytes) - 1;NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];// 2.将NALU的头&NALU的体写入文件[self.fileHandle writeData:ByteHeader];[self.fileHandle writeData:sps];[self.fileHandle writeData:ByteHeader];[self.fileHandle writeData:pps];}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{NSLog(@"gotEncodedData %d", (int)[data length]);if (self.fileHandle != NULL){const char bytes[] = "\x00\x00\x00\x01";size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];[self.fileHandle writeData:ByteHeader];[self.fileHandle writeData:data];}
}- (void)endEncode {VTCompressionSessionCompleteFrames(self.compressionSession, kCMTimeInvalid);VTCompressionSessionInvalidate(self.compressionSession);CFRelease(self.compressionSession);self.compressionSession = NULL;
}@end
(3)在主页面ViewController.m中,加入点击开始采集和结束采集的按钮并实现对应方法:
@interface ViewController () /** 视频捕捉对象 */
@property (nonatomic, strong) VideoCapture *videoCapture;@end@implementation ViewController- (IBAction)startCapture {[self.videoCapture startCapture:self.view];
}- (IBAction)stopCapture {[self.videoCapture stopCapture];
}- (VideoCapture *)videoCapture {if (_videoCapture == nil) {_videoCapture = [[VideoCapture alloc] init];}return _videoCapture;
}
demo源码下载:https://github.com/dolacmeng/encodeWithVideoToolBox
相关文章:

nRF51800 蓝牙学习 进程记录 2:关于二维数组 执念执战
前天在玩OLED时想完成一直想弄得一个东西,就是简单的单片机游戏。因为STM32和nRF51822的内存足够,所以就用缓存数组的方法来显示图像(我也不知道术语是啥,反正就是在内存中建立一个128X64的二维数组,更新显示时将整个数…

.net卸载程序制作
.net卸载程序制作 原文:.net卸载程序制作方法一: 在打包项目中添加文件msiexec.exe(一般在c:\windows\system32(系统目录中)找到)。 在文件系统视图中选择应用程序文件,在msiexec.exe上单击右键选择“创建快捷方式”,重命名快捷方式为“unins…

Windows下的DNS命令用法
“Windows下DNS相关命令的用法。”在协议分析过程中,经常会遇到一种情况,一次对某域名抓包的过程中,抓到了某个域名的DNS请求,之后再抓包,却抓不到的情况。这时候就需要DNS命令出马了,本文介绍Windows下的D…

Kafka集群配置说明
#kafka数据的存放地址,多个地址的话用逗号分 log.dirs/tmp/kafka-logs #broker server服务端口 port9092 #这个参数会在日志segment没有达到log.segment.bytes设置的大小,也会强制新建一个segment会被 topic创建时的指定参数覆盖 log.roll.hours24 #是否…

Tomcat漏洞说明与安全加固
Tomcat是Apache软件基金会的一个免费的、开放源码的WEB应用服务器,可以运行在Linux和Windows等多个平台上,由于其性能稳定、扩展性好、免费等特点深受广大用户的喜爱。目前,互联网上绝大多数JAVA WEB应用都运行在Tomcat服务器上。 Tomcat作为…

《人性的优点》笔记
1.相信自己,做一个成功的人 2.《圣经》中说:“攻克己心,强如攻城” 3.人最大的敌人,不是别人,正是自己 4.不要为木已成舟的事情耗费太多的心血,你无法改变它 5.忧虑是健康的大敌,它只会让你的生…

SIP协议分析
“ 音视频通话控制协议SIP介绍。”SIP(Session Initiation Protocol),即会话发起协议,在RFC2543、RFC3261等中被定义,是一个VOIP信令协议,其目的是在IP网络中实现电话功能,即软电话功能。在互联…

Struts2的工作原理
Struts2是在Struts1的基础上发展而来的,Struts是WebWork和Struts1的集合,采用的正是WebWork的核心,更多的是WebWork。 下载的Struts2源代码文件 主要的包和类: 包名 说明 org.apache.struts2. components 该包封装视图组件&…

Tiny4412 Uboot
1. Build uboot a) 安装好toolchain (arm-linux-gcc-4.5.1-v6-vfp-20120301.tgz)并设置好 环境变量PATH,保证可以正常使用。 b) 解压 uboot_tiny4412-20130729.tgz 并进入相应的目录 tar xzf uboot_tiny4412-20130729.tgz c) 配置 uboot 并编译 cd uboot_tiny4412 m…

iOS逆向(1)——利用ipa重签名,3分钟iPhone安装多个微信
本文要达成如图效果,在一台iPhone上安装第二个微信: 准备: Xcode微信ipa(可通过iTool进行下载)重签名脚本 步骤 打开Xcode,新建Single View App项目,名字可以随意,这里就用Wech…

使用Fiddler进行HTTP流量分析
“ Fiddler抓包工具使用。”Fiddler作为一个PC端的HTTP/HTTPS协议分析工具,能够抓取PC上的流量,并且它对HTTP类数据的分析,要比Wireshark要简单,友好,它对数据的组织格式很好地提高了分析效率。本文介绍如何在PC上使用…

Java学习笔记(二一)——Java 泛型
【前面的话】 最近脸好干,掉皮,需要买点化妆品了。 Java泛型好好学习一下。 【定义】 一、泛型的定义主要有以下两种: 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象。&#x…

GitHub与Git入门
一、GitHub GitHub为开发者提供Git仓库的托管服务,可以进行代码共享、团队协同开发,创建了社会化(social coding)编程的概念。 二、GitHub与Git的区别 开发者将源代码存入“Git”仓库,而GitHub则在网络上提供Git仓库…

《UML大战需求分析》阅读笔记1
通过阅读本书的序和第一章,让我对于UML的理解更加深刻了,并且懂了怎样把你UML学的更好。 作者先让我们明白什么是UML,大概知道了UML各个图的形态和各种用途,然后再详细的介绍各个图怎样使用。 UML是个非必要的建模工具,…

裸奔的支付X聊天,你还敢用吗?
“ 一直想在社交领域突破的某付宝,却自始至终对社交功能如此的不用心,让用户的数据在网络中裸奔,使用户不寒而栗。”没错,这篇文章要说的就是BAT中A家的支付X,那个千方百计做社交的支付工具。如果你使用了它的聊天窗口…

MVC3项目依赖文件错误解决
MVC3的项目依赖分为两大类: 1、ASP.NET Web Pages 2、ASP.NET MVC 3 如果没有正确引入,或者项目的版本有错误会出现程序集引用错误。 在服务器上部署时,解决思路如下: 1、下载MVC3的安装包,然后在服务器上安装…

让自己的开源项目支持CocoaPods集成
平时我们会经常用CocoaPods集成第三方库,那如何使自己的代码也可以通过CocoaPods集成呢?只需要简单几步: 创建git仓库,把代码提交到Github或码云等在git仓库中创建.Podspec文件,修改里面的配置(如代码的版…

由于客户端检测到一个协议错误 代码0x1104
重新连接N次都还是这个错误提示,最后再重起电脑,还是没用。研究了一下错误终于解决了。 首先检查远程连接端口对不对?Windows远程默认的连接端口是3389,一般大家连接时直接输入IP或域名就可以连接了。如果还要加:端口号的话&#…

使用Fiddler抓取手机HTTP流量包
“ Fiddler手机流量抓包实战。”Fiddler作为一个PC端的HTTP/HTTPS协议分析工具,不仅能够抓取PC上的流量,还能作为代理,抓取手机流量,在之前的文章《使用Fiddler进行HTTP流量分析》中介绍了Fiddler的基本用法,本文介绍如…

Spark Steaming 点滴
Spark Streaming 模块是对于 Spark Core 的一个扩展,目的是为了以高吞吐量,并且容错的方式处理持续性的数据流。目前 Spark Streaming 支持的外部数据源有 Flume、 Kafka、Twitter、ZeroMQ、TCP Socket 等。Discretized Stream 也叫 DStream) 是 Spark S…

CocoaPods远程私有库
上一篇(让自己的开源项目支持CocoaPods集成)介绍了将自己开发的框架代码发布到Cocoapods,全球的开发者都可以通过pod search搜索到我们的框架代码以及通过pod install进行安装。但有时候我们希望只有我们项目内部的人才可以集成和修改&#x…

大数据平台的秘密
大数据,这个词越来越热,很多人都在谈大数据,其实很多张口闭口大数据的人,或许都不知道数据是如何产生、传递、存储、运算到应用。有段时间,看到一些大数据文章,就感觉纯属凑热闹,小数据都没搞明…

Fiddler使用技巧:强大的数据文本编解码功能
“ 郑重推荐Fiddler工具自带的TextWizard功能。”Fiddler作为一个HTTP类协议的抓包分析工具,之前已介绍过抓包分析功能,可参考文章:《使用Fiddler进行HTTP流量分析》《使用Fiddler抓取手机HTTP流量》在抓包分析功能之外,我们一定不…

jquery validate 详解一
jQuery校验 官网地址:http://bassistance.de/jquery-plugins/jquery-plugin-validation 一导入js库 <script src"../js/jquery.js" type"text/javascript"></script><script src"../js/jquery.validate.js" type"…

安装VMWare tools,以及解决安装后/mnt中有hgfs但没共享文件的方法
一、首先是安装VMWare tools安装过程可参考:http://www.cnblogs.com/jiu0821/p/7559949.html二、解决安装VMWare tools后/mnt中有hgfs但没共享文件 前提:在虚拟机软件中设置了共享目录 此时在linux中进入 /mnt/hgfs 文件夹,但发现共享的文件没…

音乐(文件)断点下载
这篇文章介绍音乐等文件的下载,支持断点续传。 我们需要创建两个类 HYDownLoader:音乐下载的主类,可以进行新建下载、暂停下载、取消下载等。 HYFileTool:文件管理类,主要为HYDownLoader服务,可以判断文件…

php设计模式
原型模式(prototype) PHP设计模式之:原型模式 刚刚了解了原型模式,通过资料以及自身了解整合以下资料: 原型模式通过复制已经存在的实例来返回新的实例,而不是新建实例,并且原型(被复…

TextKit及应用
在iOS开发中我们常常使用UIKit的UITextView、UITextField、UILabel来显示文字。它们底层都是基于一个叫做TextKit的强大引擎。通过TextKit,我们可以方便地修改文字的样式和排版,而不需要直接操作复杂的Core Text。 1.什么是TextKit 在iOS7中,…

判断类之间的父子关系
如何判断两个类之间的父子关系? java为我们提供了instanceof运算符,可以用来判断一个对象是否是否个类的实例,所以很容易的想到子类的对象肯定是父类的实例。但是如何所涉及到的类是不可实例化的该怎么办呢?好在java的Class为我们…

顺F速运,你被爱加M坑了
“ 顺F速运APP安全性分析。”之前的文章《Wireshark分析实战:某达速递登录帐号密码提取》对某达速递的APP进行了分析,该APP将数据完全暴露于网络流量中。于是对快递行业老大顺F速运的APP进行了分析,发现该APP质量还算上乘,但是&am…