IOS解码MP4播放

From: http://hawk0620.github.io/blog/2015/11/17/ios-play-video/

从体验说起

  对比微信和Instagram可以发现播放视频的两个思路:微信的处理是把视频加载好后播放,这样确保了视频是完整的,用户很直观视频是否下载完成,不影响用户观看视频的体验;而Instagram的做法是边加载边播,当网络不给力的时候,视频就卡在那里,给用户增加了观看视频的焦虑,并且用户还得自己判断下视频是不是加载完成了,最不幸的是,当视频的网络请求不可达时,不能给出加载失败的提示引导用户重新加载,只能滑动列表触发刷新。

播放视频的实现

1、通过实践,我发现Instagram采用的应该是AVPlayer实现的。

AVPlayerItem可以通过远程URL创建出来,并且支持流的形式播放,还可以添加视频播放卡住和视频播放完成的两个观察者:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidBufferPlaying:) name:AVPlayerItemPlaybackStalledNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidFinishPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

但遗憾的是,我没有找到视频加载失败的观察者。

2、结合微信团队的技术分享[链接][1],得知微信的视频播放是采用AVAssetReader+AVAssetReaderTrackOutput,根据微信的思路,自己也尝试实现了一番: buffer的转换:

[1]: http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207686973&idx=1&sn=1883a6c9fa0462dd5596b8890b6fccf6&scene=0#wechat_redirect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (CGImageRef) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// Lock the base address of the pixel buffer
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// Get the number of bytes per row for the pixel buffer
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// Get the pixel buffer width and height
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
//Generate image to edit
unsigned char* pixel = (unsigned char *)CVPixelBufferGetBaseAddress(imageBuffer);
CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();
CGContextRef context=CGBitmapContextCreate(pixel, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little|kCGImageAlphaPremultipliedFirst);
CGImageRef image = CGBitmapContextCreateImage(context);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return image;
}

视频的解码:

AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:path] options:nil];
NSError *error;
AVAssetReader* reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack* videoTrack = [videoTracks objectAtIndex:0];

int m_pixelFormatType = kCVPixelFormatType_32BGRA;
NSDictionary* options = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt: (int)m_pixelFormatType] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput* videoReaderOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];
[reader addOutput:videoReaderOutput];
[reader startReading];

// 读取视频每一个buffer转换成CGImageRef
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   CMSampleBufferRef audioSampleBuffer = NULL;
   while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
   CMSampleBufferRef sampleBuffer = [videoReaderOutput copyNextSampleBuffer];
   CGImageRef image = [self imageFromSampleBuffer:sampleBuffer];
   if (self.delegate && [self.delegate respondsToSelector:@selector(mMovieDecoder:onNewVideoFrameReady:)]) {
        [self.delegate mMovieDecoder:self onNewVideoFrameReady:image];
    }
   if(sampleBuffer) {
       if(audioSampleBuffer) { // release old buffer.
            CFRelease(audioSampleBuffer);
            audioSampleBuffer = nil;
       }
       audioSampleBuffer = sampleBuffer;
   } else {
       break;
   }

// 休眠的间隙刚好是每一帧的间隔
   [NSThread sleepForTimeInterval:CMTimeGetSeconds(videoTrack.minFrameDuration)];
 }
 // decode finish
 float durationInSeconds = CMTimeGetSeconds(asset.duration);
  if (self.delegate && [self.delegate respondsToSelector:@selector(mMovieDecoderOnDecodeFinished:duration:)]) {
     [self.delegate mMovieDecoderOnDecodeFinished:self duration:durationInSeconds];
   }
});

处理每一帧CGImageRef的回调:

- (void)mMovieDecoder:(VideoDecoder *)decoder onNewVideoFrameReady:(CGImageRef)imgRef {
    __weak PlayBackView *weakView = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        weakView.layer.contents = (__bridge id _Nullable)(imgRef);
    });
}

处理视频解码完成的回调:

images即每一帧传上来的CGImageRef的数组
- (void)mMovieDecoderOnDecodeFinished:(VideoDecoder *)decoder images:(NSArray *)imgs duration:(float)duration {
    __weak PlayBackView *weakView = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        weakView.layer.contents = nil;

        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath: @"contents"];
        animation.calculationMode = kCAAnimationDiscrete;
        animation.duration = duration;
        animation.repeatCount = HUGE; //循环播放
        animation.values = images; // NSArray of CGImageRefs
        [weakView.layer addAnimation:animation forKey: @"contents"];
    });
}

//写在最后: 看到多种这样的方式,基本出处差不多, 然后发现,10秒的视频还行 超过的话基本上都会爆内存 , 30fps 算的话 10秒300张图片, 我现在的处理方式是间隔取图 ,想当于减少fps

iOS中block的循环引用问题

From: http://www.jianshu.com/p/492be28d63c4

本文主要介绍ARC下block的循环引用问题,举例说明引起循环引用的场景和相应的解决方案。

在讲block的循环引用问题之前,我们需要先了解一下iOS的内存管理机制和block的基本知识

iOS的内存管理机制

Objective-C在iOS中不支持GC(垃圾回收)机制,而是采用的引用计数的方式管理内存。

引用计数(Reference Count)

在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。

我们通过开关房间的灯为例来说明引用计数机制。

引用《Pro Multithreading and Memory Management for iOS and OS X》中的图片

图中,“需要照明的人数”即对应我们要说的引用计数值。

  1. 第一个人进入办公室,“需要照明的人数”加1,计数值从0变为1,因此需要开灯;
  2. 之后每当有人进入办公室,“需要照明的人数”就加1。如计数值从1变成2;
  3. 每当有人下班离开办公室,“需要照明的人数”加减1如计数值从2变成1;
  4. 最后一个人下班离开办公室时,“需要照明的人数”减1。计数值从1变成0,因此需要关灯。

在Objective-C中,”对象“相当于办公室的照明设备,”对象的使用环境“相当于进入办公室的人。上班进入办公室的人对办公室照明设备发出的动作,与Objective-C中的对应关系如下表

对照明设备所做的动作对Objective-C对象所做的动作

开灯
生成对象

需要照明
持有对象

不需要照明
释放对象

关灯
废弃对象

使用计数功能计算需要照明的人数,使办公室的照明得到了很好的管理。同样,使用引用计数功能,对象也就能得到很好的管理,这就是Objective-C内存管理,如下图所示

引用《Pro Multithreading and Memory Management for iOS and OS X》中的图片

MRC(Manual Reference Counting)中引起应用计数变化的方法

Objective-C对象方法说明

alloc/new/copy/mutableCopy
创建对象,引用计数加1

retain
引用计数加1

release
引用计数减1

dealloc
当引用计数为0时调用

[NSArray array]
引用计数不增加,由自动释放池管理

[NSDictionary dictionary]
引用计数不增加,由自动释放池管理

自动释放池

关于自动释放,不是本文的重点,这里就不讲了。

ARC(Automatic Reference Counting)中内存管理

Objective-C对象所有权修饰符说明

__strong
对象默认修饰符,对象强引用,在对象超出作用域时失效。其实就相当于retain操作,超出作用域时执行release操作

__weak
弱引用,不持有对象,对象释放时会将对象置nil。

__unsafe_unretained
弱引用,不持有对象,对象释放时不会将对象置nil。

__autoreleasing
自动释放,由自动释放池管理对象

block的基本知识

block的基本知识这里就不细说了,可以看看我的文章说说Objective-C中的block

循环引用问题

两个对象相互持有,这样就会造成循环引用,如下图所示

两个对象相互持有

图中,对象A持有对象B,对象B持有对象A,相互持有,最终导致两个对象都不能释放。

block中循环引用问题

由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。如下,

typedef void(^block)();

@property (copy, nonatomic) block myBlock;
@property (copy, nonatomic) NSString *blockString;

- (void)testBlock {
    self.myBlock = ^() {
        //其实注释中的代码,同样会造成循环引用
        NSString *localString = self.blockString;
          //NSString *localString = _blockString;
          //[self doSomething];
    };
}

注:以下调用注释掉的代码同样会造成循环引用,因为不管是通过self.blockString还是_blockString,或是函数调用[self doSomething],因为只要 block中用到了对象的属性或者函数,block就会持有该对象而不是该对象中的某个属性或者函数。

当有someObj持有self对象,此时的关系图如下。

当someObj对象release self对象时,self和myblock相互引用,retainCount都为1,造成循环引用

解决方法:

__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
    NSString *localString = weakSelf.blockString;
};

使用__weak修饰self,使其在block中不被持有,打破循环引用。开始状态如下

当someObj对象释放self对象时,Self的retainCount为0,走dealloc,释放myBlock对象,使其retainCount也为0。

其实以上循环引用的情况很容易发现,因为此时Xcode就会报警告。而发生在多个对象间的时候,Xcode就检测不出来了,这往往就容易被忽略。

//ClassB
@interface ClassB : NSObject
@property (strong, nonatomic) ClassA *objA;
- (void)doSomething;
@end

//ClassA
@property (strong, nonatomic) ClassB *objB;
@property (copy, nonatomic) block myBlock;

- (void)testBlockRetainCycle {
    ClassB* objB = [[ClassB alloc] init];
    self.myBlock = ^() {
        [objB doSomething];
    };
    objB.objA = self;
}

解决方法:

- (void)testBlockRetainCycle {
    ClassB* objB = [[ClassB alloc] init];
    __weak typeof(objB) weakObjB = objB;
    self.myBlock = ^() {
        [weakObjB doSomething];
    };
    objB.objA = self;
}

将objA对象weak,使其不在block中被持有

注:以上使用__weak打破循环的方法只在ARC下才有效,在MRC下应该使用__block

或者,在block执行完后,将block置nil,这样也可以打破循环引用

- (void)testBlockRetainCycle {
    ClassB* objB = [[ClassB alloc] init];
    self.myBlock = ^() {
        [objB doSomething];
    };
    objA.objA = self;
    self.myBlock();
    self.myBlock = nil;
}

这样做的缺点是,block只会执行一次,因为block被置nil了,要再次使用的话,需要重新赋值。

一些不会造成循环引用的block

在开发工程中,发现一些同学并没有完全理解循环引用,以为只要有block的地方就会要用__weak来修饰对象,这样完全没有必要,以下几种block是不会造成循环引用的。

大部分GCD方法

dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});

因为self并没有对GCD的block进行持有,没有形成循环引用。目前我还没碰到使用GCD导致循环引用的场景,如果某种场景self对GCD的block进行了持有,则才有可能造成循环引用。

block并不是属性值,而是临时变量

- (void)doSomething {
    [self testWithBlock:^{
        [self test];
    }];
}

- (void)testWithBlock:(void(^)())block {
    block();
}

- (void)test {
    NSLog(@"test");
}

这里因为block只是一个临时变量,self并没有对其持有,所以没有造成循环引用

block使用对象被提前释放

看下面例子,有这种情况,如果不只是ClassA持有了myBlock,ClassB也持有了myBlock。

当ClassA被someObj对象释放后

此时,ClassA对象已经被释放,而myBlock还是被ClassB持有,没有释放;如果myBlock这个时被调度,而此时ClassA已经被释放,此时访问的ClassA将是一个nil对象(使用__weak修饰,对象释放时会置为nil),而引发错误。

另一个常见错误使用是,开发者担心循环引用错误(如上所述不会出现循环引用的情况),使用__weak。比如

__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
    [weakSelf doSomething];
});

因为将block作为参数传给dispatch_async时,系统会将block拷贝到堆上,而且block会持有block中用到的对象,因为dispatch_async并不知道block中对象会在什么时候被释放,为了确保系统调度执行block中的任务时其对象没有被意外释放掉,dispatch_async必须自己retain一次对象(即self),任务完成后再release对象(即self)。但这里使用__weak,使dispatch_async没有增加self的引用计数,这使得在系统在调度执行block之前,self可能已被销毁,但系统并不知道这个情况,导致block执行时访问已经被释放的self,而达不到预期的结果。

注:如果是在MRC模式下,使用__block修饰self,则此时block访问被释放的self,则会导致crash。

该场景下的代码

// ClassA.m
- (void)test {
    __weak MyClass* weakSelf = self;
    double delayInSeconds = 10.0f;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        NSLog(@"%@", weakSelf);
    });
}

// ClassB.m
- (void)doSomething {
    NSLog(@"do something");
    ClassA *objA = [[ClassA alloc] init];
    [objA test];
}

运行结果

[5988:435396] do something
[5988:435396] self:(null)

解决方法:

对于这种场景,就不应该使用__weak来修饰对象,让dispatch_after对self进行持有,保证block执行时self还未被释放。

block执行过程中对象被释放

还有一种场景,在block执行开始时self对象还未被释放,而执行过程中,self被释放了,此时访问self时,就会发生错误。

对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有,block执行结束后,解除其持有。

- (void)testBlockRetainCycle {
    ClassA* objA = [[ClassA alloc] init];
    __weak typeof(objA) weakObjA = objA;
    self.myBlock = ^() {
        __strong typeof(weakObjA) strongWeakObjA = weakObjA;
        [strongWeakObjA doSomething];
    };
    objA.objA = self;
}

注:此方法只能保证在block执行期间对象不被释放,如果对象在block执行执行之前已经被释放了,该方法也无效。

GPUImage自定义滤镜

From: http://www.itiger.me/?p=143

Tiger | iOS |

GPUImage 是一个基于 GPU 图像和视频处理的开源 iOS 框架。由于使用 GPU 来处理图像和视频,所以速度非常快,它的作者 BradLarson 称在 iPhone4 上其处理速度是使用 CPU 来处理的 100 倍 (CoreImage 也能使用 GPU 来处理图像,但我觉得 CoreImage 还是慢)。除了速度上的优势,GPUImage 还提供了很多很棒的图像处理滤镜,但有时候这些基本功能仍然无法满足实际开发中的需求,不用担心 GPUImage 支持自定义滤镜。

GPUImage 自定义滤镜需要使用 OpenGL 着色语言( GLSL )编写 Fragment Shader(片段着色器),除此之外你可能还需要一点点图像处理相关的知识。下面我将尝试通过 GPUImage 中的 GPUImageColorInvertFilter(反色滤镜)来讲解一下它的运作过程。

先看[.h 文件:][4]

[4]: https://github.com/BradLarson/GPUImage/blob/2f8e61c4ffcdfa93ee3d8f39f8110e61cfc3daf1/framework/Source/GPUImageColorInvertFilter.h

#import “GPUImageFilter.h”

@interface GPUImageColorInvertFilter : GPUImageFilter
{
}

@end

很简单,可以看出 GPUImageColorInvertFilter 是继承了 GPUImageFilter

然后看 [.m 文件][5] 中 @implementation 之前的一段代码

[5]: https://github.com/BradLarson/GPUImage/blob/2f8e61c4ffcdfa93ee3d8f39f8110e61cfc3daf1/framework/Source/GPUImageColorInvertFilter.m
1
2
3
4
5
6
7
8
9
10
11
12
13
NSString *const kGPUImageInvertFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.a);
}
);

第 1 行,可以看到 SHADER_STRING 宏中包含着我们的 Shader (着色器)代码,我们的着色器字符串赋给一个 const NSString 对象(这个常量将在 GPUImageFilter 及其子类的初始化过程中用来设置 filter)。

第 2、3 行声明了两个变量。

varying 变量是Vertex 和 Fragment Shader(顶点着色器和片段着色器)之间做数据传递用的,一般 Vertex Shader(顶点着色器) 修改 varying 变量的值,然后 Fragment Shader(片段着色器)使用该varying变量的值。因此varying 变量在 Vertex 和 Fragment Shader 中声明必须一致。放到这里,也就是说 textureCoordinate 必须叫这个名字不能改。

highp 声明 textureCoordinate 精度(相应的还有mediumplowp)。

vec2 声明textureCoordinate 是一个二维向量。

uniform 声明 inputImageTexture 是外部程序传递给 Shader 的变量, Shader 程序内部只能用,不能改。 sampler2D 声明变量是一个2D纹理。

第 4 行,相信你并不陌生,没错儿 Shader 也是从 main() 函数开始执行的。

第 5 行,texture2D 纹理取样器,根据纹理坐标返回纹理单元的值。

第 6 行,(1.0 - textureColor.rgb)textureColor也就是原图的 RGB 值,做了一个向量的减法,这是图像的反色算法,然后把经过反色的 RGB 值和原图的 Alpha 值组成一个新的 vec4(四维向量)值赋给 gl_FragColorgl_FragColor 是 Fragment Shader 预先定义的变量,赋给它的值就是该片段最终的颜色值。

Shader 到这里已经解释完了,可能你依然云里雾里,我来说一下我对这部分功能的理解,可能不对,但目前是管用的,方便你理解:GPUImage 中应该有一个 Vertex Shader,它对图像逐个像素扫描,通过 textureCoordinate 变量将当前的扫描坐标传递给我们的 Fragment Shader,inputImageTexture 包含我们要处理的图像的全部信息,在 Shader 程序内部通过 texture2D 得到 inputImageTexture 在当前位置 textureCoordinate 的 RGBA 值,运用图像处理知识,算出想要的新的 RGBA 值,把结果值赋给 gl_FragColor就算完成了。

现在我们继续看代码,在 Shader 之后是 GPUImageColorInvertFilter 的实现:

@implementation GPUImageColorInvertFilter

- (id)init;
{
    if (!(self = [super initWithFragmentShaderFromString:kGPUImageInvertFragmentShaderString]))
    {
        return nil;
    }

    return self;
}

@end     

很简单,就是使用刚才的着色器代码来设置 filter。这样一个新的滤镜就诞生了~

网路上关于 GPUImage 自定义滤镜 和 GLSL 的资料不是特别多,我斗胆把自己摸索到的理解在这里和你分享,希望对你有所帮助,如有错误指出欢迎指出,另外,可以去这里查阅 OpenGL GLSL 文档,玩的愉快 (●°u°●)​ 」

Custom, Filter, Fragment, GLSL, GPUImage, iOS, OpenGL, Shader, Vertex, 图像处理, 滤镜, 自定义

iOS 自定义相机, UIImagePickerController && AVCaptureSession (附微信小视频模仿demo)

iOS 自定义相机, UIImagePickerController && AVCaptureSession (附微信小视频模仿demo)

From: http://www.jianshu.com/p/fe37cab68f7d

今天介绍自定义相机的两种方式,一种是UIImagePickerController,一种是AVCaptureSession.

UIImagePickerController

UIImagePickerController非常方便简单,是苹果自己封装好了的一套API,效果如下:

AVCaptureSession

但是上面的 API只能进行简单的相机视图修改,有时无法满足我们的需求.例如我们需要更加复杂的OverlayerView(自定义相机视图),这时候我们就要自定义一个相机了.AVCaptureSession能帮助我们.

心急的人,完整Demo在最下边儿.其他朋友可依次看下去

UIImagePickerController

属性

首先看到,需要遵守的协议有2个,分别是UINavigationControllerDelegateUIImagePickerControllerDelegate

@property(nullable,nonatomic,weak)      id <UINavigationControllerDelegate, UIImagePickerControllerDelegate> delegate;    //需要遵守的协议有2个

还有一些基本设置,如输入源/媒体类型,还有allowsEditing为 YES 即可允许用户拍照后立即进行编辑,并且出现编辑框

//基本设置
@property(nonatomic)           UIImagePickerControllerSourceType     sourceType;            //图片输入源,相机或者相册,图库
@property(nonatomic,copy)      NSArray<NSString *>                   *mediaTypes;            //媒体类型
@property(nonatomic)           BOOL                                  allowsEditing             //是否允许用户编辑
@property(nonatomic)           BOOL                                  allowsImageEditing     //废弃,用上面的即可

这2个属性和视频相关

@property(nonatomic)           NSTimeInterval                        videoMaximumDuration     //最长摄制时间
@property(nonatomic)           UIImagePickerControllerQualityType    videoQuality             //视频摄制质量

这2个和自定义相机视图有关,注意,下面的只有当输入源为UIImagePickerControllerSourceTypeCamera才可用,否则崩溃

@property(nonatomic)           BOOL                                  showsCameraControls     //默认 YES, 设置为 NO 即可关闭所有默认 UI
@property(nullable, nonatomic,strong) __kindof UIView                *cameraOverlayView      //自定义相机覆盖视图
@property(nonatomic)           CGAffineTransform                     cameraViewTransform     //拍摄时屏幕view的transform属性,可以实现旋转,缩放功能

@property(nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode             //设置模式为拍照或者摄像
@property(nonatomic) UIImagePickerControllerCameraDevice      cameraDevice                  //默认开启前置/后置摄像头
@property(nonatomic) UIImagePickerControllerCameraFlashMode   cameraFlashMode                //设置默认的闪光灯模式,有开/关/自动

方法

第一个是判断输入源的,一般用来判断是否支持拍照

typedef NS_ENUM(NSInteger, UIImagePickerControllerSourceType) {
    UIImagePickerControllerSourceTypePhotoLibrary,        //图库
    UIImagePickerControllerSourceTypeCamera,            //相机-->这个就是拍照
    UIImagePickerControllerSourceTypeSavedPhotosAlbum    //相册
};

+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType;              //判断是否支持某种输入源

这个是判断是否支持前/后置摄像头

typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraDevice) {
    UIImagePickerControllerCameraDeviceRear,            //后置
    UIImagePickerControllerCameraDeviceFront            //前置
};

+ (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice     //是否支持前/后置摄像头

拍照/摄像的方法

- (void)takePicture;        //手动调取拍照的方法,也可当做`-imagePickerController:didFinishPickingMediaWithInfo:`代理的回调,无法捕捉动态图像
- (BOOL)startVideoCapture;    //开始摄像
- (void)stopVideoCapture;    //停止摄像

代理相关

注意,UIImagePickerController不能直接取消,必须在收到以下代理方法后才可以 dismiss

@protocol UIImagePickerControllerDelegate<NSObject>
@optional

//这个已经废弃了,不用管
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo;


//主要是下面这2个
//拍照后,点击使用照片和向此选取照片后都会调用以下代理方法
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info;

//点击取消的时候会调用,通常在此 dismiss 
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;

@end

保存照片/视频到手机,三个比较重要的方法,记得导入AssetsLibrary.framework,并且在需要调用存储方法的的地方导入<AssetsLibrary/AssetsLibrary.h>

// 将照片保存到相册,回调方法必须用 - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo;
UIKIT_EXTERN void UIImageWriteToSavedPhotosAlbum(UIImage *image, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo);


//是否可以将视频保存到相册,通常在调用下面的方法之前会调用这个
UIKIT_EXTERN BOOL UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) NS_AVAILABLE_IOS(3_1);

// 将视频保存到相册,回调方法必须用 - (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo; 
UIKIT_EXTERN void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo) NS_AVAILABLE_IOS(3_1);

用法如下

- (void)savePic
{
    UIImageWriteToSavedPhotosAlbum(imgPath, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);    
}


- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{

    if (error) {
        NSLog(@"保存照片过程中发生错误,错误信息:%@",error.localizedDescription);
    }else{
        NSLog(@"照片保存成功.");
    }
}

- (void)saveVideo
{
    UISaveVideoAtPathToSavedPhotosAlbum(videoPath,self, @selector(video:didFinishSavingWithError:contextInfo:), nil);//保存视频到相簿    
}

- (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
    if (error) {
        NSLog(@"保存视频过程中发生错误,错误信息:%@",error.localizedDescription);
    }else{
        NSLog(@"视频保存成功.");
    }
}

另外一个是ALAssetsLibrary,不过在 iOS9.0 已经全部被废弃了,改成了PHPhotoLibrary(这是 Photos 框架的一个组成部分,完整的可以点这个链接,关于这个框架的用法,改天再写一个吧)

9.0及以前的写法

//保存照片到相册
 [[[ALAssetsLibrary alloc]init] writeImageToSavedPhotosAlbum:[img CGImage] orientation:(ALAssetOrientation)img.imageOrientation completionBlock:^(NSURL *assetURL, NSError *error) {
    if (error) {
        NSLog(@"Save image fail:%@",error);
    }else{
        NSLog(@"Save image succeed.");
    }
}];

//保存视频到相册
[[[ALAssetsLibrary alloc]init] writeVideoAtPathToSavedPhotosAlbum:[NSURL URLWithString:videoPath] completionBlock:^(NSURL *assetURL, NSError *error) {

    if (error) {
        NSLog(@"Save video fail:%@",error);
    }else{
        NSLog(@"Save video succeed.");
    }

}];

AVCaptureSession

先放上一个微信小视频的模仿 Demo, 因为只能真机,所以比较嘈杂~大家别介意

流程

AVCaptureSession通过把设备的麦克风/摄像头(AVCaptureDevice)实例化成数据流输入对象(AVCaptureDeviceInput)后,再通过建立连接(AVCaptionConnection)将录制数据通过数据流输出对象(AVCaptureOutput)导出,而录制的时候咱们可以同步预览当前的录制界面(AVCaptureVideoPreviewLayer).

`AVCaptureSession`是一个会话对象,是设备音频/视频整个录制期间的管理者.
`AVCaptureDevice`其实是咱们的物理设备映射到程序中的一个对象.咱们可以通过其来操作:闪光灯,手电筒,聚焦模式等
`AVCaptureDeviceInput`是录制期间输入流数据的管理对象.
`AVCaptionConnection`是将输入流/输出流连接起来的连接对象,视频/音频稳定,预览与录制方向一致都在这里设置,还有audioChannels声道
`AVCaptureOutput`是输出流数据的管理对象,通过头文件可以看到有很多子类,而我们通常也使用其子类
`AVCaptureVideoPreviewLayer`是一个 `CALyer` ,可以让我们预览拍摄过程中的图像

详解各部分

此处,咱们就模仿一个微信小视频的 demo, 然后把各个主要步骤写在下面.

授权

首先获取授权AVAuthorizationStatus,我们需要获取哪个设备的使用权限,就进行请求,需要注意的是,如果用户未进行授权授权选择,咱们还要重复请求一次.

//获取授权
- (void)getAuthorization
{

    //此处获取摄像头授权
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo])
    {
        case AVAuthorizationStatusAuthorized:       //已授权,可使用    The client is authorized to access the hardware supporting a media type.
        {
            break;
        }
        case AVAuthorizationStatusNotDetermined:    //未进行授权选择     Indicates that the user has not yet made a choice regarding whether the client can access the hardware.
        {
            //则再次请求授权
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if(granted){    //用户授权成功
                    return;
                } else {        //用户拒绝授权
                    return;
                }
            }];
            break;
        }
        default:                                    //用户拒绝授权/未授权
        {
            break;
        }
    }

}

相关枚举

/*
     AVAuthorizationStatusNotDetermined = 0,// 未进行授权选择

     AVAuthorizationStatusRestricted,    // 未授权,且用户无法更新,如家长控制情况下

     AVAuthorizationStatusDenied,       // 用户拒绝App使用

     AVAuthorizationStatusAuthorized,    // 已授权,可使用
     */

根据流程创建对象

AVCaptureSession

先创建本次小视频的会话对象_captureSession,设置视频分辨率.注意,这个地方设置的模式/分辨率大小将影响你后面拍摄照片/视频的大小

- (void)addSession
{
    _captureSession = [[AVCaptureSession alloc] init];

    if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
        [_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
    }
}

相关枚举

/*  通常支持如下格式
 (
 AVAssetExportPresetLowQuality,
 AVAssetExportPreset960x540,
 AVAssetExportPreset640x480,
 AVAssetExportPresetMediumQuality,
 AVAssetExportPreset1920x1080,
 AVAssetExportPreset1280x720,
 AVAssetExportPresetHighestQuality,
 AVAssetExportPresetAppleM4A
 )
 */
AVCaptureDevice

然后咱们要把设备接入进来了,依次是_videoDevice_audioDevice,还有注意,在给会话信息添加设备对象的时候,需要调用_captureSession的一个方法组beginConfigurationcommitConfiguration,最后,咱们把录制过程中的预览图层PreviewLayer也添加进来,设置完毕后,开启会话startRunning–>注意,不等于开始录制,在不再需要使用会话相关时,还需要stopRunning

[_captureSession beginConfiguration];

[self addVideo];
[self addAudio];
[self addPreviewLayer];

[_captureSession commitConfiguration];
[_captureSession startRunning];
video 相关
- (void)addVideo
{

    // 获取摄像头输入设备, 创建 AVCaptureDeviceInput 对象
    _videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];

    [self addVideoInput];
    [self addMovieOutput];
}

相关枚举

/* MediaType
     AVF_EXPORT NSString *const AVMediaTypeVideo                 NS_AVAILABLE(10_7, 4_0);       //视频
     AVF_EXPORT NSString *const AVMediaTypeAudio                 NS_AVAILABLE(10_7, 4_0);       //音频
     AVF_EXPORT NSString *const AVMediaTypeText                  NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeClosedCaption         NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeSubtitle              NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeTimecode              NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeMetadata              NS_AVAILABLE(10_8, 6_0);
     AVF_EXPORT NSString *const AVMediaTypeMuxed                 NS_AVAILABLE(10_7, 4_0);
     */

    /* AVCaptureDevicePosition
     typedef NS_ENUM(NSInteger, AVCaptureDevicePosition) {
     AVCaptureDevicePositionUnspecified         = 0,
     AVCaptureDevicePositionBack                = 1,            //后置摄像头
     AVCaptureDevicePositionFront               = 2             //前置摄像头
     } NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
     */

下面是获取摄像头的方法

#pragma mark 获取摄像头-->前/后

- (AVCaptureDevice *)deviceWithMediaType:(NSString *)mediaType preferringPosition:(AVCaptureDevicePosition)position
{
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:mediaType];
    AVCaptureDevice *captureDevice = devices.firstObject;

    for ( AVCaptureDevice *device in devices ) {
        if ( device.position == position ) {
            captureDevice = device;
            break;
        }
    }

    return captureDevice;
}

//下面这2个也可以获取前后摄像头,不过有一定的风险,假如手机又问题,找不到对应的 UniqueID 设备,则呵呵了
//- (AVCaptureDevice *)frontCamera
//{
//    return [AVCaptureDevice deviceWithUniqueID:@"com.apple.avfoundation.avcapturedevice.built-in_video:1"];
//}
//
//- (AVCaptureDevice *)backCamera
//{
//    return [AVCaptureDevice deviceWithUniqueID:@"com.apple.avfoundation.avcapturedevice.built-in_video:0"];
//}

添加视频输入对象AVCaptureDeviceInput,根据输入设备初始化输入对象,用户获取输入数据,将视频输入对象添加到会话 (AVCaptureSession) 中

- (void)addVideoInput
{
    NSError *videoError;

    _videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_videoDevice error:&videoError];
    if (videoError) {
        NSLog(@"---- 取得摄像头设备时出错 ------ %@",videoError);
        return;
    }

    if ([_captureSession canAddInput:_videoInput]) {
        [_captureSession addInput:_videoInput];
    }
}

接下来是视频输出对象AVCaptureMovieFileOutput及连接管理对象AVCaptureConnection,还有视频稳定设置preferredVideoStabilizationMode,视频旋转方向setVideoOrientation

- (void)addMovieOutput
{
    _movieOutput = [[AVCaptureMovieFileOutput alloc] init];

    if ([_captureSession canAddOutput:_movieOutput]) {
        [_captureSession addOutput:_movieOutput];

        AVCaptureConnection *captureConnection = [_movieOutput connectionWithMediaType:AVMediaTypeVideo];
        if ([captureConnection isVideoStabilizationSupported]) {
            captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
        }
        captureConnection.videoScaleAndCropFactor = captureConnection.videoMaxScaleAndCropFactor;
    }

}

相关枚举

//设置视频旋转方向
/*
 typedef NS_ENUM(NSInteger, AVCaptureVideoOrientation) {
 AVCaptureVideoOrientationPortrait           = 1,
 AVCaptureVideoOrientationPortraitUpsideDown = 2,
 AVCaptureVideoOrientationLandscapeRight     = 3,
 AVCaptureVideoOrientationLandscapeLeft      = 4,
 } NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
 */
audio 相关

咱们再接着添加音频相关的,包括音频输入设备AVCaptureDevice,音频输入对象AVCaptureDeviceInput,并且将音频输入对象添加到会话

- (void)addAudio
{
    NSError *audioError;
    _audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    _audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:_audioDevice error:&audioError];
    if (audioError) {
        NSLog(@"取得录音设备时出错 ------ %@",audioError);
        return;
    }
    if ([_captureSession canAddInput:_audioInput]) {
        [_captureSession addInput:_audioInput];
    }
}
AVCaptureVideoPreviewLayer

通过会话AVCaptureSession创建预览层AVCaptureVideoPreviewLayer,设置填充模式videoGravity,预览图层方向videoOrientation,并且设置 layer 想要显示的位置

- (void)addPreviewLayer
{
    _captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
    _captureVideoPreviewLayer.frame = self.view.layer.bounds;
//    _captureVideoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

    _captureVideoPreviewLayer.connection.videoOrientation = [_movieOutput connectionWithMediaType:AVMediaTypeVideo].videoOrientation;
    _captureVideoPreviewLayer.position = CGPointMake(self.view.width*0.5,self.videoView.height*0.5);

    CALayer *layer = self.videoView.layer;
    layer.masksToBounds = true;
    [self.view layoutIfNeeded];
    [layer addSublayer:_captureVideoPreviewLayer];

}

相关枚举

/* 填充模式
     Options are AVLayerVideoGravityResize, AVLayerVideoGravityResizeAspect and AVLayerVideoGravityResizeAspectFill. AVLayerVideoGravityResizeAspect is default.
     */

最后,开始录制视频,结束录制视频,重新录制

- (void)startRecord
{
    [_movieOutput startRecordingToOutputFileURL:[self outPutFileURL] recordingDelegate:self];
}

- (NSURL *)outPutFileURL
{
    return [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"outPut.mov"]];
}

- (void)stopRecord
{
    [_movieOutput stopRecording];
}
录制相关delegate

包括开始录制,录制结束等

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
{
    NSLog(@"---- 开始录制 ----");
}

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
{
    NSLog(@"---- 录制结束 ----%@ ",captureOutput.outputFileURL);

    if (self.canSave) {
        [self pushToPlay:captureOutput.outputFileURL];
        self.canSave = NO;
    }
}
压缩/保存视频

咱们需要把录制完毕的视频保存下来.而通常录制完毕的视频是很大的,咱们需要压缩一下再保存.

可以通过AVAssetExportSession来进行压缩,并且可以优化网络shouldOptimizeForNetworkUse,设置转后的格式outputFileType,并且开启异步压缩exportAsynchronouslyWithCompletionHandler

#pragma mark 保存压缩
- (NSURL *)compressedURL
{
    return [NSURL fileURLWithPath:[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) lastObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"compressed.mp4"]]];
}

- (CGFloat)fileSize:(NSURL *)path
{
    return [[NSData dataWithContentsOfURL:path] length]/1024.00 /1024.00;
}

// 压缩视频
- (IBAction)compressVideo:(id)sender
{
    NSLog(@"开始压缩,压缩前大小 %f MB",[self fileSize:self.videoUrl]);

    AVURLAsset *avAsset = [[AVURLAsset alloc] initWithURL:self.videoUrl options:nil];
    NSArray *compatiblePresets = [AVAssetExportSession exportPresetsCompatibleWithAsset:avAsset];
    if ([compatiblePresets containsObject:AVAssetExportPresetLowQuality]) {

        AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPreset640x480];
        exportSession.outputURL = [self compressedURL];
        exportSession.shouldOptimizeForNetworkUse = true;
        exportSession.outputFileType = AVFileTypeMPEG4;
        [exportSession exportAsynchronouslyWithCompletionHandler:^{
            if ([exportSession status] == AVAssetExportSessionStatusCompleted) {
                NSLog(@"压缩完毕,压缩后大小 %f MB",[self fileSize:[self compressedURL]]);
                [self saveVideo:[self compressedURL]];
            }else{
                NSLog(@"当前压缩进度:%f",exportSession.progress);
            }
        }];
    }
}

- (void)saveVideo:(NSURL *)outputFileURL
{
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    [library writeVideoAtPathToSavedPhotosAlbum:outputFileURL
                                completionBlock:^(NSURL *assetURL, NSError *error) {
                                    if (error) {
                                        NSLog(@"保存视频失败:%@",error);
                                    } else {
                                        NSLog(@"保存视频到相册成功");
                                    }
                                }];
}
播放录制完的视频以及重复播放

播放录制的视频,以及重复播放

- (void)create
{
    _playItem = [AVPlayerItem playerItemWithURL:self.videoUrl];
    _player = [AVPlayer playerWithPlayerItem:_playItem];
    _playerLayer =[AVPlayerLayer playerLayerWithPlayer:_player];
    _playerLayer.frame = CGRectMake(200, 200, 100, 100);
    _playerLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//视频填充模式
    [self.view.layer addSublayer:_playerLayer];
    [_player play];
}

-(void)playbackFinished:(NSNotification *)notification
{
    [_player seekToTime:CMTimeMake(0, 1)];
    [_player play];
}

交互相关

接下来,是一些和交互相关的,比如切换摄像头,开关闪光灯,还有白平衡啥的.

注意,改变设备属性前一定要首先调用lockForConfiguration方法加锁,调用完之后使用unlockForConfiguration方法解锁.

意义是—进行设备属性修改期间,先锁定设备,防止多处同时修改设备.因为可能有多处不同的修改,咱们将其封装起来最好

-(void)changeDevicePropertySafety:(void (^)(AVCaptureDevice *captureDevice))propertyChange{

    //也可以直接用_videoDevice,但是下面这种更好
    AVCaptureDevice *captureDevice= [_videoInput device];
    NSError *error;

    BOOL lockAcquired = [captureDevice lockForConfiguration:&error];
    if (!lockAcquired) {
        NSLog(@"锁定设备过程error,错误信息:%@",error.localizedDescription);
    }else{
        [_captureSession beginConfiguration];
        propertyChange(captureDevice);
        [captureDevice unlockForConfiguration];
        [_captureSession commitConfiguration];
    }
}
开/关闪光灯

闪光模式开启后,并无明显感觉,所以还需要开启手电筒,并且开启前先判断是否自持,否则崩溃

- (IBAction)changeFlashlight:(UIButton *)sender {

    BOOL con1 = [_videoDevice hasTorch];    //支持手电筒模式
    BOOL con2 = [_videoDevice hasFlash];    //支持闪光模式

    if (con1 && con2)
    {
        [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {
            if (_videoDevice.flashMode == AVCaptureFlashModeOn)         //闪光灯开
            {
                [_videoDevice setFlashMode:AVCaptureFlashModeOff];
                [_videoDevice setTorchMode:AVCaptureTorchModeOff];
            }else if (_videoDevice.flashMode == AVCaptureFlashModeOff)  //闪光灯关
            {
                [_videoDevice setFlashMode:AVCaptureFlashModeOn];
                [_videoDevice setTorchMode:AVCaptureTorchModeOn];
            }
        }];
        sender.selected=!sender.isSelected;
    }else{
        NSLog(@"不能切换闪光模式");
    }
}
切换摄像头

根据现在正在使用的摄像头来判断需要切换的摄像头

- (IBAction)changeCamera{

    switch (_videoDevice.position) {
        case AVCaptureDevicePositionBack:
            _videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionFront];
            break;
        case AVCaptureDevicePositionFront:
            _videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];
            break;
        default:
            return;
            break;
    }

    [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {
        NSError *error;
        AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_videoDevice error:&error];

        if (newVideoInput != nil) {
            //必选先 remove 才能询问 canAdd
            [_captureSession removeInput:_videoInput];
            if ([_captureSession canAddInput:newVideoInput]) {
                [_captureSession addInput:newVideoInput];
                _videoInput = newVideoInput;
            }else{
                [_captureSession addInput:_videoInput];
            }

        } else if (error) {
            NSLog(@"切换前/后摄像头失败, error = %@", error);
        }
    }];

}
聚焦模式,曝光模式,拉近/远镜头(焦距)

同时存在单击和双击的手势,咱们如下设置,requireGestureRecognizerToFail的作用就是每次只生效一个手势

-(void)addGenstureRecognizer{

    UITapGestureRecognizer *singleTapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(singleTap:)];
    singleTapGesture.numberOfTapsRequired = 1;
    singleTapGesture.delaysTouchesBegan = YES;

    UITapGestureRecognizer *doubleTapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(doubleTap:)];
    doubleTapGesture.numberOfTapsRequired = 2;
    doubleTapGesture.delaysTouchesBegan = YES;

    [singleTapGesture requireGestureRecognizerToFail:doubleTapGesture];
    [self.videoView addGestureRecognizer:singleTapGesture];
    [self.videoView addGestureRecognizer:doubleTapGesture];
}

单击修改聚焦模式setFocusMode及聚焦点setFocusPointOfInterest,还有曝光模式setExposureMode及曝光点setExposurePointOfInterest

注意,摄像头的点范围是0~1,咱们需要把点转化一下,使用captureDevicePointOfInterestForPoint

-(void)singleTap:(UITapGestureRecognizer *)tapGesture{

    CGPoint point= [tapGesture locationInView:self.videoView];

    CGPoint cameraPoint= [_captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point];
    [self setFocusCursorAnimationWithPoint:point];

    [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {

        //聚焦
        if ([captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
            [captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
        }else{
            NSLog(@"聚焦模式修改失败");
        }

        //聚焦点的位置
        if ([captureDevice isFocusPointOfInterestSupported]) {
            [captureDevice setFocusPointOfInterest:cameraPoint];
        }

        //曝光模式
        if ([captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose]) {
            [captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
        }else{
            NSLog(@"曝光模式修改失败");
        }

        //曝光点的位置
        if ([captureDevice isExposurePointOfInterestSupported]) {
            [captureDevice setExposurePointOfInterest:cameraPoint];
        }

    }];
}

下面是双击设置焦距videoZoomFactor

-(void)doubleTap:(UITapGestureRecognizer *)tapGesture{

    NSLog(@"双击");

    [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {
        if (captureDevice.videoZoomFactor == 1.0) {
            CGFloat current = 1.5;
            if (current < captureDevice.activeFormat.videoMaxZoomFactor) {
                [captureDevice rampToVideoZoomFactor:current withRate:10];
            }
        }else{
            [captureDevice rampToVideoZoomFactor:1.0 withRate:10];
        }
    }];
}

相关枚举

/*
         @constant AVCaptureFocusModeLocked 锁定在当前焦距
         Indicates that the focus should be locked at the lens' current position.

         @constant AVCaptureFocusModeAutoFocus 自动对焦一次,然后切换到焦距锁定
         Indicates that the device should autofocus once and then change the focus mode to AVCaptureFocusModeLocked.

         @constant AVCaptureFocusModeContinuousAutoFocus 当需要时.自动调整焦距
         Indicates that the device should automatically focus when needed.
         */

/*
         @constant AVCaptureExposureModeLocked  曝光锁定在当前值
         Indicates that the exposure should be locked at its current value.

         @constant AVCaptureExposureModeAutoExpose 曝光自动调整一次然后锁定
         Indicates that the device should automatically adjust exposure once and then change the exposure mode to AVCaptureExposureModeLocked.

         @constant AVCaptureExposureModeContinuousAutoExposure 曝光自动调整
         Indicates that the device should automatically adjust exposure when needed.

         @constant AVCaptureExposureModeCustom 曝光只根据设定的值来
         Indicates that the device should only adjust exposure according to user provided ISO, exposureDuration values.

         */

Demo

终于说完了,感觉越到后面越无力,还是放上一个 demo 吧,里面包含了上面介绍的两种方式.

Demo 下载地址

原创文章,转载请注明地址: https://kevinmky.github.io

在 iOS 上捕获视频

在 iOS 上捕获视频
转自https://objccn.io/issue-23-1/

随着每一代 iPhone 处理能力和相机硬件配置的提高,使用它来捕获视频也变得更加有意思。它们小巧,轻便,低调,而且与专业摄像机之间的差距已经变得非常小,小到在某些情况下,iPhone 可以真正替代它们。

这篇文章讨论了关于如何配置视频捕获管线 (pipeline) 和最大限度地利用硬件性能的一些不同选择。 这里有个使用了不同管线的样例 app,可以在 GitHub 查看。

UIImagePickerController

目前,将视频捕获集成到你的应用中的最简单的方法是使用 UIImagePickerController。这是一个封装了完整视频捕获管线和相机 UI 的 view controller。

在实例化相机之前,首先要检查设备是否支持相机录制:

1
2
3
4
5
6
7
8
if ([UIImagePickerController
isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
NSArray *availableMediaTypes = [UIImagePickerController
availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
if ([availableMediaTypes containsObject:(NSString *)kUTTypeMovie]) {
// 支持视频录制
}
}

然后创建一个 UIImagePickerController 对象,设置好代理便于进一步处理录制好的视频 (比如存到相册) 以及对于用户关闭相机作出响应:

1
2
3
4
UIImagePickerController *camera = [UIImagePickerController new];
camera.sourceType = UIImagePickerControllerSourceTypeCamera;
camera.mediaTypes = @[(NSString *)kUTTypeMovie];
camera.delegate = self;

这是你实现一个功能完善的摄像机所需要写的所有代码。

###相机配置
UIImagePickerController 提供了额外的配置选项。

通过设置 cameraDevice 属性可以选择一个特定的相机。这是一个 UIImagePickerControllerCameraDevice 枚举,默认情况下是 UIImagePickerControllerCameraDeviceRear,你也可以把它设置为 UIImagePickerControllerCameraDeviceFront。每次都应事先确认你想要设置的相机是可用的:

1
2
3
4
UIImagePickerController *camera = …
if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) {
[camera setCameraDevice:UIImagePickerControllerCameraDeviceFront];
}

videoQuality 属性用于控制录制视频的质量。它允许你设置一个特定的编码预设,从而改变视频的比特率和分辨率。以下是六种预设:

前三种为相对预设 (low, medium, high)。这些预设的编码配置会因设备不同而不同。如果选择 high,那么你选定的相机会提供给你该设备所能支持的最高画质。后面三种是特定分辨率的预设 (640x480 VGA, 960x540 iFrame, 和 1280x720 iFrame)。

###自定义 UI
就像上面提到的,UIImagePickerController 自带一套相机 UI,可以直接使用。然而,你也可以自定义相机的控件,通过隐藏默认控件,然后创建带有控件的自定义视图,并覆盖在相机预览图层上面

1
2
3
UIView *cameraOverlay = …
picker.showsCameraControls = NO;
picker.cameraOverlayView = cameraOverlay;

然后你需要将你覆盖层上的控件关联上 UIImagePickerController 的控制方法 (比如,startVideoCapture 和 stopVideoCapture)。


#AVFoundation

如果你想要更多关于处理捕获视频的方法,而这些方法是 UIImagePickerController 所不能提供的,那么你需要使用 AVFoundation。

AVFoundation 中关于视频捕获的主要的类是 AVCaptureSession。它负责调配影音输入与输出之间的数据流:

使用一个 capture session,你需要先实例化,添加输入与输出,接着启动从输入到输出之间的数据流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AVCaptureSession *captureSession = [AVCaptureSession new];
AVCaptureDeviceInput *cameraDeviceInput = …
AVCaptureDeviceInput *micDeviceInput = …
AVCaptureMovieFileOutput *movieFileOutput = …
if ([captureSession canAddInput:cameraDeviceInput]) {
[captureSession addInput:cameraDeviceInput];
}
if ([captureSession canAddInput:micDeviceInput]) {
[captureSession addInput:micDeviceInput];
}
if ([captureSession canAddOutput:movieFileOutput]) {
[captureSession addOutput:movieFileOutput];
}
[captureSession startRunning];

(为了简单起见,调度队列 (dispatch queue) 的相关代码已经从上面那段代码中省略了。所有对 capture session 的调用都是阻塞的,因此建议将它们分配到后台串行队列中。)

capture session 可以通过一个 sessionPreset 来进一步配置,这可以用来指定输出质量的等级。有 11 种不同的预设模式:

1
2
3
4
5
6
7
8
9
10
11
NSString *const AVCaptureSessionPresetPhoto;
NSString *const AVCaptureSessionPresetHigh;
NSString *const AVCaptureSessionPresetMedium;
NSString *const AVCaptureSessionPresetLow;
NSString *const AVCaptureSessionPreset352x288;
NSString *const AVCaptureSessionPreset640x480;
NSString *const AVCaptureSessionPreset1280x720;
NSString *const AVCaptureSessionPreset1920x1080;
NSString *const AVCaptureSessionPresetiFrame960x540;
NSString *const AVCaptureSessionPresetiFrame1280x720;
NSString *const AVCaptureSessionPresetInputPriority;

第一个代表高像素图片输出。 接下来的九个和之前我们在设置 UIImagePickerController 的 videoQuality 时看到过的 UIImagePickerControllerQualityType 选项非常相似,不同的是,这里有一些额外可用于 capture session 的预设。 最后一个 (AVCaptureSessionPresetInputPriority) 代表 capture session 不去控制音频与视频输出设置。而是通过已连接的捕获设备的 activeFormat 来反过来控制 capture session 的输出质量等级。在下一节,我们将会看到更多关于设备和设备格式的细节。

##输入
AVCaptureSession 的输入其实就是一个或多个的 AVCaptureDevice 对象,这些对象通过 AVCaptureDeviceInput 连接上 capture session。

我们可以使用 [AVCaptureDevice devices] 来寻找可用的捕获设备。以 iPhone 6 为例:

1
2
3
4
5
(
“<AVCaptureFigVideoDevice: 0x136514db0 [Back Camera][com.apple.avfoundation.avcapturedevice.built-in_video:0]>”,
“<AVCaptureFigVideoDevice: 0x13660be80 [Front Camera][com.apple.avfoundation.avcapturedevice.built-in_video:1]>”,
“<AVCaptureFigAudioDevice: 0x174265e80 [iPhone Microphone][com.apple.avfoundation.avcapturedevice.built-in_audio:0]>”
)

##视频输入
配置相机输入,需要实例化一个 AVCaptureDeviceInput 对象,参数是你期望的相机设备,然后把它添加到 capture session:

1
2
3
4
5
6
7
AVCaptureSession *captureSession = …
AVCaptureDevice *cameraDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *cameraDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:cameraDevice error:&error];
if ([captureSession canAddInput:input]) {
[captureSession addInput:cameraDeviceInput];
}

如果上面提到的 capture session 预设列表里能满足你的需求,那你就不需要做更多的事情了。如果不够,比如你想要高的帧率,你将需要配置具体的设备格式。一个视频捕获设备有许多设备格式,每个都带有特定的属性和功能。下面是对于 iPhone6 的后置摄像头的一些例子 (一共有 22 种可用格式):

格式 分辨率 FPS HRSI FOV VIS 最大放大比例 Upscales AF ISO SS HDR
420v 1280x720 5 - 240 1280x720 54.626 YES 49.12 1.09 1 29.0 - 928 0.000003-0.200000 NO
420f 1280x720 5 - 240 1280x720 54.626 YES 49.12 1.09 1 29.0 - 928 0.000003-0.200000 NO
420v 1920x1080 2 - 30 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000013-0.500000 YES
420f 1920x1080 2 - 30 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000013-0.500000 YES
420v 1920x1080 2 - 60 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000008-0.500000 YES
420f 1920x1080 2 - 60 3264x1836 58.040 YES 95.62 1.55 2 29.0 - 464 0.000008-0.500000 YES
格式 = 像素格式
FPS = 支持帧数范围
HRSI = 高像素静态图片尺寸
FOV = 视角
VIS = 该格式支持视频防抖
Upscales = 加入数字 upscaling 时的放大比例
AF = 自动对焦系统(1 是反差对焦,2 是相位对焦)
ISO = 支持感光度范围
SS = 支持曝光时间范围
HDR = 支持高动态范围图像
通过上面的那些格式,你会发现如果要录制 240 帧每秒的视频的话,可以根据想要的像素格式选用第一个或第二个格式。另外若是要捕获 1920x1080 的分辨率的视频的话,是不支持 240 帧每秒的。

配置一个具体设备格式,你首先需要调用 lockForConfiguration: 来获取设备的配置属性的独占访问权限。接着你简单地使用 setActiveFormat: 来设置设备的捕获格式。这将会自动把 capture session 的预设设置为 AVCaptureSessionPresetInputPriority。

一旦你设置了预想的设备格式,你就可以在这种设备格式的约束参数范围内进行进一步的配置了。

对于视频捕获的对焦,曝光和白平衡的设置,与图像捕获时一样,具体可参考第 21 期“iOS 上的相机捕捉”。除了那些,这里还有一些视频特有的配置选项。

你可以用捕获设备的 activeVideoMinFrameDuration 和 activeVideoMaxFrameDuration 属性设置帧速率,一帧的时长是帧速率的倒数。设置帧速率之前,要先确认它是否在设备格式所支持的范围内,然后锁住捕获设备来进行配置。为了确保帧速率恒定,可以将最小与最大的帧时长设置成一样的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSError *error;
CMTime frameDuration = CMTimeMake(1, 60);
NSArray *supportedFrameRateRanges = [device.activeFormat videoSupportedFrameRateRanges];
BOOL frameRateSupported = NO;
for (AVFrameRateRange *range in supportedFrameRateRanges) {
if (CMTIME_COMPARE_INLINE(frameDuration, >=, range.minFrameDuration) &&
CMTIME_COMPARE_INLINE(frameDuration, <=, range.maxFrameDuration)) {
frameRateSupported = YES;
}
}
if (frameRateSupported && [device lockForConfiguration:&error]) {
[device setActiveVideoMaxFrameDuration:frameDuration];
[device setActiveVideoMinFrameDuration:frameDuration];
[device unlockForConfiguration];
}

视频防抖 是在 iOS 6 和 iPhone 4S 发布时引入的功能。到了 iPhone 6,增加了更强劲和流畅的防抖模式,被称为影院级的视频防抖动。相关的 API 也有所改动 (目前为止并没有在文档中反映出来,不过可以查看头文件)。防抖并不是在捕获设备上配置的,而是在 AVCaptureConnection 上设置。由于不是所有的设备格式都支持全部的防抖模式,所以在实际应用中应事先确认具体的防抖模式是否支持:

1
2
3
4
5
6
7
AVCaptureDevice *device = ...;
AVCaptureConnection *connection = ...;
AVCaptureVideoStabilizationMode stabilizationMode = AVCaptureVideoStabilizationModeCinematic;
if ([device.activeFormat isVideoStabilizationModeSupported:stabilizationMode]) {
[connection setPreferredVideoStabilizationMode:stabilizationMode];
}

iPhone 6 的另一个新特性就是视频 HDR (高动态范围图像),它是“高动态范围的视频流,与传统的将不同曝光度的静态图像合成成一张高动态范围图像的方法完全不同”,它是内建在传感器中的。有两种方法可以配置视频 HDR:直接将 capture device 的 videoHDREnabled 设置为启用或禁用,或者使用 automaticallyAdjustsVideoHDREnabled 属性来留给系统处理。

技术参考:iPhone 6 和 iPhone Plus 的新 AV Foundation 相机特性

###音频输入

之前展示的捕获设备列表里面只有一个音频设备,你可能觉得奇怪,毕竟 iPhone 6 有 3 个麦克风。然而因为有时会放在一起使用,便于优化性能,因此可能被当做一个设备来使用。例如在 iPhone 5 及以上的手机录制视频时,会同时使用前置和后置麦克风,用于定向降噪。

Technical Q&A: AVAudioSession - Microphone Selection
大多数情况下,设置成默认的麦克风配置即可。后置麦克风会自动搭配后置摄像头使用 (前置麦克风则用于降噪),前置麦克风和前置摄像头也是一样。

然而想要访问和配置单独的麦克风也是可行的。例如,当用户正在使用后置摄像头捕获场景的时候,使用前置麦克风来录制解说也应是可能的。这就要依赖于 AVAudioSession。 为了变更要访问的音频,audio session 首先需要设置为支持这样做的类别。然后我们需要遍历 audio session 的输入端口和端口数据来源,来找到我们想要的麦克风:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 配置 audio session
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
[audioSession setActive:YES error:nil];
// 寻找期望的输入端口
NSArray* inputs = [audioSession availableInputs];
AVAudioSessionPortDescription *builtInMic = nil;
for (AVAudioSessionPortDescription* port in inputs) {
if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
builtInMic = port;
break;
}
}
// 寻找期望的麦克风
for (AVAudioSessionDataSourceDescription* source in builtInMic.dataSources) {
if ([source.orientation isEqual:AVAudioSessionOrientationFront]) {
[builtInMic setPreferredDataSource:source error:nil];
[audioSession setPreferredInput:builtInMic error:&error];
break;
}
}

除了设置非默认的麦克风配置,你也可以使用 AVAudioSession 来配置其他音频设置,比如音频增益和采样率等。

###访问权限

有件事你需要记住,访问相机和麦克风需要先获得用户授权。当你给视频或音频创建第一个 AVCaptureDeviceInput 对象时,iOS 会自动弹出一次对话框,请求用户授权,但你最好还是自己实现下。之后你就可以在还没有被授权的时候,使用相同的代码来提示用户进行授权。当用户未授权时,对于录制视频或音频的尝试,得到的将是黑色画面和无声。

###输出
输入配置完了,现在把我们的注意力转向 capture session 的输出。

AVCaptureMovieFileOutput

将视频写入文件,最简单的选择就是使用 AVCaptureMovieFileOutput 对象。把它作为输出添加到 capture session 中,就可以将视频和音频写入 QuickTime 文件,这只需很少的配置。

1
2
3
4
5
6
7
8
AVCaptureMovieFileOutput *movieFileOutput = [AVCaptureMovieFileOutput new];
if([captureSession canAddOutput:movieFileOutput]){
[captureSession addOutput:movieFileOutput];
}
// 开始录制
NSURL *outputURL = …
[movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];

当实际的录制开始或停止时,想要接收回调的话就必须要一个录制代理。当录制停止时,输出通常还在写入数据,等它完成之后会调用代理方法。

AVCaptureMovieFileOutput 有一些其他的配置选项,比如在某段时间后,在达到某个指定的文件尺寸时,或者当设备的最小磁盘剩余空间达到某个阈值时停止录制。如果你还需要更多设置,比如自定义视频音频的压缩率,或者你想要在写入文件之前,处理视频音频的样本,那么你需要一些更复杂的操作。

AVCaptureDataOutput 和 AVAssetWriter

如果你想要对影音输出有更多的操作,你可以使用 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 而不是我们上节讨论的 AVCaptureMovieFileOutput。

这些输出将会各自捕获视频和音频的样本缓存,接着发送到它们的代理。代理要么对采样缓冲进行处理 (比如给视频加滤镜),要么保持原样传送。使用 AVAssetWriter 对象可以将样本缓存写入文件:

配置一个 asset writer 需要定义一个输出 URL 和文件格式,并添加一个或多个输入来接收采样的缓冲。我们还需要将输入的 expectsMediaInRealTime 属性设置为 YES,因为它们需要从 capture session 实时获得数据。

1
2
3
4
5
6
7
8
9
10
11
12
NSURL *url = …;
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:nil];
AVAssetWriterInput *videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:nil];
videoInput.expectsMediaDataInRealTime = YES;
AVAssetWriterInput *audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:nil];
audioInput.expectsMediaDataInRealTime = YES;
if ([assetWriter canAddInput:videoInput]) {
[assetWriter addInput:videoInput];
}
if ([assetWriter canAddInput:audioInput]) {
[assetWriter addInput:audioInput];
}

(这里推荐将 asset writer 派送到后台串行队列中调用。)

在上面的示例代码中,我们将 asset writer 的 outputSettings 设置为 nil。这就意味着附加上来的样本不会再被重新编码。如果你确实想要重新编码这些样本,那么需要提供一个包含具体输出参数的字典。关于音频输出设置的键值被定义在这里, 关于视频输出设置的键值定义在这里。

为了更简单点,AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 分别带有 recommendedVideoSettingsForAssetWriterWithOutputFileType: 和 recommendedAudioSettingsForAssetWriterWithOutputFileType: 方法,可以生成与 asset writer 兼容的带有全部键值对的字典。所以你可以通过在这个字典里调整你想要重写的属性,来简单地定义你自己的输出设置。比如,增加视频比特率来提高视频质量等。

或者,你也可以使用 AVOutputSettingsAssistant 来配置输出设置的字典,但是从我的经验来看,使用上面的方法会更好,它们会提供更实用的输出设置,比如视频比特率。另外,AVOutputSettingsAssistant 似乎存在一些缺点,例如,当你改变希望的视频的帧速率时,视频的比特率并不会改变。

###实时预览

当使用 AVFoundation 来做图像捕获时,我们必须提供一套自定义的用户界面。其中一个关键的相机交互组件是实时预览图。最简单的实现方式是通过把 AVCaptureVideoPreviewLayer 对象作为一个 sublayer 加到相机图层上去:

1
2
3
4
5
AVCaptureSession *captureSession = ...;
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:captureSession];
UIView *cameraView = ...;
previewLayer.frame = cameraView.bounds;
[cameraView.layer addSublayer:previewLayer];

如果你想要更进一步操作,比如,在实时预览图加滤镜,你需要将 AVCaptureVideoDataOutput 对象加到 capture session,并且使用 OpenGL 展示画面,具体可查看该文“iOS 上的相机捕捉”

###总结

有许多不同的方法可以给 iOS 上的视频捕获配置管线,从最直接的 UIImagePickerController,到精密配合的 AVCaptureSession 与 AVAssetWriter 。如何抉择取决于你的项目要求,比如期望的视频质量和压缩率,或者是你想要展示给用户的相机控件。

GIF解析和生成

//gif的解析, 和生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
网络转载,忘记出处了
//制作gif是基于ImageIO.framework,所以要先添加这个库gif分解图片
- (void)viewDidLoad
{
[super viewDidLoad];
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"test101" ofType:@"gif"]];
//通过data获取image的数据源
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
//获取帧数
size_t count = CGImageSourceGetCount(source);
NSMutableArray* tmpArray = [NSMutableArray array];
for (size_t i = 0; i < count; i++)
{
//获取图像
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
//生成image
UIImage *image = [UIImage imageWithCGImage:imageRef scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp];
[tmpArray addObject:image];
CGImageRelease(imageRef);
}
CFRelease(source);
int i = 0;
for (UIImage *img in tmpArray) {
//写文件
NSData *imageData = UIImagePNGRepresentation(img);
NSString *pathNum = [[self backPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%d.png",i]];
[imageData writeToFile:pathNum atomically:NO];
i++;
}
}
//返回保存图片的路径
-(NSString *)backPath{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = [UniversalMethod backDocumentDirectoryPath];
NSString *imageDirectory = [path stringByAppendingPathComponent:@"Normal"];
[fileManager createDirectoryAtPath:imageDirectory withIntermediateDirectories:YES attributes:nil error:nil];
return imageDirectory;
}
gif的制作
制作gif需要依赖MobileCoreServices.framework
//gif的制作
//获取源数据image
NSMutableArray *imgs = [[NSMutableArray alloc]initWithObjects:[UIImage imageNamed:@"bear_1"],[UIImage imageNamed:@"bear_2"], nil];
//图像目标
CGImageDestinationRef destination;
//创建输出路径
NSArray *document = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentStr = [document objectAtIndex:0];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *textDirectory = [documentStr stringByAppendingPathComponent:@"gif"];
[fileManager createDirectoryAtPath:textDirectory withIntermediateDirectories:YES attributes:nil error:nil];
NSString *path = [textDirectory stringByAppendingPathComponent:@"test101.gif"];
NSLog(@"%@",path);
//创建CFURL对象
/*
CFURLCreateWithFileSystemPath(CFAllocatorRef allocator, CFStringRef filePath, CFURLPathStyle pathStyle, Boolean isDirectory)
allocator : 分配器,通常使用kCFAllocatorDefault
filePath : 路径
pathStyle : 路径风格,我们就填写kCFURLPOSIXPathStyle 更多请打问号自己进去帮助看
isDirectory : 一个布尔值,用于指定是否filePath被当作一个目录路径解决时相对路径组件
*/
CFURLRef url = CFURLCreateWithFileSystemPath (
kCFAllocatorDefault,
(CFStringRef)path,
kCFURLPOSIXPathStyle,
false);
//通过一个url返回图像目标
destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, imgs.count, NULL);
//设置gif的信息,播放间隔时间,基本数据,和delay时间
NSDictionary *frameProperties = [NSDictionary
dictionaryWithObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:0.3], (NSString *)kCGImagePropertyGIFDelayTime, nil]
forKey:(NSString *)kCGImagePropertyGIFDictionary];
//设置gif信息
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2];
[dict setObject:[NSNumber numberWithBool:YES] forKey:(NSString*)kCGImagePropertyGIFHasGlobalColorMap];
[dict setObject:(NSString *)kCGImagePropertyColorModelRGB forKey:(NSString *)kCGImagePropertyColorModel];
[dict setObject:[NSNumber numberWithInt:8] forKey:(NSString*)kCGImagePropertyDepth];
[dict setObject:[NSNumber numberWithInt:0] forKey:(NSString *)kCGImagePropertyGIFLoopCount];
NSDictionary *gifProperties = [NSDictionary dictionaryWithObject:dict
forKey:(NSString *)kCGImagePropertyGIFDictionary];
//合成gif
for (UIImage* dImg in imgs)
{
CGImageDestinationAddImage(destination, dImg.CGImage, (__bridge CFDictionaryRef)frameProperties);
}
CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)gifProperties);
CGImageDestinationFinalize(destination);
CFRelease(destination);

gif 解析方法

From: http://blog.csdn.net/qxuewei/article/details/50782855

原生方法:

1.UIWebView
特点:加载速度略长,性能更优,播放的gif动态图更加流畅。

-(void)showGifImageWithWebView{

    NSData *gifData = [NSData dataWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"earthGif" ofType:@"gif"]];

    UIWebView *imageWebView = [[UIWebView alloc] initWithFrame:CGRectMake(112, 302, 132, 102)];

    imageWebView.userInteractionEnabled = NO;

    [imageWebView loadData:gifData MIMEType:@"image/gif" textEncodingName:nil baseURL:nil];

    [self.view addSubview:imageWebView];
}

2.UIImagView
加载的方式更加快速,性能不如UIWebView,优点:易于扩展

1)
增加一个UIImageView的类别(category),增加两个方法
UIImage+Tool
.h

#import <UIKit/UIKit.h>

@interface UIImageView (Tool)

/** 解析gif文件数据的方法 block中会将解析的数据传递出来 */
-(void)getGifImageWithUrk:(NSURL *)url returnData:(void(^)(NSArray<UIImage *> * imageArray,NSArray<NSNumber *>*timeArray,CGFloat totalTime, NSArray<NSNumber *>* widths, NSArray<NSNumber *>* heights))dataBlock;

/** 为UIImageView添加一个设置gif图内容的方法: */
-(void)yh_setImage:(NSURL *)imageUrl;

@end

.m

#import "UIImageView+Tool.h"

#import <ImageIO/ImageIO.h>

@implementation UIImageView (Tool)




-(void)getGifImageWithUrk:(NSURL *)url returnData:(void(^)(NSArray<UIImage *> * imageArray, NSArray<NSNumber *>*timeArray,CGFloat totalTime, NSArray<NSNumber *>* widths,NSArray<NSNumber *>* heights))dataBlock{

    CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL);

    size_t count = CGImageSourceGetCount(source);

    float allTime=0;

    NSMutableArray * imageArray = [[NSMutableArray alloc]init];

    NSMutableArray * timeArray = [[NSMutableArray alloc]init];

    NSMutableArray * widthArray = [[NSMutableArray alloc]init];

    NSMutableArray * heightArray = [[NSMutableArray alloc]init];

    for (size_t i=0; i<count; i++) {
        CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
        [imageArray addObject:(__bridge UIImage *)(image)];
        CGImageRelease(image);

        NSDictionary * info = (__bridge NSDictionary*)CGImageSourceCopyPropertiesAtIndex(source, i, NULL);
        CGFloat width = [[info objectForKey:(__bridge NSString *)kCGImagePropertyPixelWidth] floatValue];
        CGFloat height = [[info objectForKey:(__bridge NSString *)kCGImagePropertyPixelHeight] floatValue];
        [widthArray addObject:[NSNumber numberWithFloat:width]];
        [heightArray addObject:[NSNumber numberWithFloat:height]];
        NSDictionary * timeDic = [info objectForKey:(__bridge NSString *)kCGImagePropertyGIFDictionary];
        CGFloat time = [[timeDic objectForKey:(__bridge NSString *)kCGImagePropertyGIFDelayTime]floatValue];
        allTime+=time;
        [timeArray addObject:[NSNumber numberWithFloat:time]];
    }
    CFRelease(source);
    dataBlock(imageArray,timeArray,allTime,widthArray,heightArray);
}


-(void)yh_setImage:(NSURL *)imageUrl{
    __weak id __self = self;
    [self getGifImageWithUrk:imageUrl returnData:^(NSArray<UIImage *> *imageArray, NSArray<NSNumber *> *timeArray, CGFloat totalTime, NSArray<NSNumber *> *widths, NSArray<NSNumber *> *heights) {

        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
        NSMutableArray * times = [[NSMutableArray alloc]init];
        float currentTime = 0;

        for (int i=0; i<imageArray.count; i++) {
            [times addObject:[NSNumber numberWithFloat:currentTime/totalTime]];
            currentTime+=[timeArray[i] floatValue];
        }
        [animation setKeyTimes:times];
        [animation setValues:imageArray];
        [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];

        animation.repeatCount= MAXFLOAT;

        animation.duration = totalTime;

        [[(UIImageView *)__self layer]addAnimation:animation forKey:@"gifAnimation"];
    }];
}

@end

在加载gif的地方使用
导入 UIImageView+Tool

-(void)showGifImageWithImageView{

    UIImageView * imageView = [[UIImageView alloc]initWithFrame:CGRectMake(112, 342, 132, 102)];
    NSURL * url = [[NSURL alloc]initFileURLWithPath:[[NSBundle mainBundle] pathForResource:@"earthGif.gif" ofType:nil]];
    [imageView yh_setImage:url];
    [self.view addSubview:imageView];

}

第三方:
1.YLGIFImage
github链接: [https://github.com/liyong03/YLGIFImage][1]

[1]: https://github.com/liyong03/YLGIFImage

#import “YLGIFImage.h”

#import "YLImageView.h"

-(void)showGifImageWithYLImageView{
    YLImageView* imageView = [[YLImageView alloc] initWithFrame:CGRectMake(112, 342, 132, 102)];
    CGFloat centerX = self.view.center.x;
    [imageView setCenter:CGPointMake(centerX, 402)];
    [self.view addSubview:imageView];
    imageView.image = [YLGIFImage imageNamed:@"earthGif.gif"];
}

2.FLAnimatedImage
github链接:[https://github.com/Flipboard/FLAnimatedImage][2]

[2]: https://github.com/Flipboard/FLAnimatedImage

-(void)showGifImageWithFLAnimatedImage{

    NSString *pathForFile = [[NSBundle mainBundle] pathForResource: @"earthGif" ofType:@"gif"];

    NSData *dataOfGif = [NSData dataWithContentsOfFile: pathForFile];

    FLAnimatedImage *image = [FLAnimatedImage animatedImageWithGIFData:dataOfGif];

    FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];

    imageView.animatedImage = image;
    imageView.frame = CGRectMake(112, 342, 132, 102);
    [self.view addSubview:imageView];
}

IOS知识点-小技巧-小笔记(2)

xcode中,全局去掉项目warning的开头在Builder Setttings->Inhibit All Warnings(抵制所有的警告).当把它设置为Yes时,编译项目就不会出现warning警告了.
因为部分引入的第三方的项目 去掉警告


清理icon 角标 对于ios11 没有去测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
刷新本地和服务器的图标 (不在appIcon上显示推送数量,但是在系统通知栏保留推送通知的方法)
@param noti <#noti description#>
*/
- (void)refreshPushBageValue:(NSNotification *)noti{
NSNumber *value = [noti object];
//
if(kiOS11Later){
/*
iOS 11后,直接设置badgeNumber = -1就生效了
*/
[UIApplication sharedApplication].applicationIconBadgeNumber = value.integerValue;
[JPUSHService setBadge:value.integerValue];
}else{
// 原理是 发送了一个本地推送 无消息的, 如果本地推送有处理 需要处理下
UILocalNotification *clearEpisodeNotification = [[UILocalNotification alloc] init];
clearEpisodeNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:(0.3)];
clearEpisodeNotification.timeZone = [NSTimeZone defaultTimeZone];
/// 根据这个消息 不处理本地
clearEpisodeNotification.alertTitle = @"清理icon";
clearEpisodeNotification.applicationIconBadgeNumber = value.integerValue;
[[UIApplication sharedApplication] scheduleLocalNotification:clearEpisodeNotification];
}
}

终端代理(临时方案)

1
2
3
在终端中执行以下代码, 1 为http 代理, 2 为全部代理 . ;电脑开启代理软件
export http_proxy=http://127.0.0.1:1087 当前临时方案, 关闭后就不走代理了
export all_proxy=socks5://127.0.0.1:1086 这个是临时 都走代理 不光http ,会很快

通过宏定义判断是否引入的是framework,反之则使用双引号,实用!

1
2
3
4
5
6
7
8
9
10
#if __has_include(<xxx/xxx.h>)
#import <xxx/xxx.h>
#else
#import "xxx.h"
#endif
#if __has_include(<GPUImage/GPUImageFramework.h>)
#import <GPUImage/GPUImageFramework.h>
#else
#import "GPUImage.h"

swift 打印内存地址

Unmanaged.passRetained(self as AnyObject)//这个引用计数会+1
Unmanaged.passUnretained(self as AnyObject) // 这个引用计数+0

如下输出 Unmanaged(_value: )


部分服务端更改, 验证了cookie app 中没有cookie 清除 造成服务端返回400 和413, 然后加入cookie 清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
URLCache.shared.removeAllCachedResponses()
let cookies = HTTPCookieStorage.shared.cookies
if cookies != nil && cookies!.count>0{
for cookie in cookies!{
HTTPCookieStorage.shared.deleteCookie(cookie)
}
}
//iOS9.0以上使用的方法
if #available(iOS 9.0, *) {
let dataStore = WKWebsiteDataStore.default()
dataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), completionHandler: { (records) in
for record in records{
//清除本站的cookie
if record.displayName.contains("sina.com"){//这个判断注释掉的话是清理所有的cookie
WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {
//清除成功
print("清除成功\(record)")
})
}
}
})
} else {
//ios8.0以上使用的方法
let libraryPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.libraryDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first
let cookiesPath = libraryPath! + "/Cookies"
try!FileManager.default.removeItem(atPath: cookiesPath)
}

1 宏
1.1 has_include
用于判断是否包含某些头文件 例如:
`#if
has_include()`

1.2 NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END
两者搭配使用,在这两个宏之间的所有函数变量都是不可空。如果在这两个宏之间,想要可空的函数变量,需要单独设置 nullable关键字。

1.3 UNAVAILABLE_ATTRIBUTE
不可用。在方法或者属性 加上这个宏之后。将变成不可用。

1.4 UI_APPEARANCE_SELECTOR
加到属性后面,所有该属性的实例都统一设置。


IPv6 问题:
1 使用域名
2 手动转换
假设访问http://67.218.154.33
转换为ipv6 形式 : http://[::ffff:67.218.154.33]

注 找了一个解释:https://www.jianshu.com/p/1312e98cd35b

—- 获取类名—
oc

1
2
3
4
/// switf 代码会带程序名
NSString *selfClassName = NSStringFromClass([self class]);
/// 去除程序名
selfClassName = [selfClassName componentsSeparatedByString:@"."].lastObject;

swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 返回内部类名
print("class: \(object_getClassName(self))")
// 返回应用程序名+类名
print("class: \(NSStringFromClass(self.dynamicType))")
// 返回应用程序名+类名,并去掉应用程序名
print("class: \(NSStringFromClass(self.dynamicType).componentsSeparatedByString(".").last!)")
// 返回应用程序名+类名+内存地址
print("class: \(self)")
// 返回应用程序名+类名+内存地址
print("class: \(self.description)")
// 返回类名
print("class: \(self.dynamicType)")

Swizzle touchesBegan:withEvent:事故

得实现override touches 方法原因见
地址


ios 设备目录获取

1
2
3
4
5
6
7
8
9
10
// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
// 获取Documents目录路径
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library的目录路径
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// 获取Caches目录路径
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取tmp目录路径
NSString *tmpDir = NSTemporaryDirectory();

程序目录

1
2
3
NSLog(@"%@",[[NSBundle mainBundle] bundlePath]);
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"png"];
UIImage *appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];

libstdc++适配Xcode10与iOS12

原因是苹果在XCode10和iOS12中移除了libstdc++这个库,由libc++这个库取而代之,苹果的解释是libstdc++已经标记为废弃有5年了,建议大家使用经过了llvm优化过并且全面支持C++11的libc++库。

beta 版本的xcode 10 拷贝下xcode9 的文件(注 模拟器 还是崩溃)

1
2
3
cp /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/libstdc++.* /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/
cp /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/libstdc++.* /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/

根本解决办法:

如果你自己的业务模块使用了libstdc++,那么就把模块代码重新调整为依赖libc++,然后重新检查是否存在问题,重新编译
如果你引用的三方库使用了libstdc++,那么向三方库寻求支持,进行升级


查看应用初始化时间
在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS。(这个是更详细的)
如图设置:控制台会输出时间

一个App在执行main函数前包括app delegate的系列方法如applicationWillFinishLaunching时,会做许多系统级别的准备.而在iOS10之前,开发者很难清楚自己App为何启动加载慢.而通过在工程的scheme中添加环境变量DYLD_PRINT_STATISTICS,设置Value为1,App启动加载时就会有启动过程的日志输出. 现在(iOS 10之后)Apple对DYLD_PRINT_STATISTICS的日志输出结果进行了简化,使得更容易让开发者理解.


iOS开发之使用P3图片导致崩溃的解决方法

最近app刚上架,突然收到大面积投诉….一看bugly,9.0-9.3的机器无一幸免,由于项目里有些图标是我直接从阿里图库下载的,问了UI P3,16进制的图片是什么他也说不清,索性让他重新做图了,这个问题只要图片是UI做图基本就可避免

1.打包成ipa

2.把ipa的后缀改成zip,解压缩(这时候会看到一个Payload文件夹)

3.打开终端 输入 cd

4.把 Payload 拖动到终端里(这里的拖动只是为了获取这个文件在电脑上的地址), 回车

5.在终端输入 find . -name ‘Assets.car’ 回车(会输出找到的位置)

6.在终端输入 sudo xcrun –sdk iphoneos assetutil –info ./Assets.car > /tmp/Assets.json 回车
(car 地址可以根据上面找到的位置填入)

7.在终端输入 open /tmp/Assets.json 回车

8.这时候会打开一个text 搜索 DisplayGamut 看看后面是不是P3 如果搜索到的是p3 图片格式还是不对,如果是空或者搜索到显示的不是P3,那图片就对了,根据Name去查找项目里的这张图片吧,然后将其替换.

转自点击


UIView 设置单边圆角

1
2
3
4
5
6
7
8
9
10
11
/// 设置单边圆角
private func setMaskLayer(){
let corner = self.height/2
let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [UIRectCorner.bottomLeft,UIRectCorner.topLeft], cornerRadii: CGSize.init(width: corner, height: corner));
let maskLayer = CAShapeLayer.init()
maskLayer.frame = self.bounds
maskLayer.path = maskPath.cgPath
self.layer.mask = maskLayer;
}

jsonp 转json 正则

str.match(“.?({.}).*”)返回数组第一个


记录一个逗号分隔用法

1
2
3
4
5
6
7
// 多值的设置,使用逗号分隔
// 注意:if let语句中不能使用&& || 条件
// if let中只要有任何一个条件为nil,就跳出循环
if let name = oName, age = oAge {
print("Hi~" + name + "年龄:" + String(age))
}

IOS脚本打包 IPA(.APP转.IPA)

将要转化的.app文件放到 convertToIpa.sh 同目录之中

运行 convertToIpa.sh 脚本

打开 Terminal,cd 到 convertToIpa.sh 的目录,执行

./convertToIpa.sh appName(.app 的名字)

如果提示 permission denied,则用 chmod 777 distribute.sh 命令赋予权限后,再执行一次。

等脚本之行结束后,会在当前文件夹下生成 appName 文件夹,里面的 appName.ipa 就是我们最终想要的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
mkdir $1
mkdir $1/Payload
cp -r $1.app $1/Payload/$1.app
cp Icon.png $1/iTunesArtwork
cd $1
zip -r $1.ipa Payload iTunesArtwork
exit 0

xcode 编译线程数

1.获取当前内核数:
$ sysctl -n hw.ncpu
2.设置编译线程数:
$ defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 8
3.获取编译线程数:
$ defaults read com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks
4.显示编译时长:

$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

iOS12.1 使用 UINavigationController + UITabBarController( UITabBar 磨砂),设置hidesBottomBarWhenPushed后,在 pop 后,会引起TabBar布局异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// .h
@interface CYLTabBar : UITabBar
@end
// .m
#import "CYLTabBar.h"
/**
* 用 block 重写某个 class 的指定方法
* @param targetClass 要重写的 class
* @param targetSelector 要重写的 class 里的实例方法,注意如果该方法不存在于 targetClass 里,则什么都不做
* @param implementationBlock 该 block 必须返回一个 block,返回的 block 将被当成 targetSelector 的新实现,所以要在内部自己处理对 super 的调用,以及对当前调用方法的 self 的 class 的保护判断(因为如果 targetClass 的 targetSelector 是继承自父类的,targetClass 内部并没有重写这个方法,则我们这个函数最终重写的其实是父类的 targetSelector,所以会产生预期之外的 class 的影响,例如 targetClass 传进来 UIButton.class,则最终可能会影响到 UIView.class),implementationBlock 的参数里第一个为你要修改的 class,也即等同于 targetClass,第二个参数为你要修改的 selector,也即等同于 targetSelector,第三个参数是 targetSelector 原本的实现,由于 IMP 可以直接当成 C 函数调用,所以可利用它来实现“调用 super”的效果,但由于 targetSelector 的参数个数、参数类型、返回值类型,都会影响 IMP 的调用写法,所以这个调用只能由业务自己写。
*/
CG_INLINE BOOL
OverrideImplementation(Class targetClass, SEL targetSelector, id (^implementationBlock)(Class originClass, SEL originCMD, IMP originIMP)) {
Method originMethod = class_getInstanceMethod(targetClass, targetSelector);
if (!originMethod) {
return NO;
}
IMP originIMP = method_getImplementation(originMethod);
method_setImplementation(originMethod, imp_implementationWithBlock(implementationBlock(targetClass, targetSelector, originIMP)));
return YES;
}
@implementation CYLTabBar
+ (void)load {
/* 这个问题是 iOS 12.1 Beta 2 的问题,只要 UITabBar 是磨砂的,并且 push viewController 时 hidesBottomBarWhenPushed = YES 则手势返回的时候就会触发。
出现这个现象的直接原因是 tabBar 内的按钮 UITabBarButton 被设置了错误的 frame,frame.size 变为 (0, 0) 导致的。如果12.1正式版Apple修复了这个bug可以移除调这段代码(来源于QMUIKit的处理方式)*/
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(iOS 12.1, *)) {
OverrideImplementation(NSClassFromString(@"UITabBarButton"), @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP originIMP) {
return ^(UIView *selfObject, CGRect firstArgv) {
if ([selfObject isKindOfClass:originClass]) {
// 如果发现即将要设置一个 size 为空的 frame,则屏蔽掉本次设置
if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) {
return;
}
}
// call super
void (*originSelectorIMP)(id, SEL, CGRect);
originSelectorIMP = (void (*)(id, SEL, CGRect))originIMP;
originSelectorIMP(selfObject, originCMD, firstArgv);
};
});
}
});
}
@end

来源 https://github.com/ChenYilong/iOS12AdaptationTips/issues/3


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pod 'AFNetworking' //不显式指定依赖库版本,表示每次都获取最新版本
pod 'AFNetworking', '~>0' //高于0的版本,写这个限制和什么都不写是一个效果,都表示使用最新版本
pod 'AFNetworking', '~> 0.1.2' //使用大于等于0.1.2但小于0.2的版本
pod 'AFNetworking', '~>0.1' //使用大于等于0.1但小于1.0的版本
pod 'AFNetworking', '2.0' //只使用2.0版本
pod 'AFNetworking', '= 2.0' //只使用2.0版本
pod 'AFNetworking', '> 2.0' //使用高于2.0的版本
pod 'AFNetworking', '>= 2.0' //使用大于或等于2.0的版本
pod 'AFNetworking', '< 2.0' //使用小于2.0的版本
pod 'AFNetworking', '<= 2.0' //使用小于或等于2.0的版本
pod 'AFNetworking', :git => 'http://gitlab.xxxx.com/AFNetworking.git', :branch => 'R20161010' //指定分支
pod 'AFNetworking', :path => '../AFNetworking' //指定本地库


1
2
3
4
// //排序
// NSSortDescriptor *isDefault = [NSSortDescriptor sortDescriptorWithKey:@"isDefault" ascending:NO];
// NSSortDescriptor *isUse = [NSSortDescriptor sortDescriptorWithKey:@"isOnlyUse" ascending:NO];
// [self.dataArray sortUsingDescriptors:@[isDefault,isUse]];

系统装 carthage 造成的终端编译ipa找不到编辑器

1
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/

——node link— 失败问题

Could not symlink share/doc/node/gdbinit
Target /usr/local/share/doc/node/gdbinit
already exists. You may want to remove it:
rm ‘/usr/local/share/doc/node/gdbinit’

解决
1 sudo chown -R $USER /usr/local
2 brew link –overwrite node


xcode 打包多taget 版本号自动同步
添加 Shell 脚本; 在Xcode Build Phases -> 添加 Run Script;
注: 会影响打包, 直接自动打包shell 脚步直接处理, 方法同样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Type a script or drag a script file from your workspace to insert its path.
if [ $CONFIGURATION == Release ]; then
echo "Bumping build number..."
plist=${INFOPLIST_FILE}
buildnum=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${plist}")
if [[ "${buildnum}" == "" ]]; then
echo "No build number in $plist"
exit 2
fi
echo "Bumped build number to $buildnum"
buildnum=$(expr $buildnum + 1)
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildnum" "${INFOPLIST_FILE}"
echo "Update build number to Current Project Version"
agvtool new-version -all $buildnum
echo "Keep Extension Target build version and number as same as app"
buildver=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${plist}")
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $buildver" "$SRCROOT/$ZBiOSNotificationService/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $buildver" "$SRCROOT/$ZBiOSNotificationService/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildnum" "$SRCROOT/$ZBiOSNotificationService/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildnum" "$SRCROOT/$ZBiOSNotificationService/Info.plist"
else
echo $CONFIGURATION "build - Not bumping build number."
fi

时间的一个处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取代表公历的NSCalendar对象
let gregorian = Calendar(
identifier: .gregorian)
// 获取当前日期
let dt = Date()
// 定义一个时间字段的旗标,指定将会获取指定年、月、日、时、分、秒的信息
// let unitFlags: NSCalendar.Unit = [.year, .month, .day, .hour, .minute, .second, .weekday]
// 获取不同时间字段的信息
let comp = gregorian.dateComponents(
[.year, .month, .day, .hour, .minute, .second, .weekday],
from: dt)
// 获取各时间字段的数值
// 再次创建一个NSDateComponents对象
var comp2 = DateComponents()
// 设置各时间字段的数值
comp2.year = comp.year
comp2.month = comp.month
comp2.day = comp.day
comp2.hour = comp.hour
comp2.minute = 34
// 通过NSDateComponents所包含的时间字段的数值来恢复NSDate对象
let date = gregorian.date(from: comp2)
if let date = date {
print("获取的日期为:\(date)")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 获取代表公历的NSCalendar对象
NSCalendar *gregorian = [[NSCalendar alloc]
initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
// 获取当前日期
NSDate* dt = [NSDate date];
// 定义一个时间字段的旗标,指定将会获取指定年、月、日、时、分、秒的信息
unsigned unitFlags = NSCalendarUnitYear |
NSCalendarUnitMonth | NSCalendarUnitDay |
NSCalendarUnitHour | NSCalendarUnitMinute |
NSCalendarUnitSecond | NSCalendarUnitWeekday;
// 获取不同时间字段的信息
NSDateComponents* comp = [gregorian components: unitFlags
fromDate:dt];
// 获取各时间字段的数值
NSLog(@"现在是%ld年" , comp.year);
NSLog(@"现在是%ld月 " , comp.month);
NSLog(@"现在是%ld日" , comp.day);
NSLog(@"现在是%ld时" , comp.hour);
NSLog(@"现在是%ld分" , comp.minute);
NSLog(@"现在是%ld秒" , comp.second);
NSLog(@"现在是星期%ld" , comp.weekday);
// 再次创建一个NSDateComponents对象
NSDateComponents* comp2 = [[NSDateComponents alloc]
init];
// 设置各时间字段的数值
comp2.year = 2013;
comp2.month = 4;
comp2.day = 5;
comp2.hour = 18;
comp2.minute = 34;
// 通过NSDateComponents所包含的时间字段的数值来恢复NSDate对象
NSDate *date = [gregorian dateFromComponents:comp2];
NSLog(@"获取的日期为:%@" , date);


pod 组件,使用时, 头文件报 重复导入,使用检查头文件的方式导入

1
2
3
4
5
#if __has_include(<ShareSDK/ShareSDK.h>)
#import <ShareSDK/ShareSDK.h>
#else
#endif

格式化数据

1
2
3
4
5
6
7
8
NSLog(@"%02ld",2);
NSLog(@"%0.2f",0.2656);
NSLog(@"%0.2f",0.2646);
注意的是%0.2f 是会对数字进行一个四舍五入
14:57:28.506 App[4010:98217] 02
2016-06-20 14:57:28.507 App[4010:98217] 0.27
2016-06-20 14:57:28.507 App[4010:98217] 0.26

IOS知识点-小技巧-小笔记(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 保存到相册的老的方法 注意回调函数的写法 必须固定格式
let saveBool = UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(newURL!.path)
if saveBool == true{
// UISaveVideoAtPathToSavedPhotosAlbum(newURL!.path, self, nil, nil)
UISaveVideoAtPathToSavedPhotosAlbum(newURL!.path, self, #selector(self.image(image:didFinishSavingWithError:contextInfo:)), nil)
}
func image(image: UIImage, didFinishSavingWithError error: NSErrorPointer, contextInfo:UnsafeRawPointer) {
if error == nil {
printLog("保存成功")
} else {
printLog("保存失败\(error)")
}
}
1
2
3
4
5
6
//跳转到appstore
// let str = "http://itunes.apple.com/cn/app/id966492118?mt=8" // 可直接跳转到appstore
// let str = "itms://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=966492118" //跳转到itunes Store 也会定位到应用
let str = "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=966492118"//跳转到评分页 appstore
let url = URL(string: str)
UIApplication.shared.openURL(url!)

忽略 “Undeclared selector…” 的 Warning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (_delegate && [_delegate respondsToSelector:@selector(retrievingProgressMP4:)])
{
[_delegate performSelector:@selector(retrievingProgressMP4:) withObject:[NSNumber numberWithFloat:_exportSession.progress]];
// NSLog(@"Effect Progress: %f", exportSession.progress);
}
// 方法没有显示声明 ,会报一个警告, 可以这样局部去除, 局部去除比较合适
//忽略 "Undeclared selector..." 的 Warning
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
// 需要禁用警告的代码
#pragma clang diagnostic pop

将GIF图片分解成多张PNG图片,使用UIImageView播放。

需要导入#import

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"<#gifName#>" withExtension:@"gif"]; //加载GIF图片
CGImageSourceRef gifSource = CGImageSourceCreateWithURL((CFURLRef) fileUrl, NULL); //将GIF图片转换成对应的图片源
size_t frameCout = CGImageSourceGetCount(gifSource); //获取其中图片源个数,即由多少帧图片组成
NSMutableArray *frames = [[NSMutableArray alloc] init]; //定义数组存储拆分出来的图片
for (size_t i = 0; i < frameCout; i++) {
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(gifSource, i, NULL); //从GIF图片中取出源图片
UIImage *imageName = [UIImage imageWithCGImage:imageRef]; //将图片源转换成UIimageView能使用的图片源
[frames addObject:imageName]; //将图片加入数组中
CGImageRelease(imageRef);
}
UIImageView *gifImageView = [[UIImageView alloc] initWithFrame:CGRectMake(<#x#>, <#y#>, <#w#>, <#h#>)];
gifImageView.animationImages = frames; //将图片数组加入UIImageView动画数组中
gifImageView.animationDuration = 0.15; //每次动画时长
[gifImageView startAnimating]; //开启动画,此处没有调用播放次数接口,UIImageView默认播放次数为无限次,故这里不做处理
[self.view addSubview:gifImageView];

// 视频中获取图片 视频截取图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 正常mp4 好获取,不说 hls 渐进式加载, 理论我觉得seek 到位置 然后在获取 或者获取当前
CMTime itemTime = self.playerItem.currentTime;
CVPixelBufferRef pixelBuffer = [_playerItemVideoOutput copyPixelBufferForItemTime:itemTime itemTimeForDisplay:nil];
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CIContext *temporaryContext = [CIContext contextWithOptions:nil];
CGImageRef videoImage = [temporaryContext
createCGImage:ciImage
fromRect:CGRectMake(0, 0,
CVPixelBufferGetWidth(pixelBuffer),
CVPixelBufferGetHeight(pixelBuffer))];
UIImage *uiImage = [UIImage imageWithCGImage:videoImage];
CGImageRelease(videoImage);
NSLog(@"uiImage:%@", uiImage);
self.myImgView.image = uiImage;
分析得到:copyPixelBufferForItemTime这个方法需要的参数是当前avplayerItem的cmtime, 也就是说前提是正在播放视频流,不知道有没有理解错。

视频播放时候如何在静音模式下有声音

1
2
3
4
5
6
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
//swift
let audioSession = AVAudioSession.sharedInstance()
try? audioSession.setCategory(AVAudioSessionCategoryPlayback);

// 简单保存视频到相册 —

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let filepath = GetDownFilePath(fileName).path
printLog("测试保存地址 \(filepath)")
if ( UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(filepath) == true){
UISaveVideoAtPathToSavedPhotosAlbum(filepath, self, #selector(self.video(videoPath:didFinishSavingWithError:contextInfo:)), nil);
}
func video(videoPath: String, didFinishSavingWithError error: NSError?, contextInfo info: AnyObject) {
}
////UIImageWriteToSavedPhotosAlbum 保存函数的通知处理函数
func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
if error == nil{
MBManage.shareMode.showBriefHUD("保存成功")
}else{
MBManage.shareMode.showBriefHUD("保存失败")
}
}

view 层级调整 ,互换 层最上层等

1
2
3
4
5
6
7
8
9
//将view1挪到最上边
self.view.bringSubviewToFront(view1)
//将view1挪到最下边
self.view.sendSubviewToBack(view1)
//互换
self.view.exchangeSubviewAtIndex(2, withSubviewAtIndex: 3)

swift 3 继承变量问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 父类
/// 基类 是否支持旋转 默认false 不旋转
open var isRotate:Bool = false
/// 子类
override var isRotate: Bool {
get{
// return super.isRotate // 可设置
return true// 直接改成需要的true
}
set{
super.isRotate = newValue
}
}

是否显示状态栏

1
2
3
4
5
//在info.plist中添加
//View controller-based status bar appearance
//并且把值设定为NO,就可以在程序中自由控制状态栏的隐藏和显示了。
UIApplication.shared.setStatusBarHidden(false, with: UIStatusBarAnimation.none)

navigationController 自带边缘右滑 退出, 代理 可以设置启用和不启用 UIGestureRecognizerDelegate

1
2
3
4
5
6
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
//代理
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}

iOS获取数组的最大值

1
2
3
4
5
6
7
NSMutableArray* array = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
CGFloat num = arc4random() % 100 + 1;
[array addObject:[NSNumber numberWithFloat:num]];
}
CGFloat maxValue = [[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat minValue = [[array valueForKeyPath:@"@min.floatValue"] floatValue];

重点在这句话上
@”@max.floatValue”(获取最大值),
@”@min.floatValue”(获取最小值),
@”@avg.floatValue” (获取平均值),
@”@count.floatValue”(获取数组大小)
等等。。。。


oc通过强制类型转换四舍五入。swift 同理可行

1
2
3
4
float f = 1.5;
int a;
a = (int)(f+0.5);
NSLog("a = %d",a);

TabBarViewController 和UINavigationController的bar 上的黑线

1
2
3
let tabFrame = CGRect(x: 0, y: 0, width: stageWidth, height: self.tabBar.frame.height)
self.tabBar.backgroundImage = Getdevice.shared().image(withFrame: tabFrame, alphe: 1.0)
self.tabBar.shadowImage = UIImage()

UINavigationController 中的navigationBar 同理


iOS 字符串 中包含 % 百分号的方法

百分号的转换,NSString中需要格式化的字符串中百分号使用%%表示,而char*中百分号也是使用%%表示。

例如:NSLog(@”%%%@%%”,@”hello”),控制台会打印出%hello%。


cell 选择按钮替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/// 用户发布的视频显示 cell
class UserVideoManagerTableViewCell: UITableViewCell {
var showView:ShowVideoTableCellView!
override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 8 , *){
// ios7 没效果 第一次也不出现,还是得子定义
for control in self.subviews{
if control.isMember(of: NSClassFromString("UITableViewCellEditControl")!){
for v in control.subviews{
if v.isKind(of: UIImageView.classForCoder()){
//var img = v as! UIImageView
// v = UIImageView(image: UIImage(named: "activity_close"))
if self.isSelected{
(v as! UIImageView).image = UIImage(named: "v2_4_btn_set_selected")
}else{
//(v as! UIImageView).image = UIImage(named: "activity_new")//必须原先的图 因为效果不能一直有
}
}
}
}
}
}else{
for view in self.subviews{
if view.isMember(of: NSClassFromString("UITableViewCellScrollView")!){
printLog("views \(view.subviews)")
let views2 = view.subviews
for control in views2{
if control.isMember(of: NSClassFromString("UITableViewCellEditControl")!){
for v in control.subviews{
if v.isKind(of: UIImageView.classForCoder()){
let img = v as! UIImageView
if self.isSelected{
img.image = UIImage(named: "v2_4_btn_set_selected")
}else{
//img.image = UIImage(named: "activity_new")//必须原先的图 因为效果不能一直有
}
}
}
}
}
}
}
}
}
}

下面的方法本地还在
github 删除错误提交

git reset –hard HEAD~1
git push –force
就会删除远程的提交

1
2
3
git reset --hard <commit_id>
git push origin HEAD --force

其他:

1
2
3
4
5
6
7
8
9
10
11
根据–soft –mixed –hard,会对working tree和index和HEAD进行重置:
git reset –mixed:此为默认方式,不带任何参数的git reset,即时这种方式,它回退到某个版本,只保留源码,回退commitindex信息
git reset –soft:回退到某个版本,只回退了commit的信息,不会恢复到index file一级。如果还要提交,直接commit即可
git reset –hard:彻底回退到某个版本,本地的源码也会变为上一个版本的内容
HEAD 最近一个提交
HEAD^ 上一次
<commit_id> 每次commitSHA1值. 可以用git log 看到,也可以在页面上commit标签页里找到.
commit合并:

使用Cocoapods时忽略Xcode警告
我使用相当多的第三方库,其中有很多警告,在最新的Xcode更新后。 (例如Facebook SDK pod)
现在所有这些警告都显示在我的Xcode上我想看到自己的警告或错误的地方。
有没有办法忽略这些错误?修复它们不会有帮助,因为在每个“pod安装”之后,更改被丢弃。
添加到您的Podfile:

1
2
3
4
5
6
7
platform :ios
# ignore all warnings from all pods
inhibit_all_warnings!
# ignore warnings from a specific pod
pod 'FBSDKCoreKit', :inhibit_warnings => true

然后执行:pod install


链接
down vote
As it has already been answered, ObjC doesn’t support method overloading (two methods with the same name) and In swift 2 under Xcode 7 there are two options to solve this kind of problems. One option is to rename the method using the attribute: @objc(newNameMethod:)

func methodOne(par1, par2) {…}

@objc(methodTwo:) 标记为objc
func methodOne(par1) {…}
another option to solve this problem in Xcode 7+ is by applying @nonobjc attribute to any method, subscript or initialiser

func methodOne() {…}

@nonobjc // 标记为非objc
func methodOne() {…}


//NSRange转化为range
extension String {
func range(from nsRange: NSRange) -> Range? {
guard
let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self)
else { return nil }
return from ..< to
}
}

//range转换为NSRange
extension String {
func nsRange(from range: Range) -> NSRange {
let from = range.lowerBound.samePosition(in: utf16)
let to = range.upperBound.samePosition(in: utf16)
return NSRange(location: utf16.distance(from: utf16.startIndex, to: from),
length: utf16.distance(from: from, to: to))
}
}

swift 4

NSRange(range, in: string) //return 这个 range 转NSRange

/// string 中 查找子串
extension String {
func nsranges(of string: String) -> [NSRange] {
return ranges(of: string).map { (range) -> NSRange in
self.nsrange(fromRange: range)
}
}
}

// range 转 NSRange
extension String {
func nsrange(fromRange range : Range) -> NSRange {
return NSRange(range, in: self)
}
}

链接:
链1 链2


1
2
3
4
5
6
7
8
9
10
// kvc 实现提示 TextView 的提示文本(私有api)
self.placeHolderLabel = UILabel()
self.placeHolderLabel.font = GlobalFont_32
self.placeHolderLabel.textColor = UIColor.lightGray
self.placeHolderLabel.text = "请输入内容"
self.placeHolderLabel.sizeToFit()
self.inputeTextView.addSubview(self.placeHolderLabel)
self.inputeTextView.setValue(self.placeHolderLabel, forKey: "_placeholderLabel")
或者用titleTextField.attributedPlaceholder 参数

—YYlabel-YY–

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 一个yylabel 搞点点击处理
func setData(mode:MJ_UserMessage){
self.mode = mode
let titleString = "\(mode.source_title) 赞了你的视频"
let attrText = NSMutableAttributedString(string: titleString)
attrText.yy_font = GlobalFont_32
attrText.yy_color = GlobalMainToneTextColor_lime
let fromUserRange = NSMakeRange(0, mode.source_title.characters.count)
let fromUserHighlight = YYTextHighlight.init(backgroundColor: UIColor.clear)// 高亮的背景色 先没有
fromUserHighlight.userInfo = ["MJ_UserMessage":mode];// 把评论者信息存储到userinfo中
attrText.yy_setTextHighlight(fromUserHighlight, range: fromUserRange)
attrText.yy_setColor(UIColor(red:0.90, green:0.30, blue:0.25, alpha:1.00), range: fromUserRange)
self.messageNameLabel.attributedText = attrText
self.messageNameLabel.sizeToFit() //YYLabel
self.messageNameLabel.highlightTapAction = { [weak self](containerView:UIView ,text:NSAttributedString ,range:NSRange ,rect:CGRect) in
if let weakSelf = self{
let highlight:YYTextHighlight = containerView.value(forKeyPath: "_highlight") as! YYTextHighlight
let mode = highlight.userInfo?["MJ_UserMessage"] as? MJ_UserMessage
/// 代理吧数据传出去 (被评论人无user ImageUrl 字段)
if mode == nil || mode?.source_type != "user"{return}
DispatchQueue.main.async {
let controller = MyMainPageViewController(NavTitle: mode!.source_ID, userId: mode!.source_title)
weakSelf.findController().navigationController?.pushViewController(controller, animated: true)
}
}
}
}

ios 防止锁屏

1
UIApplication.shared.isIdleTimerDisabled = true // 长亮 反之正常

一个oc警告忽略
忽略 “Undeclared selector…” 的 Warning

1
2
3
4
if ([someObject respondsToSelector:@selector(someSelector)])
{
[someObject performSelector:@selector(someSelector)];
}

以上这句代码,除非你在 someObject 的头文件中显式地声明了 someSelector,否则在 Xcode 中会提示警告:
Undeclared selector ‘someSelector’
但很多情况下我们并不想去声明它,此时我们可以禁用编译器的此类警告:

1
#pragma GCC diagnostic ignored "-Wundeclared-selector"

这样将会在整个文件内禁用此类警告,也可只在部分代码处禁用,保证编译器依然会对文件内其他代码进行警告检测,避免出现预料之外的 bug:

1
2
3
4
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
// 需要禁用警告的代码
#pragma clang diagnostic pop

记录事件, GPUImage 运行时 崩溃 8.3 系统上 ,只有debug 连着电脑运行才崩溃 , 只要正常安装正常


Provisioning Profile相关
在~/Library/MobileDevice/Provisioning Profiles目录可以看到本机所有的Provisioning Profile,但是这里显示都是.mobileprovision这样的文件,并不是很好辨认。
一般更新设备删除~/Library/MobileDevice/Provisioning
在xcode 重新下载


ios 苹果的测试 The TestFlight app 中测试 可以测试正式环境的推送


xcode 代码块 导出到其他电脑使用
cd /Users/用户名/Library/Developer/Xcode/UserData/

将CodeSnippets拷贝到新电脑的对应的目录下


swift 4 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
So, change
text.substring(to: 3)
text.substring(from: 3)
to
String(text.prefix(3))
String(text[text.index(text.startIndex, offsetBy: 3)...])
let newStr = str.substring(to: index) // Swift 3
let newStr = String(str[..<index]) // Swift 4
let newStr = str.substring(from: index) // Swift 3
let newStr = String(str[index...]) // Swift 4
let range = firstIndex..<secondIndex // If you have a range
let newStr = = str.substring(with: range) // Swift 3
let newStr = String(str[range]) // Swift 4

详情


根据字符串进行类的实例化 NSClassFromString 使用

1
2
3
4
5
6
7
//oc
Class vc_class = NSClassFromString(class_name);
//swift
let classtest = NSClassFromString("项目名.类名") as? 类名.Type
classtest?.init()
printLog("测试 \(classtest?.init())")

— 代码行数统计——
项目目录终端执行
其中 -name “*.m” 就表示扩展名为.m的文件。

1
find . "(" -name "*.m" -or -name "*.mm" -or -name "*.swift" -or -name "*.cpp" -or -name "*.h" -or -name "*.rss" ")" -print | xargs wc -l

swift KVO 需要的条件

swift 4 得 @objc dynamic
前缀 基于运行时,动态变量,
一般在swift 中用自动布局SDAutoLayout 自动cell 高度 需要使用这个


一个swift 闭包内存处理

1
2
3
4
5
6
7
8
9
lazy var newBtn: UIButton = {
// 可以直接self 因为lazy 自动处理了 (其他的一些 didSet 等 同理)
let obj = UIButton();
obj.addAction({ [weak self](btn) in
// 但是这里还是得标记
self?.delegate?.xxx
})
return obj;
}()


player 播放器网络差处理 摘取自CLPlayerDemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#pragma mark - 监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"status"]) {
if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) {
self.state = CLPlayerStatePlaying;
self.player.muted = self.mute;
}
else if (self.player.currentItem.status == AVPlayerItemStatusFailed) {
self.state = CLPlayerStateFailed;
}
} else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
// 计算缓冲进度
NSTimeInterval timeInterval = [self availableDuration];
CMTime duration = self.playerItem.duration;
CGFloat totalDuration = CMTimeGetSeconds(duration);
[self.maskView.progress setProgress:timeInterval / totalDuration animated:NO];
} else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
// 当缓冲是空的时候
if (self.playerItem.isPlaybackBufferEmpty) {
[self bufferingSomeSecond];
}
} else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
// 当缓冲好的时候
if (self.playerItem.isPlaybackLikelyToKeepUp && self.state == CLPlayerStateBuffering){
self.state = CLPlayerStatePlaying;
}
}
}
#pragma mark - 缓冲较差时候
//卡顿时会走这里
- (void)bufferingSomeSecond{
self.state = CLPlayerStateBuffering;
// 需要先暂停一小会之后再播放,否则网络状况不好的时候时间在走,声音播放不出来
[self pausePlay];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self playVideo];
// 如果执行了play还是没有播放则说明还没有缓存好,则再次缓存一段时间
if (!self.playerItem.isPlaybackLikelyToKeepUp) {
[self bufferingSomeSecond];
}
});
}

一个xcode注释文档警告忽略
Bulid Settings -> Documentation Comments -> NO


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
常用的一些占位符:
%@:字符串占位符
%d:整型
%ld:长整型
%f:浮点型
%c:char类型
%%:%的占位符
BOOL studyBool = YES;
NSLog(@"打印BOOL型数据%@",studyBool?@"YES":@"NO");//打印BOOL型数据YES
NSLog(@"打印BOOL型数据%d",studyBool);//打印BOOL型数据1
BOOL alsoBool = NO;
NSLog(@"打印BOOL型数据%@",alsoBool?@"YES":@"NO");//打印BOOL型数据NO
NSLog(@"打印BOOL型数据%d",alsoBool);//打印BOOL型数据0
详细介绍:**********************************************************
%@: Objective-C对象,印有字符串返回descriptionWithLocale:如果于的话,或描述相反.CFTypeRef工作对象,返回的结果的CFCopyDescription功能.(这个翻译有问题建议按照自己的理解方式理解)。
%%: 为'%'字符;
%d,%D,%i: 为32位整型数(int);
%u,%U: 为32位无符号整型数(unsigned int);
%hi: 为有符号的16位整型数(short);
%hu: 为无符号的16位整型数(unsigned shord);
%qi: 为有符号的64位整型数(long long);
%qu: 为无符号的64位整型数(unsigned long long);
%x: 为32位的无符号整型数(unsigned int),打印使用数字0-9的十六进制,小写a-f;
%X: 为32位的无符号整型数(unsigned int),打印使用数字0-9的十六进制,大写A-F;
%qx: 为无符号64位整数(unsigned long long),打印使用数字0-9的十六进制,小写a-f;
%qX: 为无符号64位整数(unsigned long long),打印使用数字0-9的十六进制,大写A-F;
%o,%O: 为32位的无符号整数(unsigned int),打印八进制数;
%f: 为64位的浮点数(double);
%e: 为64位的浮点数(double),打印使用小写字母e,科学计数法介绍了指数的增大而减小;
%E: 为64位的浮点数(double),打印科学符号使用一个大写E介绍指数的增大而减小;
%g: 为64位的浮点数(double),用%e的方式打印指数,如果指数小于4或者大于等于精度,那么%f的风格就会有不同体现;
%G: 为64位的浮点数(double),用%E的方式打印指数,如果指数小于4或者大于等于精度,那么%f的风格就会有不同体现;
%c: 为8位的无符号字符%c(unsigned char),通过打印NSLog()将其作为一个ASCII字符,或者,不是一个ASCII字符,八进制格式\ddd或统一标准的字符编码的十六进制格式\udddd,在这里d是一个数字;
%C: 为16位Unicode字符%C(unichar),通过打印NSLog()将其作为一个ASCII字符,或者,不是一个ASCII字符,八进制格式\ddd或统一标准的字符编码的十六进制格式\udddd,在这里d是一个数字;
%s: 对于无符号字符数组空终止,%s系统中解释其输入编码,而不是别的,如utf-8;
%S: 空终止一系列的16位Unicode字符;
%p: 空指针(无效*),打印十六进制的数字0-9和小写a-f,前缀为0x;
%L: 在明确规定的长度下,进行修正,下面的一批数据a,A,e,E,f,F,g,G应用于双精度长整型的参数;
%a: 为64位的浮点数(double),按照科学计数法打印采用0x和一个十六进制数字前使用小写小数点p来介绍指数的增大而减小;
%A: 为64位的浮点数(double),按照科学计数法打印采用0X和一个十六进制数字前使用大写字母小数点P界扫指数的增大而减小;
%F: 为64位的浮点数(double),按照十进制表示法进行打印;
%z: 修改说明在%z长度以下d,i,o,u,x,X适用于某一指定类型的转换或者适用于一定尺寸的整数类型的参数;
%t: 修改说明在%t长度以下d,i,o,u,x,X适用于某一指定类型或一定尺寸的整数类型的转换的参数;
%j: 修改说明在%j长度以下d,i,o,u,x,X适用于某一指定类型或一定尺寸的整数类型的转换的参数

swift中的正则表达式

swift中的t正则表达式
正则表达式是对字符串操作的一种逻辑公式,用事先定义好的一些特定字符、及这些特定字符的组合,组成一个”规则字符串”,这个”规则字符串”用来表达对字符串的一种过滤逻辑。

正则表达式的用处:
判断给定的字符串是否符合某一种规则(专门用于操作字符串)
电话号码,电子邮箱,URL…
可以直接百度别人写好的正则
别人真的写好了,而且测试过了,我们可以直接用
要写出没有漏洞正则判断,需要大量的测试,通常最终结果非常负责
过滤筛选字符串,网络爬虫
替换文字,QQ聊天,图文混排

语法规则
1> 集合

1
2
3
4
5
6
[xyz] 字符集合(x/y或z)
[a-z] 字符范围
[a-zA-Z]
[^xyz] 负值字符集合 (任何字符, 除了xyz)
[^a-z] 负值字符范围
[a-d][m-p] 并集(a到d 或 m到p)

2> 常用元字符

1
2
3
4
5
6
7
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线或汉字 [a-zA-Z_0-9]
\s 匹配任意的空白符(空格、TAB\t、回车\r \n
\d 匹配数字 [0-9]
^ 匹配字符串的开始
$ 匹配字符串的结束
\b 匹配单词的开始或结束

2> 常用反义符

1
2
3
4
5
6
\W 匹配任意不是字母,数字,下划线,汉字的字符[^\w]
\S 匹配任意不是空白符的字符 [^\s]
\D 匹配任意非数字的字符[^0-9]
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

4> 常用限定符

1
2
3
4
5
6
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n
{n,} 重复n次或更多次
{n,m} 重复n到m次,

5> 贪婪和懒惰

1
2
3
4
5
*? 重复任意次,但尽可能少重复
*+ 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

使用过程
1、创建规则
2、创建正则表达式对象
3、开始匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private func check(str: String) {
// 使用正则表达式一定要加try语句
do {
// - 1、创建规则
let pattern = "[1-9][0-9]{4,14}"
// - 2、创建正则表达式对象
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpressionOptions.CaseInsensitive)
// - 3、开始匹配
let res = regex.matchesInString(str, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, str.characters.count))
// 输出结果
for checkingRes in res {
print((str as NSString).substringWithRange(checkingRes.range))
}
}
catch {
print(error)
}
}

其他几个常用方法

1
2
3
4
5
6
7
8
9
10
11
// 匹配字符串中所有的符合规则的字符串, 返回匹配到的NSTextCheckingResult数组
public func matchesInString(string: String, options: NSMatchingOptions, range: NSRange) -> [NSTextCheckingResult]
// 按照规则匹配字符串, 返回匹配到的个数
public func numberOfMatchesInString(string: String, options: NSMatchingOptions, range: NSRange) -> Int
// 按照规则匹配字符串, 返回第一个匹配到的字符串的NSTextCheckingResult
public func firstMatchInString(string: String, options: NSMatchingOptions, range: NSRange) -> NSTextCheckingResult?
// 按照规则匹配字符串, 返回第一个匹配到的字符串的范围
public func rangeOfFirstMatchInString(string: String, options: NSMatchingOptions, range: NSRange) -> NSRange

使用子类来匹配日期、地址、和URL
看官网文档解释,可以知道这个NSDataDetector主要用来匹配日期、地址、和URL。在使用时指定要匹配的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class NSDataDetector : NSRegularExpression {
// all instance variables are private
/* NSDataDetector is a specialized subclass of NSRegularExpression. Instead of finding matches to regular expression patterns, it matches items identified by Data Detectors, such as dates, addresses, and URLs. The checkingTypes argument should contain one or more of the types NSTextCheckingTypeDate, NSTextCheckingTypeAddress, NSTextCheckingTypeLink, NSTextCheckingTypePhoneNumber, and NSTextCheckingTypeTransitInformation. The NSTextCheckingResult instances returned will be of the appropriate types from that list.
*/
public init(types checkingTypes: NSTextCheckingTypes) throws
public var checkingTypes: NSTextCheckingTypes { get }
}
// 这个是类型选择
public static var Date: NSTextCheckingType { get } // date/time detection
public static var Address: NSTextCheckingType { get } // address detection
public static var Link: NSTextCheckingType { get } // link detection

NSDataDetector 获取URL示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
匹配字符串中的URLS
- parameter str: 要匹配的字符串
*/
private func getUrl(str:String) {
// 创建一个正则表达式对象
do {
let dataDetector = try NSDataDetector(types: NSTextCheckingTypes(NSTextCheckingType.Link.rawValue))
// 匹配字符串,返回结果集
let res = dataDetector.matchesInString(str, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, str.characters.count))
// 取出结果
for checkingRes in res {
print((str as NSString).substringWithRange(checkingRes.range))
}
}
catch {
print(error)
}
}

“.*?” 可以满足一些基本的匹配要求
如果想同时匹配多个规则 ,可以通过 “|” 将多个规则连接起来
将字符串中文字替换为表情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
显示字符中的表情
- parameter str: 匹配字符串
*/
private func getEmoji(str:String) {
let strM = NSMutableAttributedString(string: str)
do {
let pattern = "\\[.*?\\]"
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpressionOptions.CaseInsensitive)
let res = regex.matchesInString(str, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, str.characters.count))
var count = res.count
// 反向取出文字表情
while count > 0 {
let checkingRes = res[--count]
let tempStr = (str as NSString).substringWithRange(checkingRes.range)
// 转换字符串到表情
if let emoticon = EmoticonPackage.emoticonWithStr(tempStr) {
print(emoticon.chs)
let attrStr = EmoticonTextAttachment.imageText(emoticon, font: 18)
strM.replaceCharactersInRange(checkingRes.range, withAttributedString: attrStr)
}
}
print(strM)
// 替换字符串,显示到label
emoticonLabel.attributedText = strM
}
catch {
print(error)
}
}

TextKit 给URL高亮显示
主要用到三个类
NSTextStorage
NSLayoutManager
NSTextContainer
自定义UILabel来实现url高亮

1、定义要用到的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
只要textStorage中的内容发生变化, 就可以通知layoutManager重新布局
layoutManager重新布局需要知道绘制到什么地方, 所以layoutManager就会文textContainer绘制的区域
*/
// 准们用于存储内容的
// textStorage 中有 layoutManager
private lazy var textStorage = NSTextStorage()
// 专门用于管理布局
// layoutManager 中有 textContainer
private lazy var layoutManager = NSLayoutManager()
// 专门用于指定绘制的区域
private lazy var textContainer = NSTextContainer()
override init(frame: CGRect) {
super.init(frame: frame)
setupSystem()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupSystem()
}
private func setupSystem()
{
// 1.将layoutManager添加到textStorage
textStorage.addLayoutManager(layoutManager)
// 2.将textContainer添加到layoutManager
layoutManager.addTextContainer(textContainer)
}
override func layoutSubviews() {
super.layoutSubviews()
// 3.指定区域
textContainer.size = bounds.size
}

2、重写label的text属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override var text: String?
{
didSet{
// 1.修改textStorage存储的内容
textStorage.setAttributedString(NSAttributedString(string: text!))
// 2.设置textStorage的属性
textStorage.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(20), range: NSMakeRange(0, text!.characters.count))
// 3.处理URL
self.URLRegex()
// 2.通知layoutManager重新布局
setNeedsDisplay()
}
}

3、匹配字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func URLRegex()
{
// 1.创建一个正则表达式对象
do{
let dataDetector = try NSDataDetector(types: NSTextCheckingTypes(NSTextCheckingType.Link.rawValue))
let res = dataDetector.matchesInString(textStorage.string, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, textStorage.string.characters.count))
// 4取出结果
for checkingRes in res
{
let str = (textStorage.string as NSString).substringWithRange(checkingRes.range)
let tempStr = NSMutableAttributedString(string: str)
// tempStr.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, str.characters.count))
tempStr.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(20), NSForegroundColorAttributeName: UIColor.redColor()], range: NSMakeRange(0, str.characters.count))
textStorage.replaceCharactersInRange(checkingRes.range, withAttributedString: tempStr)
}
}catch
{
print(error)
}
}

4、重绘文字

1
2
3
4
5
6
7
8
9
10
// 如果是UILabel调用setNeedsDisplay方法, 系统会促发drawTextInRect
override func drawTextInRect(rect: CGRect) {
// 重绘
// 字形 : 理解为一个小的UIView
/*
第一个参数: 指定绘制的范围
第二个参数: 指定从什么位置开始绘制
*/
layoutManager.drawGlyphsForGlyphRange(NSMakeRange(0, text!.characters.count), atPoint: CGPointZero)
}

获取label中URL的点击

如果要获取URL的点击,那么必须获取点击的范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
// 1、获取手指点击的位置
let touch = (touches as NSSet).anyObject()!
let point = touch.locationInView(touch.view)
print(point)
// 2、获取URL区域
// 注意: 没有办法直接设置UITextRange的范围
let range = NSMakeRange(10, 20)
// 只要设置selectedRange, 那么就相当于设置了selectedTextRange
selectedRange = range
// 给定指定的range, 返回range对应的字符串的rect
// 返回数组的原因是因为文字可能换行
let array = selectionRectsForRange(selectedTextRange!)
for selectionRect in array {
if CGRectContainsPoint(selectionRect.rect, point) {
print("点击了URL")
}
}
}

转自:http://www.cnblogs.com/songliquan/p/4860196.html

ios 跑马灯

swift3 可将time 换成GCD 优化 使用过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/// 跑马灯View 需要手动调用清理函数
class MarqueeView: UIView {
var iconImageView:UIImageView!
var titleLabel:UILabel!
var showLabel:UILabel!
/// 显示的跑马灯数组
var showTextArray:[String] = ["测试跑马灯1","测试跑马灯2","测试跑马灯3","测试跑马灯4"]
/// 显示到的位置
var showTextCount = 0
var showtime:Timer?
override init(frame: CGRect) {
super.init(frame: frame)
let icon_W = frame.height*0.4444
self.iconImageView = UIImageView(frame: CGRect(x: interval_W_20, y: 0, width: icon_W, height: icon_W))
self.iconImageView.frame.origin.y = frame.height * 0.2777
self.addSubview(self.iconImageView)
self.titleLabel = UILabel()
self.titleLabel.font = GlobalFont_32
self.titleLabel.textColor = GlobalMainToneColor_2_4
self.addSubview(self.titleLabel)
self.showLabel = UILabel()
self.showLabel.font = GlobalFont_32
self.showLabel.textColor = GlobalTxtColor_1
self.addSubview(self.showLabel)
self.layer.masksToBounds = true
}
func setData(icon:UIImage,title:String,showTextArr:[String],backColor:UIColor = UIColor.white) {
self.backgroundColor = backColor
self.iconImageView.image = icon
self.titleLabel.text = title.truncate(start: 0, end: 8)
self.titleLabel.sizeToFit()
self.titleLabel.frame.origin.x = self.iconImageView.frame.origin.x+self.iconImageView.frame.width+interval_W_20
self.titleLabel.center.y = self.iconImageView.center.y
self.showLabel.frame.origin.x = self.titleLabel.frame.origin.x+self.titleLabel.frame.width + interval_W_20
if self.showTextArray != showTextArr{
self.showTextArray = showTextArr
self.showTextCount = 0
}
if showtime != nil{
showtime!.invalidate()
showtime = nil
}
self.timeChange()
self.showtime = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timeChange), userInfo: nil, repeats: true)
}
deinit {
printLog("deinit MarqueeView")
}
func clear(){
self.stopTime()
}
/// 必须手动调用
private func stopTime(){
if showtime != nil{
showtime!.invalidate()
showtime = nil
}
}
/// 时间间隔
let timeInterval:Double = 5
/// 时间到处理函数
func timeChange(){
if self.showTextCount < self.showTextArray.count{
self.showLabel.text = self.showTextArray[self.showTextCount]
self.showTextCount += 1
}else{
self.showTextCount = 0
self.showLabel.text = self.showTextArray[self.showTextCount]
}
self.showLabel.sizeToFit()
var frame = self.showLabel.frame
frame.origin.y = self.frame.height
self.showLabel.frame = frame
UIView.beginAnimations("showText", context: nil)
UIView.setAnimationDuration(timeInterval)
UIView.setAnimationCurve(UIViewAnimationCurve.linear)
UIView.setAnimationDelegate(self)
UIView.setAnimationRepeatAutoreverses(false)
UIView.setAnimationRepeatCount(0)
frame = self.showLabel.frame
frame.origin.y = -frame.size.height
self.showLabel.frame = frame
UIView.commitAnimations()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}