普通视图
08-iOS 多媒体技术|图形编程框架OpenGL、OpenGL ES、Metal要点回顾【渲染流水线、坐标系、着色器程序、GLSL语言等】
07-iOS 多媒体技术| 滤镜框架Core Image、GPUImage要点回顾【滤镜链、内置滤镜、自定义滤镜、GPUImage的简单使用等】
前言
我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:
- 进而 用 两篇文章 对 其中的
UIKit
相关要点 进行了分述:- 然后我们 针对 Core Animation框架的要点 进一步展开分述:
- 紧接着 我们快速回顾了 2D绘图引擎Core Graphics框架的要点
- 我们 在此篇文章 ,将 针对 滤镜框架Core Image、GPUImage框架的要点 进一步展开分述:
一、Core Image
Core Image
是苹果提供的图像处理框架,主要用于实现图像处理
、滤镜应用
和图像分析
等功能。以下是Core Image的核心要点:
1. 主要特点和功能
-
- 滤镜(Filter):
- Core Image提供了丰富的滤镜效果,如
模糊
、锐化
、色彩调整
、边缘检测
等。 - 开发者可以通过Core Image的滤镜功能对图像进行处理和增强。
- iOS 8 之后 引入 CIFilter,Core Image从此支持自定义滤镜的创建和应用,实现个性化的图像处理效果。
-
- 图像处理链(Image Processing Pipeline):
- Core Image使用图像处理链来处理图像,包括
输入图像
、滤镜效果
、和输出图像
。 - 开发者可以构建自定义的图像处理链,实现复杂的图像处理流程。
-
- 图像分析(Image Analysis):
- Core Image支持图像分析功能,如
人脸检测
、特征识别
、颜色识别
、物体识别
等。 - 开发者可以利用Core Image进行图像分析,
提取图像中的信息和特征
。
-
- Metal性能优化:
- Core Image可以与Metal框架结合,利用GPU硬件加速来提高图像处理的性能。
- 开发者可以通过Metal框架优化Core Image的性能,实现高效的图像处理和滤镜效果。
-
- 图像格式转换(Image Format Conversion):
- Core Image支持图像格式的转换和处理,如
颜色空间转换
、像素格式转换
等。 - 开发者可以使用Core Image进行图像格式的转换和处理,满足不同的图像处理需求。
-
- 实时预览(Real-time Preview):
- Core Image提供实时预览功能,可以在应用程序中实时显示滤镜效果的预览。
- 开发者可以通过Core Image实现实时的滤镜预览,方便用户调整和查看效果。
-
- 其它
-
Core Image
与 Core Graphics 恰恰相反- Core Graphics 用于在运行时创建图像
- 而 Core Image 是用来处理已经创建的图像的。
-
Core Image
是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。- iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。
-
Core Image
的优点在于十分高效。大部分情况下,它会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。如果设备支持 Metal,那么会使用 Metal 处理。这些操作会在底层完成,Apple 的工程师们已经帮助开发者们完成这些操作了。- 例如他可以根据需求选择 CPU 或者 GPU 来处理。
// 创建基于 CPU 的 CIContext 对象 (默认是基于 GPU,CPU 需要额外设置参数) context = [CIContext contextWithOptions: [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]]; // 创建基于 GPU 的 CIContext 对象 context = [CIContext contextWithOptions: nil]; // 创建基于 GPU 的 CIContext 对象 EAGLContext *eaglctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; context = [CIContext contextWithEAGLContext:eaglctx];
2. 核心类介绍
-
- CIImage:
- CIImage是Core Image框架中表示图像数据的类,可以用来表示图像数据源
- CIImage可以从各种来源创建,如UIImage、CGImage、NSData、图像文件或者像素数据等,用于输入到Core Image的滤镜中进行处理。
-
- CIFilter:
- CIFilter是Core Image框架中的滤镜类,用于实现各种图像处理和滤镜效果。
- CIFilter包含了各种内置的滤镜效果,也可以自定义滤镜来实现特定的图像处理需求。
- 这个框架中对图片属性进行细节处理的类。它对所有的像素进行操作,用一些键-值设置来决定具体操作的程度。
-
- CIContext:
- CIContext是Core Image框架中的上下文类,用于管理图像处理的环境和输出。
- CIContext可以指定渲染目标(如屏幕、图像文件)、渲染选项(如颜色空间、缩放比例)等参数。
-
- CIFeature:
- CIFeature是Core Image框架中的特征类,用于检测图像中的特征和对象。
- CIFeature可以用于人脸检测、文本识别、条形码识别等应用场景。
-
- CIColor:
- CIColor是Core Image框架中的颜色类,用于表示颜色信息。
- CIColor可以用来创建颜色对象,设置滤镜效果中的颜色参数。
-
- CIVector:
- CIVector是Core Image框架中的向量类,用于表示多维向量数据。
- CIVector可以用来设置滤镜效果中的向量参数,如位置、大小等。
- Core Image 的 API 主要就是三类:
- CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于处理绘制渲染以及处理托管对象一样,Core Image 的上下文也是实现对图像处理的具体对象。可以从其中取得图片的信息。
3. 代码示例
3.1 滤镜|模糊、锐化、色彩调整、边缘检测 和 自定义滤镜
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>
// 模糊处理
UIImage *applyBlurFilter(UIImage *image) {
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"];
[filter setValue:ciImage forKey:kCIInputImageKey];
[filter setValue:@10 forKey:kCIInputRadiusKey];
CIImage *outputImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return resultImage;
}
// 锐化处理
UIImage *applySharpenFilter(UIImage *image) {
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
CIFilter *filter = [CIFilter filterWithName:@"CISharpenLuminance"];
[filter setValue:ciImage forKey:kCIInputImageKey];
[filter setValue:@0.5 forKey:kCIInputSharpnessKey];
CIImage *outputImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return resultImage;
}
// 色彩调整
UIImage *applyColorAdjustmentFilter(UIImage *image) {
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
[filter setValue:ciImage forKey:kCIInputImageKey];
[filter setValue:@1.2 forKey:kCIInputContrastKey];
[filter setValue:@0.5 forKey:kCIInputBrightnessKey];
[filter setValue:@0.8 forKey:kCIInputSaturationKey];
CIImage *outputImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return resultImage;
}
// 边缘检测
UIImage *applyEdgeDetectionFilter(UIImage *image) {
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
CIFilter *filter = [CIFilter filterWithName:@"CIEdges"];
[filter setValue:ciImage forKey:kCIInputImageKey];
CIImage *outputImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return resultImage;
}
// 自定义滤镜
UIImage *applyCustomFilter(UIImage *image) {
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
// 自定义滤镜
CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"];
[filter setValue:ciImage forKey:kCIInputImageKey];
// 设置自定义参数
CIVector *RVector = [CIVector vectorWithX:1 Y:0 Z:0 W:0];
CIVector *GVector = [CIVector vectorWithX:0 Y:1 Z:0 W:0];
CIVector *BVector = [CIVector vectorWithX:0 Y:0 Z:1 W:0];
CIVector *AVector = [CIVector vectorWithX:0 Y:0 Z:0 W:1];
[filter setValue:RVector forKey:@"inputRVector"];
[filter setValue:GVector forKey:@"inputGVector"];
[filter setValue:BVector forKey:@"inputBVector"];
[filter setValue:AVector forKey:@"inputAVector"];
CIImage *outputImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *resultImage = [UIImage imageWithCGImage
Swift示例:
import UIKit
import CoreImage
// 图像处理类
class ImageProcessor {
// 应用模糊滤镜
func applyBlurFilter(to image: UIImage) -> UIImage? {
if let ciImage = CIImage(image: image) {
let filter = CIFilter(name: "CIGaussianBlur")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(5.0, forKey: kCIInputRadiusKey)
if let outputImage = filter?.outputImage {
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage)
}
}
}
return nil
}
// 应用锐化滤镜
func applySharpenFilter(to image: UIImage) -> UIImage? {
if let ciImage = CIImage(image: image) {
let filter = CIFilter(name: "CISharpenLuminance")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(0.5, forKey: kCIInputSharpnessKey)
if let outputImage = filter?.outputImage {
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage)
}
}
}
return nil
}
// 应用色彩调整滤镜
func applyColorAdjustmentFilter(to image: UIImage) -> UIImage? {
if let ciImage = CIImage(image: image) {
let filter = CIFilter(name: "CIColorControls")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(1.2, forKey: kCIInputContrastKey)
filter?.setValue(0.8, forKey: kCIInputBrightnessKey)
filter?.setValue(0.5, forKey: kCIInputSaturationKey)
if let outputImage = filter?.outputImage {
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage)
}
}
}
return nil
}
// 应用边缘检测滤镜
func applyEdgeDetectionFilter(to image: UIImage) -> UIImage? {
if let ciImage = CIImage(image: image) {
let filter = CIFilter(name: "CIEdges")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
if let outputImage = filter?.outputImage {
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage)
}
}
}
return nil
}
// 应用自定义滤镜
func applyCustomFilter(to image: UIImage) -> UIImage? {
if let ciImage = CIImage(image: image) {
// 自定义滤镜:将图像转为灰度图
let filter = CIFilter(name: "CIPhotoEffectMono")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
if let outputImage = filter?.outputImage {
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage)
}
}
}
return nil
}
}
// 使用示例
let image = UIImage(named: "example.jpg")!
let processor = ImageProcessor()
let blurredImage = processor.applyBlurFilter(to: image)
let sharpenedImage = processor.applySharpenFilter(to: image)
let colorAdjustedImage = processor.applyColorAdjustmentFilter(to: image)
let edgeDetectedImage = processor.applyEdgeDetectionFilter(to: image)
let customFilteredImage = processor.applyCustomFilter(to: image)
3.1 图像处理链|输入图像、滤镜效果、和输出图像
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>
UIImage *applyFilterToImage(UIImage *inputImage) {
CIImage *ciImage = [[CIImage alloc] initWithImage:inputImage];
CIFilter *filter = [CIFilter filterWithName:@"CIPhotoEffectMono"];
[filter setValue:ciImage forKey:kCIInputImageKey];
CIImage *outputImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage *filteredImage = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return filteredImage;
}
Swift示例:
// Swift示例:
import UIKit
import CoreImage
func applyFilterToImage(inputImage: UIImage) -> UIImage? {
if let ciImage = CIImage(image: inputImage) {
let filter = CIFilter(name: "CIPhotoEffectMono")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
if let outputImage = filter?.outputImage {
let context = CIContext(options: nil)
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
let filteredImage = UIImage(cgImage: cgImage)
return filteredImage
}
}
}
return nil
}
3.1 图像分析|人脸检测、特征识别、颜色识别、物体识别
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>
- (void)analyzeImage:(UIImage *)image {
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
// 人脸检测
NSDictionary *options = @{CIDetectorAccuracy: CIDetectorAccuracyHigh};
CIDetector *faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:nil options:options];
NSArray *faces = [faceDetector featuresInImage:ciImage];
for (CIFaceFeature *faceFeature in faces) {
NSLog(@"Detected face at %@", NSStringFromCGRect(faceFeature.bounds));
}
// 特征识别
NSDictionary *featureOptions = @{CIDetectorImageOrientation: @(1)};
CIDetector *featureDetector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:featureOptions];
NSArray *features = [featureDetector featuresInImage:ciImage];
for (CIQRCodeFeature *feature in features) {
NSLog(@"Detected QR code with message: %@", feature.messageString);
}
// 颜色识别
CIColor *averageColor = [ciImage valueForKey:@"inputImage"];
NSLog(@"Average color of the image: %@", averageColor);
// 物体识别
NSDictionary *objectOptions = @{CIDetectorAccuracy: CIDetectorAccuracyHigh};
CIDetector *objectDetector = [CIDetector detectorOfType:CIDetectorTypeRectangle context:nil options:objectOptions];
NSArray *objects = [objectDetector featuresInImage:ciImage];
for (CIRectangleFeature *objectFeature in objects) {
NSLog(@"Detected object at %@", NSStringFromCGRect(objectFeature.bounds));
}
}
Swift示例:
import UIKit
import CoreImage
class ImageAnalysis {
func analyzeImage(image: UIImage) {
if let ciImage = CIImage(image: image) {
let context = CIContext()
// 人脸检测
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: context, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
let faces = faceDetector?.features(in: ciImage)
for face in faces as! [CIFaceFeature] {
print("Face bounds: \(face.bounds)")
if face.hasLeftEyePosition {
print("Left eye position: \(face.leftEyePosition)")
}
if face.hasRightEyePosition {
print("Right eye position: \(face.rightEyePosition)")
}
}
// 特征识别
let featureDetector = CIDetector(ofType: CIDetectorTypeRectangle, context: context, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
let features = featureDetector?.features(in: ciImage)
for feature in features as! [CIRectangleFeature] {
print("Feature bounds: \(feature.bounds)")
}
// 颜色识别
let colorDetector = CIDetector(ofType: CIDetectorTypeColor, context: context, options: nil)
let colors = colorDetector?.features(in: ciImage)
for color in colors as! [CIColorFeature] {
print("Detected color: \(color.color)")
}
// 物体识别
let objectDetector = CIDetector(ofType: CIDetectorTypeQRCode, context: context, options: nil)
let objects = objectDetector?.features(in: ciImage)
for object in objects as! [CIQRCodeFeature] {
print("Detected QR code: \(object.messageString ?? "")")
}
}
}
}
// 使用示例
let image = UIImage(named: "sampleImage")
let imageAnalysis = ImageAnalysis()
imageAnalysis.analyzeImage(image: image)
3.1 Metal性能优化
Objective-C示例:
// Objective-C示例:
#import <Metal/Metal.h>
#import <MetalKit/MetalKit.h>
#import <CoreImage/CoreImage.h>
// 创建Metal设备和CIContext
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
CIContext *ciContext = [CIContext contextWithMTLDevice:device];
// 创建Metal纹理
MTKView *metalView = [[MTKView alloc] initWithFrame:CGRectZero device:device];
id<MTLTexture> metalTexture = metalView.currentDrawable.texture;
// 创建CIImage
CIImage *ciImage = [CIImage imageWithMTLTexture:metalTexture options:nil];
// 创建滤镜
CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
[filter setValue:ciImage forKey:kCIInputImageKey];
[filter setValue:@(1.2) forKey:kCIInputContrastKey];
// 渲染并显示结果
CIImage *outputImage = [filter outputImage];
[ciContext render:outputImage toMTLTexture:metalTexture commandBuffer:nil bounds:outputImage.extent colorSpace:outputImage.colorSpace];
Swift示例:
// Swift示例:
import Metal
import MetalKit
import CoreImage
// 创建Metal设备和CIContext
let device = MTLCreateSystemDefaultDevice()
let ciContext = CIContext(mtlDevice: device!)
// 创建Metal纹理
let metalView = MTKView(frame: CGRect.zero, device: device)
let metalTexture = metalView.currentDrawable?.texture
// 创建CIImage
let ciImage = CIImage(mtlTexture: metalTexture!, options: nil)
// 创建滤镜
let filter = CIFilter(name: "CIColorControls")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(1.2, forKey: kCIInputContrastKey)
// 渲染并显示结果
if let outputImage = filter?.outputImage {
ciContext.render(outputImage, to: metalTexture!, commandBuffer: nil, bounds: outputImage.extent, colorSpace: outputImage.colorSpace)
}
3.1 实时预览
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>
@interface ViewController : UIViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
@property (nonatomic, strong) CIContext *ciContext;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.captureSession = [[AVCaptureSession alloc] init];
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if (videoInput) {
[self.captureSession addInput:videoInput];
AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
[videoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
[self.captureSession addOutput:videoOutput];
self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
self.previewLayer.frame = self.view.bounds;
[self.view.layer addSublayer:self.previewLayer];
[self.captureSession startRunning];
self.ciContext = [CIContext contextWithOptions:nil];
} else {
NSLog(@"Error setting up video input: %@", error);
}
}
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
// 应用滤镜
CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
[filter setValue:ciImage forKey:kCIInputImageKey];
[filter setValue:@(1.2) forKey:kCIInputContrastKey];
CIImage *outputImage = [filter outputImage];
// 渲染到预览层
[self.ciContext render:outputImage toCVPixelBuffer:pixelBuffer];
}
@end
Swift示例:
// Swift示例:
import UIKit
import AVFoundation
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
var captureSession: AVCaptureSession!
var previewLayer: AVCaptureVideoPreviewLayer!
var ciContext: CIContext!
override func viewDidLoad() {
super.viewDidLoad()
captureSession = AVCaptureSession()
guard let videoDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoDevice) else {
return
}
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
captureSession.addOutput(videoOutput)
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.bounds
view.layer.addSublayer(previewLayer)
captureSession.startRunning()
ciContext = CIContext()
}
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
// 应用滤镜
let filter = CIFilter(name: "CIColorControls")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(1.2, forKey: kCIInputContrastKey)
if let outputImage = filter?.outputImage {
// 渲染到预览层
ciContext.render(outputImage, to: pixelBuffer)
}
}
}
4. 总结
Core Image
的图像分析功能比较鸡肋,且滤镜功能比较封闭。我们在日常开发中一般用GPUImage
较多.因此在这里只做基本了解.
相对 Core Image
有更深的认识和实践,推荐几篇文章:
二、GPUImage
1. 简介
GPUImage是一个基于OpenGL ES 2.0
的开源的图像处理库,作者是Brad Larson。GPUImage
将OpenGL ES
封装为简洁的Objective-C
或Swift
接口,可以用来给图像、实时相机视频、电影等添加滤镜。
目前GPUImage有三个版本:
- 1.0版本是OC + OPenGL ES
GPUImage1.0 - 2.0版本是Swift + OPenGL ES
GPUImage2.0 - 3.0版本是Swift + Metal
GPUImage3.0
本文 主要针对 GPUImage1
展开介绍。GPUImage2、GPUImage3与GPUImage1大同小异。
2. 与Core Image区别
对于图像的处理苹果官方提供了Core Image框架,那么GPUImage和Core Image有哪些区别呢?
2.1 GPUImage
- 最低支持
iOS 4.0
,iOS 5.0
之后就支持自定义滤镜 - 在低端机型上,
GPUImage
有更高的表现 -
GPUImage
在视频处理上有更好的表现. -
GPUImage
代码完全开源,实现透明. - 可以根据自己的业务需求,定制更加复杂的管线操作.可定制程度高.
-
GPUImage
当处理超过纹理限制的图像的时候,会先做判断,压缩成最大纹理限制的图像,导致图像质量损失
2.2 Core Image
-
官方框架
,使用放心,维护方便. - 支持
CPU
渲染,可以在后台继续处理和保存图片. - 支持使用
Metal
渲染图像,而Metal在iOS 平台上有更好的表现. - 一些滤镜的性能更强劲,例如由
Metal Performance Shaders
支持的模糊滤镜等 - 与
Metal
,SpriteKit
,Core Animation
等更完美的配合. - 支持
图像识别功能
.包括人脸识别
,条形码识别
,文本识别
等. - 支持自动增强图像效果,会分析图像的直方图,图像属性,脸部区域,然后通过一组滤镜来改善图像效果.
- 支持对原生
RAW
格式图片的处理. - 滤镜链的性能比
GPUImage
高. - 支持对大图进行处理,超过GPU纹理限制
4096 * 4096
的时候,会自动拆分成几个小块处理(Automatic tiling).-
GPUImage
对大图的处理方案则是压缩图片到最大纹理限制,会导致图像质量损失.
-
3. GPUImage特性
- 丰富的输入组件摄像头、图片、视频、OpenGL纹理、二进制数据、UIElement (UIView, CALayer)
- 大量现成的内置滤镜(4大类)
-
颜色类
(亮度、色度、饱和度、对比度、曲线、白平衡..) -
图像类
(仿射变换、裁剪、高斯模糊、毛玻璃效果..) -
颜色混合类
(差异混合、alpha混合、遮罩混合..) -
效果类
(像素化、素描效果、压花效果、球形玻璃效果..)
-
- 丰富的输出组件UIView 、视频文件、GPU纹理、二进制数据
- 灵活的滤镜链滤镜效果之间可以相互串联、并联,调用管理相当灵活。
- 接口易用,滤镜和OpenGL资源的创建及使用都做了统一的封装,简单易用,并且内置了一个cache模块实现了framebuffer 的复用。
- 线程管理OpenGLContext不是多线程安全的, GPUImage创建了专的contextQueue,所有的滤镜都会扔到统一的线程中处理。
- 轻松实现自定义滤镜效果继承GPUImageFilter自动获得上面全部特性,无需关注上下文的环境搭建,专注于效果的核心算法实现即可。
4. 使用流程|滤镜链介绍
在 GPUImage 中,对图像数据的处理都是通过建立滤镜链来实现的。
这里就涉及到了一个类 GPUImageOutput
和一个协议 GPUImageInput
。
- 对于继承了
GPUImageOutput
的类,可以理解为具备输出图像数据的能力; - 对于实现了
GPUImageInput
协议的类,可以理解为具备接收图像数据输入的能力。 - 顾名思义,滤镜链作为一个链路,具有起点、中间节点和终点。根据前面的描述,
-
滤镜链的起点
应该只继承了GPUImageOutput
类, -
滤镜链的终点
应该只实现了GPUImageInput
协议 - 而对于中间的结点应该同时继承了
GPUImageOutput
类并实现了GPUImageInput
协议,这样才具备承上启下的作用。
-
GPUImage的使用主要分为三部分:
- 滤镜链起点:
输入数据
- 滤镜:
处理数据
- 滤镜链终点:
输出数据
4.1 滤镜链起点
在 GPUImage 中,只继承了 GPUImageOutput
,而没有实现 GPUImageInput
协议的类有六个,也就是说有六种类型的输入源:
-
GPUImagePicture
:
用来处理静态图片。本质解压图片->纹理->用滤镜来进行处理.GPUImagePicture
通过图片来初始化,本质上是先将图片转化为CGImageRef
,然后将CGImageRef
转化为纹理。 -
GPUImageRawDataInput
:
二进制数据->纹理图片. CVPixelFormatGPUImageRawDataInput
通过二进制数据初始化,然后将二进制数据转化为纹理,在初始化的时候需要指明数据的格式(GPUPixelFormat
)。 -
GPUImageTextureInput
:
用纹理数据.GPUImageTextureInput
通过已经存在的纹理来初始化。既然纹理已经存在,在初始化的时候就不会重新去生成,只是将纹理的索引保存下来 -
GPUImageUIElement
:
UIView/CAL ayer ->图像数据->纹理.GPUImageUIElement
可以通过UIView
或者CALayer
来初始化,最后都是调用CALayer
的renderInContext:
方法,将当前显示的内容绘制到 CoreGraphics 的上下文中,从而获取图像数据。然后将数据转化为纹理。简单来说就是截屏,截取当前控件的内容。
这个类可以用来实现在视频上添加文字水印的功能。因为在 OpenGL 中不能直接进行文本的绘制,所以如果我们想把一个UILabel
的内容添加到滤镜链里面去,使用GPUImageUIElement
来实现是很合适的。 -
GPUImageMovie
:
视频文件-> AVAssetReader -> 逐帧读取视频->帧数据转化成纹理->滤镜处理.AVAssetReader0utput -> CMSamplerBufferRef -> CVImageBufferRef ->CVOpenGLESTextureRef -> Texture
GPUImageMovie
通过本地的视频来初始化。首先通过AVAssetReader
来逐帧读取视频,然后将帧数据转化为纹理,具体的流程大概是:AVAssetReaderOutput
->CMSampleBufferRef
->CVImageBufferRef
->CVOpenGLESTextureRef
->Texture
。 -
GPUImageVideoCamera
:
基于AVFundation -> didoutPutSampleBufferGPUImageVideoCamera
通过相机参数来初始化,通过屏幕比例和相机位置(前后置) 来初始化相机。
这里主要使用AVCaptureVideoDataOutput
来获取持续的视频流数据输出,在代理方法captureOutput:didOutputSampleBuffer:fromConnection:
中可以拿到CMSampleBufferRef
,将其转化为纹理的过程与GPUImageMovie
类似。 -
子类(
GPUImageStillCamera
)
我们在项目中使用的是它的子类GPUImageStillCamera
。GPUImageStillCamera
在原来的基础上多了一个AVCaptureStillImageOutput
,它是我们实现拍照功能的关键,在captureStillImageAsynchronouslyFromConnection:completionHandler:
方法的回调中,同样能拿到我们熟悉CMSampleBufferRef
简单来说,GPUImageVideoCamera
只能录制视频,GPUImageStillCamera
还可以拍照, 因此我们使用 GPUImageStillCamera
。
4.2 滤镜(GPUImageFilter
及其子类)
基类:GPUImageFilter
- 滤镜链的关键角色是
GPUImageFilter
,它同时继承了GPUImageOutput
类并实现了GPUImageInput
协议。GPUImageFilter
实现承上启下功能的基础是「渲染到纹理」 - 目前GPUImage大概有200多个滤镜,这里不一一赘述.
- 如果需要自定义滤镜,只需要一个继承于
GPUImageFilter
.并根据需求修改着色器程序
即可. - GPUImage库中提供的大部分滤镜都是通过
片元着色器
的一系列操作来实现相应的效果 - 大部分滤镜都是对图片中的像素进行计算
产生新的像素颜色处理
-
滤镜处理的原理:
将静态图片
或者视频的每一帧
进行图形变换后再显示到屏幕上,其本质就是像素点的位置和颜色的变化
- 每一个滤镜都能把输出的纹理可以作为下一个滤镜的输入,实现多层滤镜效果的叠加(多层滤镜处理)
4.3 滤镜终点
-
GPUImageMoviewWriter
: AVAssetWriter把每-帧纹理的数据从帧缓存区->指定文件. -
GPUImageRawData0utput
: 处理滤镜帧缓存区的二进制数据 GPUImageTextureOutput
GPUlmageView
在 GPUImage 中,实现了 GPUImageInput
协议,而没有继承 GPUImageOutput
的类有四个:
-
1、GPUImageMovieWriter
GPUImageMovieWriter
封装了AVAssetWriter
,可以逐帧从帧缓存的渲染结果中读取数据,最后通过AVAssetWriter
将视频文件保存到指定的路径。 -
2、GPUImageRawDataOutput
GPUImageRawDataOutput
通过rawBytesForImage
属性,可以获取到当前输入纹理的二进制数据。
假设我们的滤镜链在输入源和终点之间,连接了三个滤镜,而我们需要拿到第二个滤镜渲染后的数据,用来做人脸识别。那我们可以在第二个滤镜后面再添加一个GPUImageRawDataOutput
作为输出,则可以拿到对应的二进制数据,且不会影响原来的渲染流程。 -
3、GPUImageTextureOutput
这个类的实现十分简单,提供协议方法newFrameReadyFromTextureOutput:
,在每一帧渲染结束后,将自身返回,通过texture
属性就可以拿到输入纹理的索引。 -
4、GPUImageView
GPUImageView
继承自UIView
,通过输入的纹理,执行一遍渲染流程。这次的渲染目标不是新的纹理,而是自身的layer
。
这个类是我们实现相机功能的重要组成部分,我们所有的滤镜效果,都要依靠它来呈现。
5. 框架解析
-
GPUImage
框架采用的链式( Chain )结构.- 主要有一个
GPUImageoutput interface
和GPUImageInput protocol
串联起来.
- 主要有一个
-
GPUImageOutput
负责输出
纹理Texture ; -
GPUImageInput
负责输入
纹理Texture ;
整个链式图像数据过程,纹理作为核心载体.
- 当然
GPUImage
不仅仅适用于静态图片,还适用视频、实时拍摄等,这些不同的载体都是继承于GPUImageOutput
类。 - 基本上每一个滤镜都是继承自
GPUImageFilter
,而GPUImageFilter
是整套框架的核心-
GPUImageFilter
接收一个GPUImageFrameBuffer
输入,调用GLProgram
渲染处理之后,输出一个GPUImageFrameBuffer
- 再把输出的
GPUImageFrameBuffer
传给通过targets
属性关联的下一级滤镜,直到传递给最终的输出组件。
-
6. 源码结构分析
7. 内置滤镜介绍
目前GPUImage内置了125滤镜,分为以下几类:
7.1 颜色调整
-
GPUImageBrightnessFilter : 调整图像的亮度
- brightness:调整后的亮度(-1.0 - 1.0,默认为 0.0)
-
GPUImageExposureFilter : 调整图像的曝光
- exposure:调整后的曝光(-10.0 - 10.0,默认为 0.0)
-
GPUImageContrastFilter : 调整图像的对比度
- contrast:调整后的对比度(0.0 - 4.0,默认为 1.0)
-
GPUImageSaturationFilter:调整图像的饱和度
- 饱和度:应用于图像的饱和度或去饱和度(0.0 - 2.0,默认值为 1.0)
-
GPUImageGammaFilter:调整图像的伽玛
- gamma:要应用的 gamma 调整(0.0 - 3.0,默认值为 1.0)
-
GPUImageLevelsFilter:类似 Photoshop 的级别调整。min、max、minOut 和 maxOut 参数是 [0, 1] 范围内的浮点数。如果 Photoshop 的参数在 [0, 255] 范围内,则必须先将它们转换为 [0, 1]。gamma/mid 参数是一个 >= 0 的浮点数。这与来自 Photoshop 的值相匹配。如果您想将级别应用到 RGB 以及单个通道,您需要使用此过滤器两次 - 首先是单个通道,然后是所有通道。
-
GPUImageColorMatrixFilter:通过对图像应用矩阵来转换图像的颜色
- colorMatrix:一个 4x4 矩阵,用于转换图像中的每种颜色 - intensity:新变换的颜色替换每个像素的原始颜色的程度
-
GPUImageRGBFilter:调整图像的各个 RGB 通道
- red:每个颜色通道乘以的归一化值。范围从 0.0 开始,默认值为 1.0。 - 绿色: - 蓝色:
-
GPUImageHueFilter:调整图像的色调
- 色调:色调角度,以度为单位。默认90度
-
GPUImageVibranceFilter:调整图像的振动
- vibrance:要应用的 vibrance 调整,使用 0.0 作为默认值,建议的最小值/最大值分别约为 -1.2 和 1.2。
-
GPUImageWhiteBalanceFilter:调整图像的白平衡。
- temperature:调整图像的温度,以ºK为单位。值 4000 非常凉爽,7000 非常温暖。默认值为 5000。请注意,4000 到 5000 之间的比例在视觉上几乎与 5000 到 7000 之间的比例一样重要。 - tint:调整图像的色调。值 -200非常绿,200非常粉红色。默认值为 0。
-
GPUImageToneCurveFilter:根据每个颜色通道的样条曲线调整图像的颜色。
- 红色控制点: - 绿色控制点: - 蓝色控制点: - rgbCompositeControlPoints:色调曲线采用一系列控制点,这些控制点为每个颜色分量或合成中的所有三个分量定义样条曲线。它们在 NSArray 中存储为 NSValue-wrapped CGPoints,具有从 0 - 1 的标准化 X 和 Y 坐标。默认值为 (0,0)、(0.5,0.5)、(1,1)。
-
GPUImageHighlightShadowFilter:调整图像的阴影和高光
- shadows:增加以减轻阴影,从 0.0 到 1.0,默认值为 0.0。 - highlights:从 1.0 到 0.0 减少以加深高光,默认值为 1.0。
-
GPUImageHighlightShadowTintFilter:允许您使用颜色和强度独立地为图像的阴影和高光着色
-
shadowTintColor:阴影色调 RGB 颜色 (GPUVector4)。默认值:(
{1.0f, 0.0f, 0.0f, 1.0f}
红色)。 - highlightTintColor:突出显示色调 RGB 颜色 (GPUVector4)。默认值:({0.0f, 0.0f, 1.0f, 1.0f}
蓝色)。 - shadowTintIntensity:阴影色调强度,从 0.0 到 1.0。默认值:0.0 - highlightTintIntensity:突出显示色调强度,从 0.0 到 1.0,默认值为 0.0。
-
shadowTintColor:阴影色调 RGB 颜色 (GPUVector4)。默认值:(
-
GPUImageLookupFilter:使用 RGB 颜色查找图像重新映射图像中的颜色。首先,使用您最喜欢的照片编辑应用程序对来自 GPUImage/framework/Resources 的 lookup.png 应用过滤器。为了使其正常工作,每个像素颜色不得依赖于其他像素(例如,模糊将不起作用)。如果您需要更复杂的过滤器,您可以根据需要创建尽可能多的查找表。准备就绪后,使用新的 lookup.png 文件作为 GPUImageLookupFilter 的第二个输入。
-
GPUImageAmatorkaFilter:基于 Amatorka 的 Photoshop 动作的照片滤镜:http: //amatorka.deviantart.com/art/Amatorka-Action-2-121069631。如果您想使用此效果,您必须将 GPUImage Resources 文件夹中的 lookup_amatorka.png 添加到您的应用程序包中。
-
GPUImageMissEtikateFilter:基于 Miss Etikate 的 Photoshop 动作的照片滤镜:http: //miss-etikate.deviantart.com/art/Photoshop-Action-15-120151961。如果您想使用此效果,您必须将 GPUImage Resources 文件夹中的 lookup_miss_etikate.png 添加到您的应用程序包中。
-
GPUImageSoftEleganceFilter:另一个基于查找的颜色重映射过滤器。如果您想使用此效果,您必须将 GPUImage Resources 文件夹中的 lookup_soft_elegance_1.png 和 lookup_soft_elegance_2.png 添加到您的应用程序包中。
-
GPUImageSkinToneFilter:一种肤色调整滤镜,可影响独特的浅肤色范围,并相应地调整粉红色/绿色或粉红色/橙色范围。默认值针对白种人皮肤,但可以根据需要进行调整。
-
skinToneAdjust:调整肤色的量。默认值:0.0,建议的最小值/最大值:分别为 -0.3 和 0.3。 - skinHue:要检测的肤色。默认值:0.05(白种人至微红皮肤)。 - skinHueThreshold:肤色的变化量。默认值:40.0。 - maxHueShift:允许的最大色调偏移量。默认值:0.25。 - maxSaturationShift = 要移动的最大饱和度(使用橙色时)。默认值:0.4。 - upperSkinToneColor =
GPUImageSkinToneUpperColorGreen
或GPUImageSkinToneUpperColorOrange
-
skinToneAdjust:调整肤色的量。默认值:0.0,建议的最小值/最大值:分别为 -0.3 和 0.3。 - skinHue:要检测的肤色。默认值:0.05(白种人至微红皮肤)。 - skinHueThreshold:肤色的变化量。默认值:40.0。 - maxHueShift:允许的最大色调偏移量。默认值:0.25。 - maxSaturationShift = 要移动的最大饱和度(使用橙色时)。默认值:0.4。 - upperSkinToneColor =
-
GPUImageColorInvertFilter:反转图像的颜色
-
GPUImageGrayscaleFilter:将图像转换为灰度(饱和度过滤器的实现速度稍快,但无法改变颜色贡献)
-
GPUImageMonochromeFilter:根据每个像素的亮度将图像转换为单色版本
- intensity : 特定颜色替代正常图像颜色的程度(0.0 - 1.0,默认为 1.0) - color:用作效果基础的颜色,默认值为 (0.6, 0.45, 0.3, 1.0)。
-
GPUImageFalseColorFilter:使用图像的亮度在两种用户指定的颜色之间进行混合
- firstColor:第一种和第二种颜色分别指定用什么颜色替换图像的暗区和亮区。默认值是 (0.0, 0.0, 0.5) 和 (1.0, 0.0, 0.0)。 - 第二颜色:
-
GPUImageHazeFilter:用于添加或去除雾度(类似于 UV 过滤器)
- distance : 应用颜色的强度。默认值 0。最佳值介于 -.3 和 .3 之间。 - 斜率:颜色变化量。默认值 0。最佳值介于 -.3 和 .3 之间。
-
GPUImageSepiaFilter : 简单的棕褐色调滤镜
- intensity:棕褐色调替换正常图像颜色的程度(0.0 - 1.0,默认值为 1.0)
-
GPUImageOpacityFilter:调整传入图像的 alpha 通道
- 不透明度:将每个像素的传入 alpha 通道乘以的值(0.0 - 1.0,默认值为 1.0)
-
GPUImageSolidColorGenerator:这会输出具有纯色的生成图像。您需要使用 -forceProcessingAtSize 定义图像大小:
- color:用于填充图像的颜色,采用四分量格式。
-
GPUImageLuminanceThresholdFilter:亮度高于阈值的像素将显示为白色,低于阈值的像素将显示为黑色
- threshold : 亮度阈值,从0.0到1.0,默认为0.5
-
GPUImageAdaptiveThresholdFilter:确定像素周围的局部亮度,如果低于该局部亮度则将像素变为黑色,如果高于该亮度则将像素变为白色。这对于在不同光照条件下挑选文本很有用。
- blurRadiusInPixels:背景平均模糊半径的乘数(以像素为单位),默认值为 4。
-
GPUImageAverageLuminanceThresholdFilter:这应用了阈值操作,其中阈值根据场景的平均亮度不断调整。
- thresholdMultiplier:这是一个因子,平均亮度将乘以该因子以达到要使用的最终阈值。默认情况下,这是 1.0。
-
GPUImageHistogramFilter:\ This analyzes the incoming image and creates an output histogram with the frequency at which each color value occurs. The output of this filter is a 3-pixel-high, 256-pixel-wide image with the center (vertical) pixels containing pixels that correspond to the frequency at which various color values occurred. Each color value occupies one of the 256 width positions, from 0 on the left to 255 on the right. This histogram can be generated for individual color channels (kGPUImageHistogramRed, kGPUImageHistogramGreen, kGPUImageHistogramBlue), the luminance of the image (kGPUImageHistogramLuminance), or for all three color channels at once (kGPUImageHistogramRGB).
- downsamplingFactor:这不是对每个像素进行采样,而是指示对图像的哪一部分进行采样。默认情况下,这是 16,最小值为 1。这是防止直方图饱和所必需的,直方图在过载之前只能为每个颜色值记录 256 个像素。
-
GPUImageHistogramGenerator:这是一个特殊的过滤器,因为它主要用于与 GPUImageHistogramFilter 一起使用。它生成由 GPUImageHistogramFilter 生成的颜色直方图的输出表示,但它可以重新用于显示其他类型的值。它接收图像并查看中心(垂直)像素。然后它在输出纹理的单独彩色图表中绘制 RGB 分量的数值。您可能需要为此过滤器强制调整大小以使其输出可见。
-
GPUImageAverageColor:这会处理输入图像并通过平均图像中每个像素的 RGBA 分量来确定场景的平均颜色。缩减过程用于在 GPU 上逐步对源图像进行下采样,然后在 CPU 上进行短暂的平均计算。此过滤器的输出没有意义,但您需要将 colorAverageProcessingFinishedBlock 属性设置为一个块,该块接收四个颜色分量和一个帧时间并对它们执行某些操作。
-
GPUImageLuminosity:与 GPUImageAverageColor 一样,这会将图像降低到其平均亮度。您需要设置 luminosityProcessingFinishedBlock 来处理这个过滤器的输出,它只返回一个亮度值和一个帧时间。
-
GPUImageChromaKeyFilter:对于图像中的给定颜色,将 alpha 通道设置为 0。这类似于 GPUImageChromaKeyBlendFilter,只是不是混合第二张图像以获得匹配的颜色,它不会接收第二张图像,只是将给定的颜色透明。
- thresholdSensitivity:颜色匹配需要与要替换的目标颜色有多接近(默认为 0.4) - smoothing:颜色匹配的平滑度(默认为 0.1)
7.2 图像处理
-
GPUImageTransformFilter:这对图像应用任意 2-D 或 3-D 转换
- affineTransform:这需要一个 CGAffineTransform 来调整二维图像 - transform3D:这需要一个 CATransform3D 来操作 3-D 图像 - ignoreAspectRatio:默认情况下,变换图像的纵横比保持不变,但可以将其设置为 YES 以使变换与纵横比无关
-
GPUImageCropFilter:将图像裁剪到特定区域,然后仅将该区域传递到过滤器的下一个阶段
- cropRegion:要从图像中裁剪的矩形区域,标准化为 0.0 - 1.0 之间的坐标。(0.0, 0.0) 位置在图像的左上角。
-
GPUImageLanczosResamplingFilter:这使您可以使用 Lanczos 重采样对图像进行上采样或下采样,从而产生比标准线性或三线性插值明显更好的质量。只需使用 -forceProcessingAtSize: 设置过滤器的目标输出分辨率,图像将根据新尺寸重新采样。
-
GPUImageSharpenFilter:锐化图像
- 清晰度:要应用的清晰度调整(-4.0 - 4.0,默认值为 0.0)
-
GPUImageUnsharpMaskFilter:应用反锐化蒙版
- blurRadiusInPixels:底层高斯模糊的模糊半径。默认值为 4.0。 - intensity : 锐化的强度,从 0.0 开始,默认为 1.0
-
GPUImageGaussianBlurFilter:硬件优化的可变半径高斯模糊
- texelSpacingMultiplier:纹素之间间距的乘数,范围从 0.0 开始,默认值为 1.0。调整此项可能会略微增加模糊强度,但会在结果中引入伪影。强烈建议先使用其他参数,然后再接触这个参数。 - blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 2.0。这会调整高斯分布函数中的 sigma 变量。 - 模糊半径作为图像宽度的分数: - blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像大小缩放 - blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器越慢。
-
GPUImageBoxBlurFilter:硬件优化的可变半径框模糊
- texelSpacingMultiplier:纹素之间间距的乘数,范围从 0.0 开始,默认值为 1.0。调整此项可能会略微增加模糊强度,但会在结果中引入伪影。强烈建议先使用其他参数,然后再接触这个参数。 - blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 2.0。这会调整高斯分布函数中的 sigma 变量。 - 模糊半径作为图像宽度的分数: - blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像大小缩放 - blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器越慢。
-
GPUImageSingleComponentGaussianBlurFilter:对 GPUImageGaussianBlurFilter 的修改,仅对红色分量进行操作
- texelSpacingMultiplier:纹素之间间距的乘数,范围从 0.0 开始,默认值为 1.0。调整此项可能会略微增加模糊强度,但会在结果中引入伪影。强烈建议先使用其他参数,然后再接触这个参数。 - blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 2.0。这会调整高斯分布函数中的 sigma 变量。 - 模糊半径作为图像宽度的分数: - blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像大小缩放 - blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器越慢。
-
GPUImageGaussianSelectiveBlurFilter:将焦点保留在圆形区域内的高斯模糊
- blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 5.0。这会调整高斯分布函数中的 sigma 变量。 - excludeCircleRadius:从模糊中排除的圆形区域的半径 - excludeCirclePoint:从模糊中排除的圆形区域的中心 - excludeBlurSize : 模糊部分和清晰圆圈之间的区域大小 - aspectRatio:图像的纵横比,用于调整对焦区域的圆度。默认情况下,这与图像纵横比匹配,但您可以覆盖该值。
-
GPUImageGaussianBlurPositionFilter : GPUImageGaussianSelectiveBlurFilter 的逆函数,仅在特定圆圈内应用模糊
- blurSize:模糊大小的乘数,范围从 0.0 到 up,默认值为 1.0 - blurCenter : 模糊中心,默认为 0.5, 0.5 - blurRadius : 模糊的半径,默认为 1.0
-
GPUImageiOSBlurFilter:尝试在控制中心等位置复制 iOS 7 上使用的背景模糊。
- blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 12.0。这会调整高斯分布函数中的 sigma 变量。 - 饱和度:饱和度范围从 0.0(完全去饱和)到 2.0(最大饱和度),0.8 为正常水平 - 下采样:下采样的程度,然后对输入图像进行上采样以最小化高斯模糊内的计算,默认值为 4.0。
-
GPUImageMedianFilter:在 3x3 区域上取三个颜色分量的中值
-
GPUImageBilateralFilter:双边模糊,它试图模糊相似的颜色值,同时保留锐利的边缘
- texelSpacingMultiplier : 纹素读取间距的倍数,范围从 0.0 到上,默认为 4.0 - distanceNormalizationFactor:中心颜色和样本颜色之间距离的归一化因子,默认值为 8.0。
-
GPUImageTiltShiftFilter:模拟倾斜移位镜头效果
- blurRadiusInPixels:底层模糊的半径,以像素为单位。默认情况下为 7.0。 - topFocusLevel : 对焦区域顶部在图像中的归一化位置,该值应低于 bottomFocusLevel,默认 0.4 - bottomFocusLevel : 对焦区域底部在图像中的归一化位置,该值应高于 topFocusLevel,默认 0.6 - focusFallOffRate:图像远离对焦区域变得模糊的速率,默认 0.2
-
GPUImage3x3ConvolutionFilter:对图像运行 3x3 卷积核
- convolutionKernel:卷积核是一个 3x3 值矩阵,应用于像素及其周围的 8 个像素。矩阵按行优先顺序指定,左上角的像素为 one.one,右下角的像素为 three.three。如果矩阵中的值加起来不等于 1.0,图像可能会变亮或变暗。
-
GPUImageSobelEdgeDetectionFilter:索贝尔边缘检测,边缘以白色突出显示
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。
-
GPUImagePrewittEdgeDetectionFilter : Prewitt 边缘检测,边缘以白色突出显示
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。
-
GPUImageThresholdEdgeDetectionFilter:执行 Sobel 边缘检测,但应用阈值而不是提供渐变强度值
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。 - threshold:任何高于此阈值的边缘都将是黑色的,任何低于该阈值的边缘都是白色的。范围从 0.0 到 1.0,默认值为 0.8
-
GPUImageCannyEdgeDetectionFilter:这使用完整的 Canny 过程来突出显示一个像素宽的边缘
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - blurRadiusInPixels:高斯模糊的基础模糊半径。默认值为 2.0。 - blurTexelSpacingMultiplier:底层模糊纹素间距乘数。默认值为 1.0。 - upperThreshold:任何梯度幅度高于此阈值的边缘都将通过并显示在最终结果中。默认值为 0.4。 - lowerThreshold:梯度幅值低于此阈值的任何边缘都将失败并从最终结果中删除。默认值为 0.1。
-
GPUImageHarrisCornerDetectionFilter:在输入图像上运行 Harris 角点检测算法,并生成一个图像,其中这些角点为白色像素,其他所有像素为黑色。可以设置 cornersDetectedBlock,您将在该回调中获得一个角列表(在标准化的 0..1 X,Y 坐标中),用于您想要执行的任何其他操作。
- blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - sensitivity:一个内部比例因子,用于调整过滤器中生成的转角图的动态范围。默认值为 5.0。 - threshold:点被检测为角点的阈值。这可能会根据尺寸、照明条件和 iOS 设备相机类型而有很大差异,因此可能需要进行一些试验才能适合您的情况。默认值为 0.20。
-
GPUImageNobleCornerDetectionFilter:在 Harris 角检测器上运行 Noble 变体。它的行为与上述 Harris 检测器相同。
- blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - sensitivity:一个内部比例因子,用于调整过滤器中生成的转角图的动态范围。默认值为 5.0。 - threshold:点被检测为角点的阈值。这可能会根据尺寸、照明条件和 iOS 设备相机类型而有很大差异,因此可能需要进行一些试验才能适合您的情况。默认值为 0.2。
-
GPUImageShiTomasiCornerDetectionFilter:运行 Shi-Tomasi 特征检测器。它的行为与上述 Harris 检测器相同。
- blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - sensitivity:一个内部比例因子,用于调整过滤器中生成的转角图的动态范围。默认值为 1.5。 - threshold:点被检测为角点的阈值。这可能会根据尺寸、照明条件和 iOS 设备相机类型而有很大差异,因此可能需要进行一些试验才能适合您的情况。默认值为 0.2。
-
GPUImageNonMaximumSuppressionFilter:目前仅用作 Harris 角点检测过滤器的一部分,这将对每个像素周围的 1 像素框进行采样,并确定中心像素的红色通道是否是该区域的最大值。如果是,它会留下来。如果不是,则将所有颜色分量设置为 0。
-
GPUImageXYDerivativeFilter:Harris 角点检测过滤器中的一个内部组件,计算该像素左右像素的平方差、该像素上下像素的平方差,以及这两个差的乘积。
-
GPUImageCrosshairGenerator:这会在图像上绘制一系列十字准线,最常用于识别机器视觉特征。它不像其他滤镜那样接收标准图像,而是接收其 -renderCrosshairsFromArray:count: 方法中的一系列点,该方法执行实际绘图。您将需要强制此过滤器以您需要的特定输出大小呈现。
- crosshairWidth:要在屏幕上绘制的十字准线的宽度(以像素为单位)。
-
GPUImageDilationFilter:这执行图像膨胀操作,其中矩形邻域中红色通道的最大强度用于该像素的强度。要采样的矩形区域的半径在初始化时指定,范围为 1-4 像素。这是为了与灰度图像一起使用,它扩展了明亮的区域。
-
GPUImageRGBDilationFilter:这与 GPUImageDilationFilter 相同,只是它作用于所有颜色通道,而不仅仅是红色通道。
-
GPUImageErosionFilter:这执行图像侵蚀操作,其中矩形邻域中红色通道的最小强度用于该像素的强度。要采样的矩形区域的半径在初始化时指定,范围为 1-4 像素。这是为了与灰度图像一起使用,它扩展了黑暗区域。
-
GPUImageRGBErosionFilter:这与 GPUImageErosionFilter 相同,只是它作用于所有颜色通道,而不仅仅是红色通道。
-
GPUImageOpeningFilter:这对图像的红色通道执行腐蚀,然后是相同半径的膨胀。半径在初始化时设置,范围为 1-4 像素。这会过滤掉较小的明亮区域。
-
GPUImageRGBOpeningFilter:这与 GPUImageOpeningFilter 相同,除了它作用于所有颜色通道,而不仅仅是红色通道。
-
GPUImageClosingFilter:这对图像的红色通道执行膨胀,然后是相同半径的腐蚀。半径在初始化时设置,范围为 1-4 像素。这会过滤掉较小的暗区。
-
GPUImageRGBClosingFilter:这与 GPUImageClosingFilter 相同,只是它作用于所有颜色通道,而不仅仅是红色通道。
-
GPUImageLocalBinaryPatternFilter:这对周围 8 个像素的红色通道与中心像素的红色通道的强度进行比较,将比较结果编码为成为该像素强度的位串。最不重要的位是右上角的比较,逆时针旋转到最右边的比较结束。
-
GPUImageLowPassFilter:这对传入的视频帧应用低通滤波器。这基本上是累积先前帧与当前帧的加权滚动平均值。这可用于视频降噪、添加运动模糊或用于创建高通滤波器。
- filterStrength:这控制了以前累积的帧与当前帧混合的程度。范围从 0.0 到 1.0,默认值为 0.5。
-
GPUImageHighPassFilter:这对传入的视频帧应用高通滤波器。这是低通滤波器的逆过程,显示当前帧与之前帧的加权滚动平均值之间的差异。这对于运动检测最有用。
- filterStrength:这控制了先前累积帧混合的程度,然后从当前帧中减去。范围从 0.0 到 1.0,默认值为 0.5。
-
GPUImageMotionDetector:这是一个基于高通滤波器的运动检测器。您设置 motionDetectionBlock 并在每个传入帧上为您提供场景中任何检测到的运动的质心(在标准化的 X、Y 坐标中)以及场景的运动强度。
- lowPassFilterStrength:它控制在幕后使用的低通滤波器的强度,以建立与传入帧进行比较的基线。范围从 0.0 到 1.0,默认值为 0.5。
-
GPUImageHoughTransformLineDetector:使用 Hough 变换到平行坐标空间来检测图像中的线。这种方法完全基于 PC 线过程,该过程由布尔诺科技大学的 Graph@FIT 研究小组开发,并在他们的出版物中进行了描述:M. Dubská、J. Havel 和 A. Herout。使用平行坐标和 OpenGL 实时检测线。SCCG 2011 会议记录,布拉迪斯拉发,SK,p. 7 ( medusa.fit.vutbr.cz/public/data… ) 和 M. Dubská, J . 哈维尔和A.赫鲁特。PClines - 使用平行坐标的线检测。2011 年 IEEE 计算机视觉和模式识别会议 (CVPR),p. 1489- 1494 (medusa.fit.vutbr.cz/public/data…).
- edgeThreshold:一个阈值,对于该阈值,点被检测为属于用于确定线的边缘。默认值为 0.9。 - lineDetectionThreshold:检测到局部最大值属于平行坐标空间中的一条线的阈值。默认值为 0.20。 - linesDetectedBlock:此块在检测线时调用,通常在每个处理过的帧上调用。包含 m、b 对 (y=mx+b) 中的归一化斜率和截距的 AC 数组被传入,以及检测到的行数计数和视频帧的当前时间戳。
-
GPUImageLineGenerator:生成可以覆盖场景的线条的辅助类。可以使用 -setLineColorRed:green:blue 调整这些线条的颜色:
- lineWidth:线条的宽度,以像素为单位,默认为 1.0。
-
GPUImageMotionBlurFilter:对图像应用定向运动模糊
- blurSize:模糊大小的乘数,范围从 0.0 到 up,默认值为 1.0 - blurAngle:模糊的角度方向,以度为单位。默认为 0 度。
-
GPUImageZoomBlurFilter:对图像应用定向运动模糊
- blurSize:模糊大小的乘数,范围从 0.0 到 up,默认值为 1.0 - blurCenter:模糊的归一化中心。(0.5, 0.5) 默认
7.3 混合模式
-
GPUImageChromaKeyBlendFilter:有选择地用第二张图像替换第一张图像中的颜色
- thresholdSensitivity:颜色匹配需要与要替换的目标颜色有多接近(默认为 0.4) - smoothing:颜色匹配的平滑度(默认为 0.1)
-
GPUImageDissolveBlendFilter:应用两个图像的溶解混合
- mix:第二个图像覆盖第一个图像的程度(0.0 - 1.0,默认为 0.5)
-
GPUImageMultiplyBlendFilter:应用两个图像的乘法混合
-
GPUImageAddBlendFilter:应用两个图像的加法混合
-
GPUImageSubtractBlendFilter:应用两个图像的减法混合
-
GPUImageDivideBlendFilter:应用两个图像的分割混合
-
GPUImageOverlayBlendFilter:应用两个图像的叠加混合
-
GPUImageDarkenBlendFilter:通过取图像之间每个颜色分量的最小值来混合两个图像
-
GPUImageLightenBlendFilter:通过取图像之间每个颜色分量的最大值来混合两个图像
-
GPUImageColorBurnBlendFilter:应用两个图像的颜色加深混合
-
GPUImageColorDodgeBlendFilter:应用两个图像的颜色减淡混合
-
GPUImageScreenBlendFilter:应用两个图像的屏幕混合
-
GPUImageExclusionBlendFilter:应用两个图像的排除混合
-
GPUImageDifferenceBlendFilter:应用两个图像的差异混合
-
GPUImageHardLightBlendFilter:应用两个图像的强光混合
-
GPUImageSoftLightBlendFilter:应用两个图像的柔光混合
-
GPUImageAlphaBlendFilter:根据第二个的 alpha 通道将第二个图像混合到第一个图像上
- mix:第二个图像覆盖第一个图像的程度(0.0 - 1.0,默认值为 1.0)
-
GPUImageSourceOverBlendFilter:在两个图像的混合上应用源
-
GPUImageColorBurnBlendFilter:应用两个图像的颜色加深混合
-
GPUImageColorDodgeBlendFilter:应用两个图像的颜色减淡混合
-
GPUImageNormalBlendFilter:应用两个图像的正常混合
-
GPUImageColorBlendFilter:应用两个图像的颜色混合
-
GPUImageHueBlendFilter:应用两个图像的色调混合
-
GPUImageSaturationBlendFilter:应用两个图像的饱和度混合
-
GPUImageLuminosityBlendFilter:应用两个图像的光度混合
-
GPUImageLinearBurnBlendFilter:应用两个图像的线性加深混合
-
GPUImagePoissonBlendFilter:应用两个图像的泊松混合
- mix:混合范围从 0.0(仅图像 1)到 1.0(仅图像 2 渐变),1.0 为正常级别 - numIterations:传播梯度的次数。如果您想接近收敛,可以将其调到 100 甚至 1000。是的,这会很慢。
-
GPUImageMaskFilter:使用另一个图像遮盖一个图像
7.4 视觉效果
-
GPUImagePixellateFilter:对图像或视频应用像素化效果
- fractionalWidthOfAPixel:像素有多大,作为图像宽度和高度的分数(0.0 - 1.0,默认 0.05)
-
GPUImagePolarPixellateFilter:基于极坐标而不是笛卡尔坐标对图像或视频应用像素化效果
- center : 应用像素化的中心,默认为 (0.5, 0.5) - pixelSize:分数像素大小,分为宽度和高度分量。默认值为 (0.05, 0.05)
-
GPUImagePolkaDotFilter:将图像分解为规则网格内的彩色点
- fractionalWidthOfAPixel:点有多大,作为图像宽度和高度的一部分(0.0 - 1.0,默认 0.05) - dotScaling:每个网格空间被点占用的比例,从 0.0 到 1.0,默认值为 0.9。
-
GPUImageHalftoneFilter:对图像应用半色调效果,如新闻印刷
- fractionalWidthOfAPixel:半色调点有多大,作为图像宽度和高度的分数(0.0 - 1.0,默认 0.05)
-
GPUImageCrosshatchFilter:这会将图像转换为黑白交叉影线图案
- crossHatchSpacing:用作剖面线间距的图像的小数宽度。默认值为 0.03。 - lineWidth:交叉影线的相对宽度。默认值为 0.003。
-
GPUImageSketchFilter:将视频转换为草图。这只是颜色反转的 Sobel 边缘检测滤波器
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。
-
GPUImageThresholdSketchFilter : 和素描滤镜一样,只是对边缘进行阈值处理,而不是灰度化
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。 - threshold:任何高于此阈值的边缘都将是黑色的,任何低于该阈值的边缘都是白色的。范围从 0.0 到 1.0,默认值为 0.8
-
GPUImageToonFilter:这使用 Sobel 边缘检测在对象周围放置黑色边框,然后它量化图像中存在的颜色以赋予图像类似卡通的质量。
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - threshold:边缘检测的灵敏度,值越小越灵敏。范围从 0.0 到 1.0,默认值为 0.2 - quantizationLevels:最终图像中要表示的颜色级别数。默认值为 10.0
-
GPUImageSmoothToonFilter:这使用与 GPUImageToonFilter 类似的过程,只是它在卡通效果之前使用高斯模糊来平滑噪声。
- 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - threshold:边缘检测的灵敏度,值越小越灵敏。范围从 0.0 到 1.0,默认值为 0.2 - quantizationLevels:最终图像中要表示的颜色级别数。默认值为 10.0
-
GPUImageEmbossFilter:对图像应用浮雕效果
- intensity : 压花的强度,从 0.0 到 4.0,1.0 为正常水平
-
GPUImagePosterizeFilter:这会将颜色动态范围减少到指定的步数,从而使图像具有卡通般的简单阴影。
- colorLevels:将图像空间缩小到的颜色级别数。范围从 1 到 256,默认值为 10。
-
GPUImageSwirlFilter:在图像上创建漩涡失真
- radius:从中心开始应用扭曲的半径,默认值为 0.5 - center:图像的中心(在 0 - 1.0 的标准化坐标中)围绕其旋转,默认值为 (0.5, 0.5) - angle:应用于图像的扭曲量,默认值为 1.0
-
GPUImageBulgeDistortionFilter:在图像上创建凸起失真
- radius:从中心开始应用扭曲的半径,默认值为 0.25 - center:图像的中心(在 0 - 1.0 的规范化坐标中)围绕其扭曲,默认值为 (0.5, 0.5) - scale:要应用的失真量,从 -1.0 到 1.0,默认值为 0.5
-
GPUImagePinchDistortionFilter:创建图像的收缩失真
- radius:从中心开始应用扭曲的半径,默认值为 1.0 - center:图像的中心(在 0 - 1.0 的规范化坐标中)围绕其扭曲,默认值为 (0.5, 0.5) - scale:要应用的失真量,从 -2.0 到 2.0,默认值为 1.0
-
GPUImageStretchDistortionFilter:创建图像的拉伸失真
- center:图像的中心(在 0 - 1.0 的规范化坐标中)围绕其扭曲,默认值为 (0.5, 0.5)
-
GPUImageSphereRefractionFilter:模拟通过玻璃球体的折射
- center:应用扭曲的中心,默认值为 (0.5, 0.5) - radius:扭曲的半径,范围从0.0到1.0,默认为0.25 - refractiveIndex:球体的折射率,默认值为 0.71
-
GPUImageGlassSphereFilter : 与 GPUImageSphereRefractionFilter 相同,只是图像没有反转,玻璃边缘有一点点磨砂
- center:应用扭曲的中心,默认值为 (0.5, 0.5) - radius:扭曲的半径,范围从0.0到1.0,默认为0.25 - refractiveIndex:球体的折射率,默认值为 0.71
-
GPUImageVignetteFilter:执行渐晕效果,在边缘淡出图像
- vignetteCenter : tex 坐标 (CGPoint) 中小插图的中心,默认值为 0.5, 0.5 - vignetteColor:用于小插图的颜色(GPUVector3),默认为黑色 - vignetteStart:距离晕影效果开始的中心的归一化距离,默认值为 0.5 - vignetteEnd:与晕影效果结束的中心的归一化距离,默认值为 0.75
-
GPUImageKuwaharaFilter:Kuwahara 图像抽象,取自 Kyprianidis 等人的作品。阿尔。在 GPU Pro 系列中的出版物“GPU 上的各向异性 Kuwahara 过滤”中。这会生成类似油画的图像,但计算量极大,因此在 iPad 2 上渲染帧可能需要几秒钟。这可能最适合静态图像。
- radius:在整数中,指定在应用过滤器时要测试的中心像素的像素数,默认值为 4。更高的值创建更抽象的图像,但以更长的处理时间为代价。
-
GPUImageKuwaharaRadius3Filter:Kuwahara 过滤器的修改版本,经过优化以仅在三个像素的半径范围内工作
-
GPUImagePerlinNoiseFilter:生成充满 Perlin 噪声的图像
- 颜色开始: - colorFinish:正在生成的噪声的颜色范围 - scale:生成的噪声的缩放比例
-
GPUImageCGAColorspaceFilter : 模拟 CGA 显示器的色彩空间
-
GPUImageMosaicFilter:此过滤器采用输入图块集,图块的亮度必须上升。它会查看输入图像,并根据输入图块的亮度用输入图块替换每个显示图块。这个想法是复制在其他应用程序中看到的 ASCII 视频过滤器,但 tileset 可以是任何东西。
- 输入图块大小: - 瓷砖数: - 显示图块大小: - 上色:
-
GPUImageJFAVoronoiFilter:生成 Voronoi 映射,供后期使用。
- sizeInPixels:单个元素的大小
-
GPUImageVoronoiConsumerFilter:接受 Voronoi 地图,并使用它来过滤传入的图像。
- sizeInPixels:单个元素的大小
如上所述,您还可以使用类似 C 的 OpenGL 着色语言轻松编写自己的自定义滤镜。
8. 自定义滤镜
8.1 基于原有的滤镜进行多个滤镜组合
我们创建一个 继承自 GPUImageFilterGroup
的类,往内部提前设置好我们想要组合的滤镜对象.调好参数即可
#import <GPUImage/GPUImageFilterGroup.h>
@interface HPBeautifulGPUBaseImageFilter : GPUImageFilterGroup
@end
#import "HPBeautifulGPUBaseImageFilter.h"
@implementation HPBeautifulGPUBaseImageFilter
- (instancetype)init
{
self = [super init];
if (self) {
[self addFilter:<#(GPUImageOutput<GPUImageInput> *)#>]
[self addFilter:<#(GPUImageOutput<GPUImageInput> *)#>]
[self addFilter:<#(GPUImageOutput<GPUImageInput> *)#>]
...
}
return self;
}
@end
8.2 基于滤镜基类GPUImageFilter
基于滤镜基类GPUImageFilter
,则是通过创建一个新的滤镜类继承自GPUImageFilter
,重新编写着色器程序;
举例说明:
#import <GPUImage/GPUImageFilter.h>
NSString * const kHPGPUImageLightFilterShaderString = SHADER_STRING
(
precision highp float;
uniform sampler2D inputImageTexture;
varying vec2 textureCoordinate;
uniform float time;
const vec2 TextureCoordsVarying = vec2(400.0, 400.0);
const vec2 TexSize = vec2(400.0, 400.0);
const vec2 mosaicSize = vec2(16.0, 16.0);
void main (void) {
vec2 intXY = vec2(TextureCoordsVarying.x*TexSize.x, TextureCoordsVarying.y*TexSize.y);
vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x,floor(intXY.y/mosaicSize.y)*mosaicSize.y) + 0.5*mosaicSize;
vec2 delXY = XYMosaic - intXY;
float delL = length(delXY);
vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x,XYMosaic.y/TexSize.y);
vec4 _finalColor;
if(delL< 0.5*mosaicSize.x)
_finalColor = texture2D(Texture,UVMosaic);
else
_finalColor = texture2D(Texture,TextureCoordsVarying);
gl_FragColor = _finalColor;
}
);
@interface NewImageFilter : GPUImageFilter
@end
@implementation NewImageFilter
- (instancetype)init {
self = [super initWithFragmentShaderFromString:kHPGPUImageLightFilterShaderString];
return self;
}
@end
这里的kHPGPUImageLightFilterShaderString
就是新写的着色器程序,是基于GLSL语言编写的。
因此,我们要自定义滤镜,还要学GLSL语言(后面回顾OPenGL ES 的时候 会重点关注)
9. 示例代码
- 官方Example: github.com/BradLarson/…
给静态图添加滤镜
//1.获取图片
_myImage = [UIImage imageNamed:@"my.jpeg"];
//2.初始化饱和度滤镜
_disFilter = [[GPUImageSaturationFilter alloc]init];
//设置饱和度值
_disFilter.saturation = 1.0;
//设置要渲染的区域 --图片大小
[_disFilter forceProcessingAtSize:_myImage.size];
//使用单个滤镜
[_disFilter useNextFrameForImageCapture];
//3.创建图片组件--输入数据(静态图片)
GPUImagePicture *stillImageSoucer = [[GPUImagePicture alloc]initWithImage:_myImage];
//为图片添加一个滤镜
[stillImageSoucer addTarget:_disFilter];
//处理数据(图片)
[stillImageSoucer processImage];
//4.处理完成,从FrameBuffer帧缓存区中获取图片--输出数据
UIImage *newImage = [_disFilter imageFromCurrentFramebuffer];
//更新图片
_myImagView.image = newImage;
文章推荐
GPUImage
的 实战,我推荐几篇文章:
- 使用 GPUImage 实现一个简单相机
- 在 iOS 中使用 GLSL 实现抖音特效|编写片元着色器代码
- 在 iOS 中给视频添加滤镜
- 在 GPUImage 中实现染发效果
- 在 GPUImage 中检测人脸关键点
- 如何优雅地实现一个分屏滤镜
- GPUImage文集
- 用GPUImage 滤镜 美颜
- 可暂停、续录、定时、带滤镜美颜的录制器
- ...
GPUImage2 内置滤镜介绍推荐:
06-iOS 多媒体技术| Core Graphics要点回顾【UIBezierPath绘制、CGContext绘制、图像处理、富文本处理、经典第三方库等】
04-iOS 多媒体技术| Core Animation要点回顾1【CAlayer与UIView树、核心动画、Core Animation渲染原理等】
05-iOS 多媒体技术| Core Animation要点回顾2【UIKit动画、Core Animation动画、粒子动画、经典第三方动画库等】
03-iOS 多媒体技术| 图形处理框架-UIKit要点回顾2【UIView、UIViewController、UIWindow、生命周期、事件响应者链等】
02-iOS 多媒体技术| 图形处理框架-UIKit要点回顾1【UITouch、UIEvent、手势处理、UIResponder、UIApplication等】
01-iOS 多媒体技术| 图形处理框架简介:Core Animation、Core Graphics、OpenGL ES、Metal、GPUImage等
01-iOS 图层处理相关| 了解 移动端图片格式 和编解码【静态图片、动态图片】 (转)
原文链接: blog.ibireme.com
图片通常是移动端流量耗费最多的部分,并且占据着重要的视觉空间。合理的图片格式选用和优化可以为你节省带宽、提升视觉效果。在这篇文章里我会分析一下目前主流和新兴的几种图片格式的特点、性能分析、参数调优,以及相关开源库的选择。
几种图片格式的简介
首先谈一下大家耳熟能详的几种老牌的图片格式吧:
JPEG 是目前最常见的图片格式,它诞生于 1992 年,是一个很古老的格式。它只支持有损压缩,其压缩算法可以精确控制压缩比,以图像质量换得存储空间。由于它太过常见,以至于许多移动设备的 CPU 都支持针对它的硬编码与硬解码。
PNG 诞生在 1995 年,比 JPEG 晚几年。它本身的设计目的是替代 GIF 格式,所以它与 GIF 有更多相似的地方。PNG 只支持无损压缩,所以它的压缩比是有上限的。相对于 JPEG 和 GIF 来说,它最大的优势在于支持完整的透明通道。
GIF 诞生于 1987 年,随着初代互联网流行开来。它有很多缺点,比如通常情况下只支持 256 种颜色、透明通道只有 1 bit、文件压缩比不高。它唯一的优势就是支持多帧动画,凭借这个特性,它得以从 Windows 1.0 时代流行至今,而且仍然大受欢迎。
在上面这些图片格式诞生后,也有不少公司或团体尝试对他们进行改进,或者创造其他更加优秀的图片格式,比如 JPEG 小组的 JPEG 2000、微软的 JPEG-XR、Google 的 WebP、个人开发者发布的 BPG、FLIF 等。它们相对于老牌的那几个图片格式来说有了很大的进步,但出于各种各样的原因,只有少数几个格式能够流行开来。下面三种就是目前实力比较强的新兴格式了:
APNG 是 Mozilla 在 2008 年发布的一种图片格式,旨在替换掉画质低劣的 GIF 动画。它实际上只是相当于 PNG 格式的一个扩展,所以 Mozilla 一直想把它合并到 PNG 标准里面去。然而 PNG 开发组并没有接受 APNG 这个扩展,而是一直在推进它自己的 MNG 动图格式。MNG 格式过于复杂以至于并没有什么系统或浏览器支持,而 APNG 格式由于简单容易实现,目前已经渐渐流行开来。Mozilla 自己的 Firefox 首先支持了 APNG,随后苹果的 Safari 也开始有了支持, Chrome 目前也已经尝试开始支持 ,可以说未来前景很好。
WebP 是 Google 在 2010 年发布的图片格式,希望以更高的压缩比替代 JPEG。它用 VP8 视频帧内编码作为其算法基础,取得了不错的压缩效果。它支持有损和无损压缩、支持完整的透明通道、也支持多帧动画,并且没有版权问题,是一种非常理想的图片格式。借由 Google 在网络世界的影响力,WebP 在几年的时间内已经得到了广泛的应用。看看你手机里的 App:微博、微信、QQ、淘宝、网易新闻等等,每个 App 里都有 WebP 的身影。Facebook 则更进一步,用 WebP 来显示聊天界面的贴纸动画。
BPG 是著名程序员 Fabrice Bellard 在去年 (2014年) 发布的一款超高压缩比的图片格式。这个程序员大家可能有些面生,但说起他的作品 FFmpeg、QEMU 大家想必是都知道的。BPG 使用 HEVC (即 H.265) 帧内编码作为其算法基础,就这点而言,它毋庸置疑是当下最为先进的图片压缩格式。相对于 JP2、JPEG-XR、WebP 来说,同等体积下 BPG 能提供更高的图像质量。另外,得益于它本身基于视频编码算法的特性,它能以非常小的文件体积保存多帧动画。 Fabrice Bellard 聪明的地方在于,他知道自己一个人无法得到各大浏览器厂商的支持,所以他还特地开发了 Javascript 版的解码器,任何浏览器只要加载了这个 76KB 大小的 JS 文件,就可以直接显示 BPG 格式的图片了。目前阻碍它流行的原因就是 HEVC 的版权问题和它较长的编码解码时间。尽管这个图片格式才刚刚发布一年,但已经有不少厂子开始试用了,比如阿里和腾讯。
移动端图片类型的支持情况
目前主流的移动端对图片格式的支持情况如何呢?我们分别来看一下 Android 和 iOS 目前的图片编解码架构吧:
Android 的图片编码解码是由 Skia 图形库负责的,Skia 通过挂接第三方开源库实现了常见的图片格式的编解码支持。
- 目前来说,Android 原生支持的格式只有
JPEG
、PNG
、GIF
、BMP
和WebP
(Android 4.0 加入),在上层能直接调用的编码方式也只有JPEG
、PNG
、WebP
这三种。 - 目前来说 Android 还不支持直接的动图编解码。
iOS 底层是用 ImageIO.framework 实现的图片编解码。
- 目前 iOS 原生支持的格式有:
JPEG
、JPEG2000
、PNG
、GIF
、BMP
、ICO
、TIFF
、PICT
- 自 iOS 8.0 起,ImageIO 又加入了
APNG
、SVG
、RAW
格式的支持。在上层,开发者可以直接调用 ImageIO 对上面这些图片格式进行编码和解码。对于动图来说,开发者可以解码动画GIF
和APNG
、可以编码动画GIF
。
两个平台在导入第三方编解码库时,都多少对他们进行了一些修改,比如 Android 对 libjpeg 等进行的调整以更好的控制内存,iOS 对 libpng 进行了修改以支持 APNG,并增加了多线程编解码的特性。除此之外,iOS 专门针对 JPEG 的编解码开发了 AppleJPEG.framework,实现了性能更高的硬编码和硬解码,只有当硬编码解码失败时,libjpeg 才会被用到。
静态图片的编码与解码
由于我目前主要是做 iOS 开发,所以下面的性能评测都是基于 iPhone 的,主要测试代码可以在这里看到。测试素材很少,只有两个:
第一张是Dribbble 的 Logo,包含 Alpha 通道,用于测试简单的、图形类的图像。
第二张经典的 Lena 图,用于测试照片类的、具有丰富细节的图像。
每个图像都有 64x64、128x128、256x256、512x512 四种分辨率。
JPEG
目前比较知名的 JPEG 库有以下三个:
libjpeg:开发时间最早,使用最广泛的 JPEG 库。由于 JPEG 标准过于复杂和模糊,并没有其他人去实现,所以这个库是 JPEG 的事实标准。
libjpeg-turbo:一个致力于提升编解码速度的 JPEG 库。它基于 libjpeg 进行了改造,用 SIMD 指令集 (MMX、SSE2、NEON) 重写了部分代码,官网称相对于 libjpeg 有 2 到 4 倍的性能提升。
MozJPEG: 一个致力于提升压缩比的 JPEG 库。它是 Mozilla 在 2014 年发布的基于 libjpeg-turbo 进行改造的库,相对于 libjpeg 有 5% ~ 15%的压缩比提升,但相应的其编码速度也慢了很多。
除了上面这三个库,苹果自己也开发了一个 AppleJPEG,但并没有开源。其调用了芯片提供的 DSP 硬编码和硬解码的功能。虽然它不如上面这三个库功能完善,但其性能非常高。在我的测试中,其编解码速度通常是 libjpeg-turbo 的 1~2 倍。可惜的是,目前开发者并不能直接访问这个库。
下面是 ImageIO (AppleJPEG/libpng) 在 iPhone 6 上的编解码性能:
可以看到,JPEG 编码中 quality 越小,图片体积就越小,质量越也差,编码时间也越短。解码时间并没有很大的差距,可能是其大部分时间消耗在了函数调用、硬件调用上。苹果在自己的相册 Demo 中提供的 quality 的默认值是 0.9,在这个值附近,图像质量和体积、编码解码时间之间都能取得不错的平衡。
PNG
相对于 JPEG 来说,PNG 标准更为清晰和简单,因此有很多公司或个人都有自己的 PNG 编码解码实现。但目前使用最广的还是 PNG 官方发布的 libpng 库。iOS 和 Android 底层都是调用这个库实现的 PNG 编解码。
下面是 PNG 在 iPhone 6 上的编解码性能:
可以看到,在编解码图形类型(颜色少、细节少)的图片时,PNG 和 JPEG 差距并不大;但是对于照片类型(颜色和细节丰富)的图片来说,PNG 在文件体积、编解码速度上都差 JPEG 不少了。
和 JPEG 不同,PNG 是无损压缩,其并不能提供压缩比的选项,其压缩比是有上限的。目前网上有很多针对 PNG 进行优化的工具和服务,旨在提升 PNG 的压缩比。下面是常见的几个 PNG 压缩工具的性能对比:
pngcrush 是 Xcode 自带的 PNG 压缩工具,相对于设计师用 Photoshop 生成的图片来说,它能取得不错的压缩效果。ImageOptim 则更进一步,对每张图用多种缩算法进行比对,选择压缩比更高的结果,进一步缩小了文件体积。TinyPNG.com 相对于其他工具来说,压缩比高得不像话。它启用了类似 GIF 那样的颜色索引表对 PNG 进行压缩,所以会导致颜色丰富的图片丢失掉一部分细节。如果使用 TinyPNG 的话,最好在压缩完成后让设计师看一下颜色效果是否可以接受。
WebP
WebP 标准是 Google 定制的,迄今为止也只有 Google 发布的 libwebp 实现了该的编解码 。 所以这个库也是该格式的事实标准。
WebP 编码主要有几个参数:
lossless: YES:有损编码 NO:无损编码。WebP 主要优势在于有损编码,其无损编码的性能和压缩比表现一般。
quality: [0~100] 图像质量,0表示最差质量,文件体积最小,细节损失严重,100表示最高图像质量,文件体积较大。该参数只针对有损压缩有明显效果。Google 官方的建议是 75,腾讯在对 WebP 评测时给出的建议也是 75。在这个值附近,WebP 能在压缩比、图像质量上取得较好的平衡。
method: [0~6] 压缩比,0表示快速压缩,耗时短,压缩质量一般,6表示极限压缩,耗时长,压缩质量好。该参数也只针对有损压缩有明显效果。调节该参数最高能带来 20% ~ 40% 的更高压缩比,但相应的编码时间会增加 5~20 倍。Google 推荐的值是 4。
对于
- 编码无损图片来说,quality=0, method=
0~3
是相对来说比较合适的参数,能够节省编码时间,同时也有不错的压缩比。 - 无损编码图片,quality=75, method=
2~4
是比较合适的参数,能在编码时间、图片质量、文件体积之间有着不错的平衡。
WebP 解码有三个参数:
use_threads: 是否启用 pthread 多线程解码。该参数只对宽度大于 512 的有损图片起作用。开启后内部会用多线程解码,CPU 占用会更高,解码时间平均能缩短 10%~20%。
bypass_filtering: 是否禁用滤波。该参数只对有损图片起作用,开启后大约能缩短 5%~10% 的解码时间,但会造成一些颜色过渡平滑的区域产生色带(banding)。
no_fancy_upsampling: 是否禁用上采样。该参数只对有损图片起作用。在我的测试中,开启该参数后,解码时间反而会增加 5~25%,同时会造成一些图像细节的丢失,线条边缘会增加杂色,显得不自然。
通常情况下,这三个参数都设为 NO 即可,如果要追求更高的解码速度,则可以尝试开启 use_threads 和 bypass_filtering 这两个参数。而 no_fancy_upsampling 在任何情况下都没必要开启。
由于 WebP 测试数据较多,这里只贴一下 512x512 大小的一部分测试结果,感兴趣的可以看文章结尾处的 Excel 附件。
对于简单的图形类型的图像(比如 App 内的各种 UI 素材),WebP 无损压缩的文件体积和解码速度已经比 PNG 还要理想了,如果你想要对 App 安装包体积进行优化,WebP 已经是个不错的选择了。
对于复杂的图像(比如照片)来说,WebP 无损编码表现并不好,但有损编码表现却非常棒。相近质量的图片解码速度 WebP 相距 JPEG 已经差不了太多了,而文件压缩比却能提升不少。
BPG
BPG 是目前已知最优秀的有损压缩格式
了,它能在相同质量下比 JPEG 减少 50% 的体积。下面是经典的 Lena 图的对比,你也可以在这里看到大量其他图片的 BPG、JPEG、JPEG2000、JPEG-XR、WebP 压缩效果的在线对比,效果非常明显。
BPG 目前只有作者发布的 libbpg 可用。但作者基于 libbpg 编译出了一个 Javascript 解码器,很大的扩展了可用范围。bpg 可以以无损和有损压缩两种方式进行编码,有损压缩时可以用 quality 参数控制压缩比,可选范围为 0~51,数值越大压缩比越高。通常来说,25 附近是一个不错的选择,BPG 官方工具默认值是 28。
libbpg 目前并没有针对 ARM NEON 做优化,所以其在移动端的性能表现一般。下面是 iPhone 6 上的性能测试:
由于 bpg 编码时间太长,我并没有将数据放到表格里。可以看到相同质量下,BPG 的解码速度还是差 JPEG 太多,大约慢了 3~5 倍。目前来说,BPG 适用于那些对流量非常敏感,但对解码时间不敏感的地方。从网上的新闻来看,手机淘宝和手机QQ都已经有所尝试,但不清楚他们是否对 BPG 解码进行了优化。
小结
-
JPEG
压缩速度快, 压缩质量较弱于PNG
(结合 压缩质量 和压缩速度 综合考虑,用JPEG格式比较多) -
PNG
- 在编解码图形类型(颜色少、细节少)的图片时,PNG 和 JPEG 差距并不大;
- 但是对于照片类型(颜色和细节丰富)的图片来说,PNG 在文件体积、编解码速度上都差 JPEG 不少了。
- 和
JPEG
不同,PNG
是无损压缩,其并不能提供压缩比的选项,其压缩比是有上限的。
-
WebP
主要优势在于有损编码,无损压缩性能表现一般 -
BPG
是目前已知最优秀的有损压缩格式
了,它能在相同质量下比 JPEG 减少 50% 的体积。相同质量下,BPG
速度比JPEG
压缩速度慢很多
动态图片的编码与解码
动图在网络上非常受欢迎,它近似视频,但通常实现简单、文件体积小,应用范围非常广泛。动图的始祖是 GIF,它自 Windows 1.0 时代就在互联网上流行开来,直到今天仍然难以被其他格式取代。尽管它非常古老,但其所用的原理和今天几种新兴格式几乎一样。
下面是一张 GIF 格式的 QQ 大表情:
这张表情由 6 幅静态图构成,每幅图片有一定的存活时间,连贯播放就形成了动画:
这几张图中,大部分内容是相近的,为了压缩文件体积,通常动图格式都支持一些特殊的方式对相似图片进行裁剪,只保留前后帧不同的部分:
在解码动图时,解码器通常采用所谓"画布模式"进行渲染。想象一下:播放的区域是一张画布,第一帧播放前先把画布清空,然后完整的绘制上第一帧图;播放第二帧时,不再清空画布,而是只把和第一帧不同的区域覆盖到画布上,就像油画的创作那样。
像这样的第一帧就被称为关键帧(即 I 帧,帧内编码帧),而后续的那些通过补偿计算得到的帧被称为预测编码帧(P帧)。一个压缩的比较好的动图内,通常只有少量的关键帧,而其余都是预测编码帧;一个较差的压缩工具制作的动图内,则基本都是关键帧。不同的动图压缩工具通常能得到不同的结果。
除此之外,动图格式通常有更为详细的参数控制每一帧的绘制过程,下面是 GIF/APNG/WebP 通用的几个参数:
Disposal Method (清除方式)
Do Not Dispose:把当前帧增量绘制到画布上,不清空画布。
Restore to Background:绘制当前帧之前,先把画布清空为默认背景色。
Restore to Previous:绘制下一帧前,把先把画布恢复为当前帧的前一帧
Blend Mode (混合模式)
Blend None: 绘制时,全部通道(包含Alpha通道)都会覆盖到画布,相当于绘制前先清空画布的指定区域。
Blend over:绘制时,Alpha 通道会被合成到画布,即通常情况下两张图片重叠的效果。
上面这些技术,就是常见动图格式的基础了,下面分别介绍一下不同动图格式的特点。
GIF
GIF 缺陷非常明显:它通常只支持 256 色索引颜色,这导致它只能通过抖动、差值等方式模拟较多丰富的颜色;它的 Alpha 通道只有 1 bit,这意味着一个像素只能是完全透明或者完全不透明。
上面这是腾讯博客里的一张演示图,可以看到 GIF 由于 Alpha 通道的问题,产生了严重的"毛边"现象。目前通常的解决方案是在图片的边缘加一圈白边,以减轻这种视觉效果:
可以仔细观察一下 QQ、微信等 App 里面的动画表情,几乎每个表情都被一圈白边所环绕,不得不说是一种很无奈的解决方案。
GIF 的制作工具有很多,但效果好、压缩比高的工具非常少。对于已经制作好的 GIF 来说,用 imagemagick 处理一下可以把文件体积压缩不少。如果需要将视频转为 GIF,Cinemagraph Pro 是个不错的傻瓜化工具。这里有一篇文章介绍如何用 ffmpeg 压缩 GIF,虽然参数调节有点麻烦,但效果非常理想。
下面是没有经过优化的 GIF 和经过 ffmpeg 优化编码的 GIF,可以看到差距非常大。
[
APNG
APNG 目前并没有被 PNG 官方所接受,所以 libpng 并不能直接解码 APNG。但由于 APNG 只是基于 PNG 的一个简单扩展,所以在已经支持 PNG 的平台上,可以很轻松的用少量代码实现 APNG 的编解码。Chromium 为了支持 APNG 播放,只增加了不到 600 行代码 ,我自己也用大概 500 行 C 代码实现了一个简单的 APNG 编解码工具。另外,在支持 canvas 的浏览器上,可以用 apng-canvas 直接显示 APNG 动画。APNG 压缩最好的工具目前是 apngasm,大部分图形化工具比如腾讯的 iSparta 都是基于这个工具开发的。
就目前而言, APNG 是 GIF 最好的替代了:实现简单,可用范围广,压缩比不错,显示效果好。
WebP
WebP 在 2010 年 发布时并没有支持动图。2012 年 libwebp v0.2 的时候,Google 才开始尝试支持动画,但其实现有很多问题,性能也非常差,以至于 Chrome 团队一直都没有接受。直到 2013 年,libwebp v0.4 时,动画格式才稳定下来才被 Chrome 所接受。
WebP 动图实际上是把多个单帧 WebP 数据简单打包到一个文件内,而并不是由单帧 WebP 扩展而来,以至于动图格式并不能向上兼容静态图。如果要支持动图,首先在编译 libwebp 时需要加上 demux 模块,解码 WebP 时需要先用 WebPDemuxer 尝试拆包,之后再把拆出来的单帧用 WebPDecode 解码。为了方便编译,我写了个脚本用于打包 iOS 的静态库,加入了 mux 和 demux 模块。
Google 提供了两个简单的命令行工具用于制作动图:gif2webp 能把 GIF 转换为 WebP, webpmux 能把多个 WebP 图片打包为动态图,并且有着很多参数可以调节。这两个工具对相近帧的压缩并不太理想,以至于有的情况下压缩比还不如 APNG,但除此以外也没有其他什么更好的工具可以用了。
BPG
BPG 本身是基于 HEVC (H.265) 视频编码的,其最开始设计时就考虑到了动图的实现。由于它充分利用了 HEVC 的高压缩比和视频编码的特性,其动图压缩比远超其他格式。这里和这里有几张 BPG 动图示例,可以看到相同质量下 BPG 动图只有 APNG/WebP/GIF 几十分之一的大小。
我在这里写了个简单的利用 libbpg 解码动图的方法,如有需要可以参考下。
动图性能对比
我把下面这张 GIF 分别转为 WebP、APNG、BPG 动图,并在 iPhone 6 上对其所有帧进行解码。
评测结果如下:
APNG 在文件体积上比 GIF 略有优势,解码时间相差不多。WebP 在体积和解码时间上都具有较大的优势。BPG 在体积上优势最大,但解码时间也最长。这么看来,APNG 和 WebP 都是不错的选择,而 BPG 还有待性能优化。
最后做一个小广告:如果你是 iOS 平台的开发者,可以试试我开发的 YYWebImage,它支持 APNG、WebP、GIF 动图的异步加载与播放、编码与解码,支持渐进式图像加载,可以替代 SDWebImage、PINRemoteImage、FLAnimatedImage 等开源库。
小结
-
GIF
缺陷非常明显:它通常只支持 256 色索引颜色,这导致它只能通过抖动、差值等方式模拟较多丰富的颜色;它的 Alpha 通道只有 1 bit,这意味着一个像素只能是完全透明或者完全不透明。 -
APNG
是 GIF 最好的替代了:实现简单,可用范围广,压缩比不错,显示效果好
评测数据
上面提到的所有评测数据表格:image_benchmark.xlsx 推荐用 Excel 打开查看。
iOS-Swift语法静态分析配置|统一编码规范【Fastlane+SwiftLint】
SwiftUI 快速上手推荐
前言
最近在回顾Flutter技术时,与老同事聊起SwiftUI,我们了解到SwiftUI和Flutter UI技术都是声明式语法,因此近两天写了一些Demo进行简单尝试
一、SwiftUI快速上手
为了快速上手,我找了一些对SwiftUI有所研究的博主,跟着对方的博客笔记敲击了十来个Demo:
在多次尝试之后,我发现,通过SwiftUI写UI,的确会一定程度上减少代码量。
二、博文推荐
SwiftUI快速上手推荐:
三、SwiftUI与UIKit的异同:
- 1. 声明式 vs 命令式:SwiftUI 是声明式的,你只需要描述界面应该是什么样子,而 SwiftUI 会处理实际的渲染过程。而 UIKit 是命令式的,你需要告诉 UIKit 如何一步步地创建和更新界面。
- 2. 跨平台:SwiftUI 可以在所有 Apple 平台(包括 iOS、macOS、watchOS 和 tvOS)上使用,而 UIKit 只能用于 iOS 和 tvOS。
- 3. 组件:SwiftUI 提供了一些 UIKit 中没有的组件,如 Grid 和 Form。同时,SwiftUI 的组件在所有 Apple 平台上都有一致的外观和行为。
- 4. 数据流:SwiftUI 引入了一些新的数据流概念,如 @State、@Binding、@ObservedObject 和 @EnvironmentObject,它们使得数据状态管理更加直观和一致。而在 UIKit 中,你通常需要手动管理数据状态和更新界面。
- 5. 兼容性:SwiftUI 需要 iOS 13 或更高版本,而 UIKit 可以在更早的 iOS 版本上使用。
- 6. 成熟度:UIKit 已经存在了很长时间,有大量的文档、教程和社区支持。而 SwiftUI 是在 2019 年才推出的,虽然它正在快速发展,但还没有 UIKit 那么成熟。
总的来说,SwiftUI 提供了一种更现代、更简洁的方式来构建界面,但如果你需要支持旧的 iOS 版本,或者需要使用 UIKit 提供的一些高级功能,那么 UIKit 仍然是一个很好的选择。
四、SwiftUI布局
写了几个Demo后,我们可以了解到,其实要用SwiftUI斜截面,掌握SwiftUI的布局方法很重要
在 SwiftUI 中,布局主要由容器视图(如 HStack、VStack 和 ZStack)和修饰符(如 padding、frame 和 offset)来控制。
以下是一些常用的容器视图:
- HStack:在水平方向上排列其子视图。
- VStack:在垂直方向上排列其子视图。
- ZStack:在深度方向上堆叠其子视图。
以下是一些常用的修饰符:
- padding:给视图添加内边距。
- frame:设置视图的尺寸和对齐方式。
- offset:移动视图的位置。
在HStack、VStack、ZStack中还可以用spacing来约束,容器内部子视图的间距
iOS13 Scene Delegate详解(转)
iOS13 项目中的SceneDelegate
类有什么作用?自从Xcode11发布以来,当你使用新XCode创建一个新的iOS项目时,SceneDelegate
会被默认创建,它到底有什么用呢。
在本文中,我们将深入探讨iOS 13和Xcode 11的一些变化。我们将重点关注SceneDelegate和AppDelegate,以及它们如何影响SwiftUI、Storyboard和基于XIB的UI项目。
通过阅读本文你将了解到:
- SceneDelegate和AppDelegate的新变化
- 他们是如何合作引导你的app启动的
- 在纯手写App中使用
SceneDelegate
- 在Storyboards 和 SwiftUI项目中使用
SceneDelegate
让我们开始吧。
本篇文章基于 Xcode 11 和 iOS 13.
AppDelegate
你可能对AppDelegate已经熟悉,他是iOS app的入口,application(_:didFinishLaunchingWithOptions:)
是你的app启动后系统调用的第一个函数。
AppDelegate
类实现了UIKit库中的UIApplicationDelegate
协议。而到了iOS13 AppDelegate
的角色将会发生变化,后面我们会详细讨论。
下面是你在iOS12中一般会在AppDelegate中做的事情:
- 创建app的第一个view controller也就是 rootViewController
- 配置并启动一些像日志记录和云服务之类的组件
- 注册推送通知处理程序,并响应发送到app的推送通知
- 响应应用程序生命周期事件,例如进入后台,恢复应用程序或退出应用程序(终止)
iOS12及以前,使用Storyboards的app,AppDelegate很简单。 像这样:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
return true
}
一个使用XIB的简单应用看起来像这样:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
let timeline = TimelineViewController()
let navigation = UINavigationController(rootViewController: timeline)
let frame = UIScreen.main.bounds
window = UIWindow(frame: frame)
window!.rootViewController = navigation
window!.makeKeyAndVisible()
return true
}
在上面的代码中,我们创建一个ViewController,并将其放在navigation controller中。然后将其分配给UIWindow对象的rootViewController属性。 这个window
对象是AppDelegate的属性,它是我们的应用的一个窗口。
应用程序的window是一个重要的概念。 本质上,窗口就是应用程序,大多数iOS应用程序只有一个窗口。 它包含您应用的用户界面(UI),将事件调度到视图,并提供了一个主要背景层来显示您的应用内容。 从某种意义上说,“ Windows”的概念就是微软定义的窗口,而在iOS上,这个概念没有什么不同。 (谢谢,Xerox!)
好了,下面让我们继续SceneDelegate。
如果“窗口”的概念仍然不了解,请查看iPhone上的应用程序切换器。 双击Home键或从iPhone底部向上滑动,然后您会看到当前正在运行的应用程序的窗口。 这就是应用程序切换器。
The Scene Delegate
在iOS 13(及以后版本)上,SceneDelegate将负责AppDelegate的某些功能。 最重要的是,window(窗口)的概念已被scene(场景)的概念所代替。 一个应用程序可以具有不止一个场景,而一个场景现在可以作为您应用程序的用户界面和内容的载体(背景)。
尤其是一个具有多场景的App的概念很有趣,因为它使您可以在iOS和iPadOS上构建多窗口应用程序。 例如,文档编辑器App中的每个文本文档都可以有自己的场景。 用户还可以创建场景的副本,同时运行一个应用程序的多个实例(类似多开)。
在Xcode 11中有三个地方可以明显地看到SceneDelegate的身影:
- 现在,一个新的iOS项目会自动创建一个
SceneDelegate
类,其中包括我们熟悉的生命周期事件,例如active,resign和disconnect。 - AppDelegate类中多了两个与“scene sessions”相关的新方法:
application(_:configurationForConnecting:options:)
和application(_:didDiscardSceneSessions:)
- Info.plist文件中提供了”Application Scene Manifest“配置项,用于配置App的场景,包括它们的场景配置名,delegate类名和storyboard
让我们一次开看一看。
1. Scene Delegate Class
首先,SceneDelegate类:
SceneDelegate的最重要的函数是:scene(_:willConnectTo:options:)
。 在某种程度上,它与iOS 12上的 application(_:didFinishLaunchingWithOptions:)
函数的作用最相似。当将场景添加到app中时scene(_:willConnectTo:options:)
函数会被调用的,因此这里是配置场景的最理想地方。 在上面的代码中,我们手动地设置了视图控制器堆栈,稍后会进行详细介绍。
这里需要特别注意的是,“SceneDelegate”采用了协议模式,并且这个delegate通常会响应任何场景。 您使用一个Delegate来配置App中的所有场景。
SceneDelegate
还具有下面这些函数:
-
sceneDidDisconnect(_:)
当场景与app断开连接是调用(注意,以后它可能被重新连接) -
sceneDidBecomeActive(_:)
当用户开始与场景进行交互(例如从应用切换器中选择场景)时,会调用 -
sceneWillResignActive(_:)
当用户停止与场景交互(例如通过切换器切换到另一个场景)时调用 -
sceneWillEnterForeground(_:)
当场景变成活动窗口时调用,即从后台状态变成开始或恢复状态 -
sceneDidEnterBackground(_:)
当场景进入后台时调用,即该应用已最小化但仍存活在后台中
看到函数的对称性了吗? Active/inactive, background/foreground, 和 “disconnect”. 。 这些是任何应用程序的典型生命周期事件。
2.App Delegate中的Scene Sessions
在iOS13中AppDelegate中有两个管理Senen Session的代理函数。在您的应用创建scene(场景)后,“scene session”对象将跟踪与该场景相关的所有信息。
这两个函数是:
-
application(_:configurationForConnecting:options:)
, 会返回一个创建场景时需要的UISceneConfiguration对象 -
application(_:didDiscardSceneSessions:)
, 当用户通过“应用切换器”关闭一个或多个场景时会被调用
目前,SceneSession被用于指定场景,例如“外部显示” 或“ CarPlay” 。 它还可用于还原场景的状态,如果您想使用【状态还原】,SceneSession将非常有用。 状态还原允许您在应用启动之间保留并重新创建UI。 您还可以将用户信息存储到场景会话中,它是一个可以放入任何内容的字典。
application(_:didDiscardSceneSessions:)
很简单。 当用户通过“应用程序切换器”关闭一个或多个场景时,即会调用该方法。 您可以在该函数中销毁场景所使用的资源,因为不会再需要它们。
了解application(_:didDiscardSceneSessions:)
与sceneDidDisconnect(_ :)
的区别很重要,后者仅在场景断开连接时调用,不会被丢弃,它可能会重新连接。而application(_:didDiscardSceneSessions:)
发生在使用【应用程序切换器】退出场景时。
3. Info.plist 中的Application Scene Manifest
您的应用支持的每个场景都需要在“Application Scene Manifest”(应用场景清单)中声明。 简而言之,清单列出了您的应用支持的每个场景。 大多数应用程序只有一个场景,但是您可以创建更多场景,例如用于响应推送通知或特定操作的特定场景。
Application Scene Manifest清单是Info.plist文件的一项,都知道该文件包含App的配置信息。 Info.plist包含诸如App的名称,版本,支持的设备方向以及现在支持的不同场景等配置。
请务必注意,您声明的是会话的“类型”,而不是会话实例。 您的应用程序可以支持一个场景,然后创建该场景的副本,来实现【多窗口】应用程序。
下面看一下的 Info.plist
中清单的一些配置:
在红框内,您会看到Application Scene Manifest 这一条。 在它下面一条是Enable Multiple Windows,需要将其设置为“ YES”以支持多个窗口。 再往下Application Session Role的值是一个数组,用于在应用程序中声明场景。 你也可以在数组中添加一条【外部屏幕】的场景声明。
最重要的信息保存在Application Session Role数组中。 从中我们可以看到以下内容:
- Configuration的名称,必须是唯一的
- 场景的代理类名,通常为
SceneDelegate
。 - 场景用于创建初始UI的storyboard名称
Storyboard名称这一项可能使您想起Main Interface设置,该设置可以在Xcode 12项目的Project Properties配置中找到。 现在,在iOS应用中,你可以在此处设置或更改主Storyboard名称。
AppDelegate中的SceneDelegate、UISceneSession和Application Scene Manifest是如何一起创建多窗口应用的呢?
- 首先,我们看
SceneDelegate
类。 它管理场景的生命周期,处理各种响应,诸如sceneDidBecomeActive(_:)
andsceneDidEnterBackground(_:)
之类的事件。 - 然后,我们再看看
AppDelegate
类中的新函数。 它管理场景会话(scene sessions),提供场景的配置数据,并响应用户丢弃场景的事件。 - 最后,我们看了一下Application Scene Manifest。 它列出了您的应用程序支持的场景,并将它们连接到delegate类并初始化storyboard。
Awesome! Now that we’ve got a playing field, let’s find out how scenes affects building UIs in Xcode 11.
太棒了! 现在让我们了解scenes(场景)是如何影响Xcode 11中的用户界面的吧。
在SwiftUI中使用Scene Delegate
不久将来,SwiftUI将是创建iOS 13项目最简单的方法。 简言之,SwiftUI应用程序主要依靠SceneDelegate来设置应用程序的初始UI。
首先,SwiftUI项目中“Application Scene Manifest ”将长这样:
特别注意,配置中没有设置“Storyboard Name”这一项。 请记住,如果要支持多个窗口,则需要将Enable Multiple Windows设置为YES
。
我们将跳过“ AppDelegate”,因为它相当标准。在SwiftUI项目中,只会返回“true”。
接下来是SceneDelegate
类。 正如我们之前讨论的,SceneDelegate负责设置您应用中的场景,以及设置首个页面。
像下面一样:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
}
上面的代码中发生了什么?
- 首先,必须明确的是 在将新场景添加到应用中后 会调用
scene(_:willConnectTo:options:)
代理函数。 它提供了一个scene
对象(和一个session)。 这个“UIWindowScene”对象是由应用创建的,您无需进行其他操作。 - 其次,
window
属性会在这里用到。 App仍然使用“ UIWindow”对象,但现在它们已成为scene(场景)的一部分。 在if let
代码块中,您可以清楚地看到如何使用scene来初始化UIWindow对象的。 - 然后是设置window的rootViewController,将
window
实例分配给了场景的window
属性,并且设置窗口makeKeyAndVisible
为true,即将该窗口置于App的前面。 - 接着为SwiftUI项目创建了ContentView实例,并通过使用UIHostingController将其添加为根视图控制器。 该控制器用于将基于SwiftUI的视图显示在屏幕上。
- 最后但并非不重要的一点,值得注意的是,UIScene的实例化对象scene实际上是UIWindowScene类型的对象。 这就是
as?
对可选类型转换的原因。 (到目前为止,已创建的场景通常为“ UIWindowScene”类型,但我猜想将来还会看到更多类型的场景。)
所有这些看起来似乎很复杂,但是从高层次的概述来看,这很简单:
- 当
scene(_:willConnectTo:options:)
被调用时,SceneDelegate会在正确的时间配置场景。 - AppDelegate和Manifest的默认配置,他们没有涉及storyboard的任何东西。
-
scene(_:willConnectTo:options :)
函数内,创建一个SwiftUI视图,将其放置在托管控制器中,然后将控制器分配给window属性的根视图控制器,并将该窗口放置在应用程序UI的前面 。
太棒了! 让我们继续。
您可以通过选择File(文件)→New(新建)→Project(项目)来建立一个基本的Xcode 11项目。 然后,选择
Single View App
, 在User Interface
处选择SwiftUI来创建一个SwiftUI项目
在Storyboards项目中使用SceneDelegate
Storyboards和XIB是为iOS应用程序构建UI的有效方法。 在iOS 13上也是如此。 在将来,我们将看到更多的SwiftUI应用,但目前,Storyboards更常见。
有趣的是,即使有了SceneDelegate,通过Storyboards创建iOS项目你也不需要做任何额外的事情 只需选择File → New → Project
。 然后,选择Single View App
。 最后,为 User Interface 处选择 Storyboard ,就完成了。
设置方法如下:
- 如我们前面提到过的,您可以在Info.plist中的“ Application Scene Manifest”中找到
Main
storyboard的设置地方。 - 默认情况下,AppDelegate将使用默认的UISceneConfiguration。
- SceneDelegate会设置一个“UIWindow”对象,并使用“Main.storyboard”来创建初始UI。
纯代码编写UI
许多开发人员喜欢手写UI,而随着SwiftUI的兴起,使用SwiftUI手写代码将会越来越常见。 如果您不使用storyboards,而使用XIB创建应用程序UI,该怎么办? 让我们看看SceneDelegate
如何修改。
首先,AppDelegate和Application Scene Manifest中保持默认值。 我们不使用storyboard,所以需要在SceneDelegate类的scene(_:willConnectTo:options:)
函数中设置初始视图控制器。
像下面这样:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
{
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let timeline = TimelineViewController()
let navigation = UINavigationController(rootViewController: timeline)
window.rootViewController = navigation
self.window = window
window.makeKeyAndVisible()
}
}
}
上面代码很简单我们就不详细介绍了。
很简单,对吧? 使用SceneDelegate的核心是将一些代码从AppDelegate移至到SceneDelegate中,并正确配置 Application Scene Manifest 。
想要为现有的iOS项目添加scene(场景)支持? 可以查看Apple官方文档https://developer.apple.com/documentation/uikit/app_and_environment/scenes/specifying_the_scenes_your_app_supports。
作者:Reinder de Vries
翻译:乐Coding
作者:乐Coding
链接:https://juejin.cn/post/6844903993496305671
Swift 加密方案与密钥生成方法|加密方案:【SwiftyRSA与CryptoSwift】、密钥生成方法:【RSA 密钥、 ECC 密钥、国密密钥】
一、SwiftyRSA与CryptoSwift
推荐两个加密方案开源框架:
-
SwiftyRSA
- RSA加密解密
- RSA签名与验证
-
CryptoSwift
- AES
- DES
- MD5
- HASH算法
- ......
具体的使用方式,参照链接中的README.md
二、密钥
根据开发的业务需求,查看生成密钥的方法。密钥包括 RSA 密钥、ECC 密钥、国密密钥。
前置条件
已通过 OpenSSL 官网 下载并安装 OpenSSL 工具(1.1.1 或以上版本)
生成 RSA 密钥
-
打开 OpenSSL 工具,使用以下命令行生成 RSA 私钥。您可以选择生成 1024 或 2048 位的私钥。
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
-
根据 RSA 私钥生成 RSA 公钥。
openssl rsa -pubout -in private_key.pem -out public_key.pem
生成 ECC 密钥
-
打开 OpenSSL 工具,使用以下命令行生成 ECC 的密钥对。您必须选择 secp256k1 椭圆曲线算法。
openssl ecparam -name secp256k1 -genkey -noout -out secp256k1-key.pem
-
根据
secp256k1-key.pem
密钥对生成 ECC 公钥。
openssl ec -in secp256k1-key.pem -pubout -out ecpubkey.pem
生成国密密钥
-
打开 OpenSSL 工具,使用以下命令行生成国密 SM2 私钥
sm2-key.pem
。
openssl ecparam -name SM2 -genkey -noout -out sm2-key.pem
-
根据
sm2-key.pem
密钥对生成国密 SM2 公钥sm2pubkey.pem
。
openssl ec -in sm2-key.pem -pubout -out sm2pubkey.pem
RxSwift以及基于RxSwift封装的库|RxSwift、RxCocoa(转)
前言
最近需要维护公司的老项目,工程代码内部实现基本都是基于RxSwift
库、基于RxSwift
封装的库实现的代码逻辑;故,筛选转发RxSwift
的博文,方便自己快速上手老项目代码。
一、RxSwift
翻了好多博文,最终觉得来自字节跳动的同学写的最精简,容易上手。
系列文章如下:
- RxSwift官方使用示例之<创建和订阅Observables>
- RxSwift官方使用示例之<使用Subjects>
- RxSwift官方使用示例之<组合操作符>
- RxSwift官方使用示例之<转换操作符>
- RxSwift官方使用示例之<过滤及条件操作符>
- RxSwift官方使用示例之<数学和聚合操作符>
- RxSwift官方使用示例之<可连接操作符>
- RxSwift官方使用示例之<错误处理操作符>
二、RxCocoa
01-H5与Native交互-JS与Native互调|WebViewJavaScriptBridge 基本使用(转)
一、iOS中的WebView
在 iOS 开发 Hybrid App 的时候,有两个 WebView 可以选择:
UIWebView
WKWebView
这两个 WebView 控件,可以完全只借助 iOS 自带的框架进行 OC & JS 交互。
-
UIWebView
使用JavaScriptCore
. -
WKWebView
使用WKUserContentController
.
二、WebView 与 原生的交互原理
UIWebView 原生的交互原理
通过一个 JSContext 获取 UIWebView 的 JS 执行上下文。
然后通过这个上下文,进行 OC & JS 的双端交互。
_jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
_jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"%@",@"获取 WebView JS 执行环境失败了!");
};
WKWebView 原生交互原理
通过 userContentController
把需要观察的 JS 执行函数注册起来。
然后通过一个协议方法,将所有注册过的 JS 函数执行的参数传递到此协议方法中。
注册 需要 观察的 JS 执行函数
[webView.configuration.userContentController addScriptMessageHandler:self name:@"jsFunc"];
在 JS 中调用这个函数并传递参数数据
window.webkit.messageHandlers.jsFunc.postMessage({name : "李四",age : 22});
OC 中遵守 WKScriptMessageHandler
协议。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
此协议方法里的 WKScriptMessage 有 name & body 两个属性。 name 可以用来判断是哪个 JSFunc 调用了。body 则是 JSFunc 传递到 OC 的参数。
三、WebViewJavaScriptBridge
WebViewJavaScriptBridge 用于 WKWebView & UIWebView 中 OC 和 JS 交互。
它的基本原理是:
把 OC 的方法注册到桥梁中,让 JS 去调用。
把 JS 的方法注册在桥梁中,让 OC 去调用。
WebViewJavascriptBridge 基本原理
注册自己,调用它人。
四、WebViewJavaScriptBridge 使用的基本步骤
- 首先在项目中导入
WebViewJavaScriptBridge 框架
。
pod ‘WebViewJavascriptBridge’
- 导入头文件
#import <WebViewJavascriptBridge.h>
。 - 建立 WebViewJavaScriptBridge 和 WebView 之间的关系。
_jsBridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
- 在HTML 文件中,复制粘贴这两段 JS 函数。
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback]; // 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
var WVJBIframe = document.createElement('iframe'); // 创建一个 iframe 元素
WVJBIframe.style.display = 'none'; // 不显示
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; // 设置 iframe 的 src 属性
document.documentElement.appendChild(WVJBIframe); // 把 iframe 添加到当前文导航上。
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
// 这里用于注册 Native 将要调用的 JS 方法。
setupWebViewJavascriptBridge(function(bridge){
});
到此为止,基本的准备工作就做完了。现在需要往桥梁中注入 OC 方法 和 JS 函数了。
往桥梁中注入 OC 方法 和 JS 函数
往桥梁中注入 OC 方法。
[_jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"dataFrom JS : %@",data[@"data"]);
responseCallback(@"扫描结果 : www.baidu.com");
}];
这段代码的意思:
- scanClick 是 OC block 的一个别名。
- block 本身,是 JS 通过某种方式调用到 scanClick 的时候,执行的代码块。
- data ,由于 OC 这端由 JS 调用,所以 data 是 JS 端传递过来的数据。
- responseCallback OC 端的 block 执行完毕之后,往 JS 端传递的数据。
往桥梁中注入 JS 函数.
OC 方法,在 OC 中注入。JS 的方法所以必然就需要在 JS 中注入的。(好像是废话)
在 JS 的方法如何注入到桥梁呢?
之前,在准备工作的时候,有两段 JS 代码。
需要在第二段 JS 代码中,注入 JS 的函数。
// 这里主要是注册 OC 将要调用的 JS 方法。
setupWebViewJavascriptBridge(function(bridge){
// 声明 OC 需要调用的 JS 方法。
bridge.registerHanlder('testJavaScriptFunction',function(data,responseCallback){
// data 是 OC 传递过来的数据.
// responseCallback 是 JS 调用完毕之后传递给 OC 的数据
alert("JS 被 OC 调用了.");
responseCallback({data: "js 的数据",from : "JS"});
})
});
这段代码的意思:
- testJavaScriptFunction 是注入到桥梁中 JS 函数的别名。以供 OC 端调用。
- 回调函数的 data。 既然 JS 函数由 OC 调用,所以 data 是 OC 端传递过来的数据。
- responseCallback 。 JS 调用在被 OC 调用完毕之后,向 OC 端传递的数据。
基本就是:
OC 端注册 OC 的方法,OC 端调用 JS 的函数。
JS 端注册 JS 的函数,JS 端调用 OC 的方法。
五、原生与Web互调场景
1.JS -> OC 的交互
在 HTML 中,有个按钮,点击这个按钮,修改 NavigationBar 的颜色。
- 在 OC 端,往桥梁注入一个修改 NavigationBar 颜色的 block.
- 在 JS 端,调用这个 block,来间接的达到修改颜色的目的。
首先,在 OC 中,通过 WebViewJavascriptBridge 注册一个修改 navigationBar 颜色的 Block。
[_jsBridge registerHandler:@"colorClick" handler:^(id data, WVJBResponseCallback responseCallback) {
self.navigationController.navigationBar.barTintColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
responseCallback(@"颜色修改完毕!");
}];
然后再 JS 中,通过某种方式去调用这个 OC 的 block。
WebViewJavascriptBridge.callHandler('colorClick',function(dataFromOC) {
alert("JS 调用了 OC 注册的 colorClick 方法");
document.getElementById("returnValue").value = dataFromOC;
})
这里通过某种方式就是使用 WebViewJavascriptBridge.callHandler('OC 中block 别名',callback) 的方式来调用。
2.OC -> JS 的交互
OC 上有一个UIButton,点击这儿按钮,把 HTML body 的颜色修改成橙色。
首先,往桥梁中,注入一个修改 HTML body 颜色的 JSFunction。
// 在这里声明 OC 需要主动调用 JS 的方法。
setupWebViewJavascriptBridge(function(bridge) {
bridge.registerHandler('changeBGColor',function(data,responseCallback){
// alert('aaaaaa');
document.body.style.backgroundColor = "orange";
document.getElementById("returnValue").value = data;
});
});
然后在 OC 端通过桥梁调用这个 changeBGColor
。
[_jsBridge callHandler:@"changeBGColor" data:@"把 HTML 的背景颜色改成橙色!!!!"];
执行效果:
六、补充
OC 调用 JS 的三种情况。
// 单纯的调用 JSFunction,不往 JS 传递参数,也不需要 JSFunction 的返回值。
[_jsBridge callHandler:@"changeBGColor"];
// 调用 JSFunction,并向 JS 传递参数,但不需要 JSFunciton 的返回值。
[_jsBridge callHandler:@"changeBGColor" data:@"把 HTML 的背景颜色改成橙色!!!!"];
// 调用 JSFunction ,并向 JS 传递参数,也需要 JSFunction 的返回值。
[_jsBridge callHandler:@"changeBGColor" data:@"传递给 JS 的参数" responseCallback:^(id responseData) {
NSLog(@"JS 的返回值: %@",responseData);
}];
JS 调用 OC 的三种情况。
// JS 单纯的调用 OC 的 block
WebViewJavascriptBridge.callHandler('scanClick');
// JS 调用 OC 的 block,并传递 JS 参数
WebViewJavascriptBridge.callHandler('scanClick',"JS 参数");
// JS 调用 OC 的 block,传递 JS 参数,并接受 OC 的返回值。
WebViewJavascriptBridge.callHandler('scanClick',{data : "这是 JS 传递到 OC 的扫描数据"},function(dataFromOC){
alert("JS 调用了 OC 的扫描方法!");
document.getElementById("returnValue").value = dataFromOC;
});
可以根据实际情况,选择合适的方法。
关于在 OC 中,往桥梁中注入 block 的注意点。
在当前控制器消失的时候,要记得把注入到桥梁中的 OC block,从桥梁中删除。
否则,可能会出现控制器无法释放的情况。
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[_jsBridge removeHandler:@"scanClick"];
[_jsBridge removeHandler:@"colorClick"];
[_jsBridge removeHandler:@"locationClick"];
[_jsBridge removeHandler:@"shareClick"];
[_jsBridge removeHandler:@"payClick"];
[_jsBridge removeHandler:@"goBackClick"];
}
最后总结:
- UIWebView & JavaScriptCore 等于原生的 JS & OC 交互方案。
- WKWebView & userContentController 等于原生了 JS & OC 交互方案。
- WebViewJavascriptBridge 可以搭配 UIWebView & WKWebView 进行 OC & JS 交互。
- WebViewJavascriptBridge 使用核心,OC 注入 OC 的方法,让 JS 调用。JS 注入 JS 函数,让 OC 调用。
- WebViewJavaScriptBridge 使用的需要 4个前提步骤。
推荐阅读
iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)
iOS的事件分为3大类型
- Touch Events(触摸事件)
- Motion Events(运动事件,比如重力感应和摇一摇等)
- Remote Events(远程事件,比如用耳机上得按键来控制手机)
在开发中,最常用到的就是Touch Events(触摸事件) ,基本贯穿于每个App中,也是本文的主角~ 因此文中所说事件均特指触摸事件。
接下来,记录、涉及的问题大致包括:
- 事件是怎么寻找事件的最佳响应者
- 事件的响应及在响应链中的传递
寻找事件的最佳响应者(Hit-Testing)
当我们触摸屏幕的某个可响应的功能点后,最终都会由UIView或者继承UIView的控件来响应
那我们先来看下UIView的两个方法:
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回寻找到的最终响应这个事件的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
//判断某一个点击的位置是否在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
每个UIView对象都有一个 hitTest: withEvent:
方法,这个方法是Hit-Testing
过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。
看看它是什么时候被调用的
- 当手指接触屏幕,UIApplication接收到手指的触摸事件之后,就会去调用UIWindow的
hitTest: withEvent:
方法 - 在
hitTest: withEvent:
方法中会调用pointInside: withEvent:
去判断当前点击的point是否属于UIWindow范围内,如果是,就会以倒序的方式遍历它的子视图,即越后添加的视图,越先遍历 - 子视图也调用自身的
hitTest: withEvent:
方法,来查找最终响应的视图
再来看个示例:
视图层级如下(同一层级的视图越在下面,表示越后添加):
A
├── B
│ └── D
└── C
├── E
└── F
现在假设在E视图所处的屏幕位置触发一个触摸,App接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:
- UIWindow将事件传递给其子视图A
- A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
- C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
- F判断自身不能响应事件,C又将事件传递给E。
- E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。
以上,就是寻找最佳响应者的整个过程。
接下来,来看下hitTest: withEvent:
方法里,都做些了什么?
我们已经知道事件在响应者之间的传递,是视图通过判断自身能否响应事件来决定是否继续向子视图传递,那么判断响应的条件是什么呢?
视图响应事件的条件:
- 允许交互:
userInteractionEnabled = YES
- 禁止隐藏:
hidden = NO
- 透明度:
alpha > 0.01
- 触摸点的位置:通过
pointInside: withEvent:
方法判断触摸点是否在视图的坐标范围内
代码的表现大概如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//3种状态无法响应事件
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event]) {
//从后往前遍历子视图数组
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint convertedPoint = [subView convertPoint:point fromView:self];
//询问子视图层级中的最佳响应视图
UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//如果子视图中有更合适的就返回
return hitTestView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}
return nil;
}
说了这么多,那我们可以运用hitTest: withEvent:
来搞些什么事情呢
使超出父视图坐标范围的子视图也能响应事件
视图层级如下:
css
A
├── B
如上图所示,视图B有一部分是不在父视图A的坐标范围内的,当我们触摸视图B的上半部分,是不会响应事件的。当然,我们可以通过重写视图A的 hitTest: withEvent:
方法来解决这个需求。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
//如果找不到合适的响应者
if (view == nil) {
//视图B坐标系的转换
CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];
if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {
// 满足条件,返回视图B
view = self.deleteButton;
}
}
return view;
}
在视图A的hitTest: withEvent:
方法中判断触摸点,是否位于视图B的视图范围内,如果属于,则返回视图B。这样一来,当我们点击视图B的任何位置都可以响应事件了。
事件的响应及在响应链中的传递
经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事情就是:
- 将事件传递给最佳响应者响应
- 事件沿着响应链传递
事件传递给最佳响应者
最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。
UIApplication中有个sendEvent:
的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给最佳响应者。
以寻找事件的最佳响应者一节中点击视图E为例,在EView的 touchesBegan:withEvent:
上打个断点查看调用栈就能看清这一过程:
当事件传递给最佳响应者后,响应者响应这个事件,则这个事件到此就结束了,它会被释放。假设响应者没有响应这个事件,那么它将何去何从?事件将会沿着响应链自上而下传递。
注意: 寻找最佳响应者一节中也说到了事件的传递,与此处所说的事件的传递有本质区别。上面所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上(父视图到子视图)的传递;而这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下(子视图到父视图)的。前者为“寻找”,后者为“响应”。
事件沿着响应链传递
在UIKit中有一个类:UIResponder,它是所有可以响应事件的类的基类。来看下它的头文件的几个属性和方法
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif
--------------省略部分代码------------
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
UIApplication,UIViewController和UIView都是继承自它,都有一个 nextResponder
方法,用于获取响应链中当前对象的下一个响应者,也通过nextResponder
来串成响应链。
在App中,所有的视图都是根据树状层次结构组织起来的,因此,每个View都有自己的SuperView。当一个View被add到SuperView上的时候,它的nextResponder
属性就会被指向它的SuperView,各个不同响应者的指向如下:
- UIView 若视图是控制器的根视图,则其
nextResponder
为控制器对象;否则,其nextResponder
为父视图。 - UIViewController 若控制器的视图是window的根视图,则其
nextResponder
为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder
为presenting view controller。 - UIWindow
nextResponder
为UIApplication对象。 - UIApplication 若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的
nextResponder
为app delegate。
这样,整个App就通过nextResponder
串成了一条链,也就是我们所说的响应链,子视图指向父视图构成的响应链。
看一下官网对于响应链的示例展示
若触摸发生在UITextField上,则事件的传递顺序是:
- UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte
图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponder
为UIViewController对象;若是直接add在UIWindow上的,则其nextResponder
为UIWindow对象。
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent:
方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
响应者对于接收到的事件有3种操作:
- 不拦截,默认操作 事件会自动沿着默认的响应链往下传递
- 拦截,不再往下分发事件 重写
touchesBegan:withEvent:
进行事件处理,不调用父类的touchesBegan:withEvent:
- 拦截,继续往下分发事件 重写
touchesBegan:withEvent:
进行事件处理,同时调用父类的touchesBegan:withEvent:
将事件往下传递
因此,你也可以通过 touchesBegan:withEvent:
方法搞点事情~
总结
触摸事件先通过自下而上(父视图-->子视图)的传递方式寻找最佳响应者,
然后以自上而下(子视图-->父视图)的方式在响应链中传递。
iOS多环境配置(转)
基本概念
- Project:包含了项⽬所有的代码,资源⽂件,所有信息。
- Target:对指定代码和资源⽂件的具体构建方式。
- Scheme:对指定Target的环境配置。
通过不同的方式配置多环境
通过Duplicate Target方式配置
- 通过上述操作之后会多生成一个target,也会多出来一个info.plist
-
可以定义一个宏的方式,对代码做一些差异化的配置
-
oc在
build setting
->Preprocessor Macros
-
swift在
build setting
->Other Swift Flags
,需要注意的是swift
中使用宏需要加上-D
参数 -
这种方式配置起来缺点
- 生成多个info.plist
- 需要配置的点比较多,比较乱
通过配置Scheme的方式
- 再通过
Edit Scheme
->Manage Schemes
新增不同环境的scheme - 再将不同的
Scheme
对应到不同的Build Configuration
模式下
使用场景举例
- 在日常开发中,不同环境下的
host url
的地址都会有不同的差异,通过定义一个宏的方式
- 再通过
info.plist
文件暴露出来
- 使用方式如下
NSString *path = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@".plist"];
NSDictionary *infoDict = [[NSDictionary alloc] initWithContentsOfFile:path];
NSLog(@"host url:%@",infoDict[@"HOST_URL"]);
复制代码
xcconfig文件
xcconfig指南
- xcconfig文件的语法比较简单,每个配置文件都由一系列键值分配组成,这些键值分配具有以下语法:
BUILD_SETTING_NAME = value
复制代码
-
Build Setting
value对应字段的缩写,Build Setting字段对应查询
注释
- xcconfig文件只有一种注释方式\。
include导入其他设置
- 在创建xcconfig文件的时候,可以根据需求,创建多个。也就意味着,可以通过include关键字导入其他的xcconfig内的配置。通过include关键字后接上双引号:
#include "Debug.xcconfig"
复制代码
- 在搜索引入的文件时,如果是以/开头,代表绝对路径,例如:
// 表示确切的文件位置
#include "/Users/xx/Desktop/.../xxx.xcconfig"
复制代码
- 或者通过相对路径,以${SRCROOT}路径为开始:
#include "Pods/Target Support Files/xxx.xcconfig"
复制代码
变量
-
变量定义,按照OC命名规则,仅由大写字母,数字和下划线
(_)
组成,原则上大写,也可以不。字符串可以是"
也可以是'
号。 -
变量有三种特殊情况:
-
- 在
xcconfig
中定义的变量与Build Settings
的一致,那么会发生覆盖。可以通过$(inherited)
,让当前变量继承变量原有值。例如:
- 在
OTHER_LDFLAGS = -framework SDWebImage OTHER_LDFLAGS = $(inherited) -framework AFNetworking // OTHER_LDFLAGS = -framework SDWebImage -framework AFNetworking 复制代码
注意⚠️:有部分变量不能通过xcconfig配置到Build Settings中,例如:配 置PRODUCT_BUNDLE_IDENTIFIER不起作用。 复制代码
-
- 引用变量,
$()
和${}
两种写法都可以:VALUE=value
- 引用变量,
TEACHER=$(VALUE)-${VALUE} 复制代码
-
- 条件变量,根据
SDK
、Arch
和Configration
对设置进行条件化,例如:
- 条件变量,根据
// 指定`Configration`是`Debug` // 指定`SDK`是模拟器,还有iphoneos*、macosx*等 // 指定生效架构为`x86_64` OTHER_LDFLAGS[config=Debug][sdk=iphonesimulator*[arch=x86_64]= $(inherited) -framework "Cat" 复制代码
注意⚠️:在Xcode 11.4及以后版本,可以使用default,来指定变量为空时的默认值: $(BUILD_SETTING_NAME:default=value) 复制代码
-
优先级(由高到低):
-
- 手动配置Target Build Settings
-
- Target中配置的xcconfig文件
-
- 手动配置Project Build Settings
-
- Project中配置的xcconfig文件
iOS蓝牙知识快速入门(详尽版)(转)
以前写过几篇蓝牙相关的文章,但是没有涉及扫描、收发指令这些基础功能的实现。所以打算写一篇尽可能详尽的蓝牙知识汇总,一方面给有需要的同学看,一方面是对自己学习蓝牙的一个总结。
这篇文章的目的:教你实现设备的扫描,连接,数据收发,蓝牙数据解析。如果在实现上面任一功能遇到问题时,欢迎留下你的问题,我将进行补充,对于说法有误的地方也请老司机予以指正。
目录
思维导图
第一次做图,大家凑合着看哈。这张是我总结的蓝牙知识的结构图,下面的内容将围绕这些东西展开进行。
这张是蓝牙连接发送数据的流程图,下文进入coding阶段的讲解顺序,大家先有个大概印象,等阅读完本文再回来看这张图将理解的更深一些。
苹果对蓝牙设备有什么要求
BLE:bluetouch low energy,蓝牙4.0设备因为低功耗,所有也叫作BLE。苹果在iphone4s及之后的手机型号开始支持蓝牙4.0,这也是最常见的蓝牙设备。低于蓝牙4.0协议的设备需要进行MFI认证,关于MFI认证的申请工作可以看这里:关于MFI认证你所必须要知道的事情
在进行操作蓝牙设备前,我们先下载一个蓝牙工具LightBlue
,它可以辅助我们的开发,在进行蓝牙开发之前建议先熟悉一下LightBlue这个工具。
操作蓝牙设备使用什么库
苹果自身有一个操作蓝牙的库CoreBluetooth.framework
,这个是大多数人员进行蓝牙开发的首选框架,除此之外目前github还有一个比较流行的对原生框架进行封装的三方库BabyBluetooth,它的机制是将CoreBluetooth中众多的delegate写成了block方法,有兴趣的同学可以了解下。下面主要介绍的是原生蓝牙库的知识。
中心和外围设备
如图所示,电脑、Pad、手机作为中心,心跳监听器作为外设,这种中心外设模式是最常见的。简单理解就是,发起连接的是中心设备(Central),被连接的是外围设备(Peripheral),对应传统的客户机-服务器体系结构。Central能够扫描侦听到,正在播放广告包的外设。
服务与特征
外设可以包含一个或多个服务(CBService),服务是用于实现装置的功能或特征数据相关联的行为集合。 而每个服务又对应多个特征(CBCharacteristic),特征提供外设服务进一步的细节,外设,服务,特征对应的数据结构如下所示
如何扫描蓝牙
在进行扫描之前我们需要,首先新建一个类作为蓝牙类,例如FYBleManager
,写成单例,作为处理蓝牙操作的管理类。引入头文件#import <CoreBluetooth/CoreBluetooth.h>
CBCentralManager
是蓝牙中心的管理类,控制着蓝牙的扫描,连接,蓝牙状态的改变。
1、初始化
dispatch_queue_t centralQueue = dispatch_queue_create(“centralQueue",DISPATCH_QUEUE_SERIAL);
NSDictionary *dic = @{
CBCentralManagerOptionShowPowerAlertKey : YES,
CBCentralManagerOptionRestoreIdentifierKey : @"unique identifier"
};
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:dic];
-
CBCentralManagerOptionShowPowerAlertKey
对应的BOOL值,当设为YES时,表示CentralManager初始化时,如果蓝牙没有打开,将弹出Alert提示框 -
CBCentralManagerOptionRestoreIdentifierKey
对应的是一个唯一标识的字符串,用于蓝牙进程被杀掉恢复连接时用的。
2、扫描
//不重复扫描已发现设备
NSDictionary *option = @{
CBCentralManagerScanOptionAllowDuplicatesKey : [NSNumber numberWithBool:NO],
CBCentralManagerOptionShowPowerAlertKey:YES
};
[self.centralManager scanForPeripheralsWithServices:nil options:option];
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;
- 扫面方法
-
serviceUUIDs
用于第一步的筛选,扫描此UUID的设备 - options有两个常用参数:
-
CBCentralManagerScanOptionAllowDuplicatesKey
- 设置为NO表示不重复扫瞄已发现设备,为YES就是允许
CBCentralManagerOptionShowPowerAlertKey
- 设置为YES就是在蓝牙未打开的时候显示弹框
-
-
3、CBCentralManagerDelegate代理方法
在初始化的时候我们调用了代理,在CoreBluetooth中有两个代理,
- CBCentralManagerDelegate
- CBPeripheralDelegate
iOS的命名很友好,我们通过名字就能看出,上面那个是关于中心设备的代理方法,下面是关于外设的代理方法。
我们这里先研究CBCentralManagerDelegate
中的代理方法
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
这个方法标了@required
是必须添加的,我们在self.centralManager
初始换之后会调用这个方法,回调蓝牙的状态。状态有以下几种:
typedef NS_ENUM(NSInteger, CBCentralManagerState{
CBCentralManagerStateUnknown = CBManagerStateUnknown,//未知状态
CBCentralManagerStateResetting = CBManagerStateResetting,//重启状态
CBCentralManagerStateUnsupported = CBManagerStateUnsupported,//不支持
CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized,//未授权
CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff,//蓝牙未开启
CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn,//蓝牙开启} NS_DEPRECATED(NA, NA, 5_0, 10_0, "Use CBManagerState instead”
);
该枚举在iOS10之后已经废除了,系统推荐使用CBManagerState
,类型都是对应的
typedef NS_ENUM(NSInteger, CBManagerState{
CBManagerStateUnknown = 0,
CBManagerStateResetting,
CBManagerStateUnsupported,
CBManagerStateUnauthorized,
CBManagerStatePoweredOff,
CBManagerStatePoweredOn,
} NS_ENUM_AVAILABLE(NA, 10_0);
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;
- peripheral是外设类
-
advertisementData
是广播的值,一般携带设备名,serviceUUIDs
等信息 - RSSI绝对值越大,表示信号越差,设备离的越远。如果想转换成百分比强度,(RSSI+100)/100,(这是一个约数,蓝牙信号值并不一定是-100 - 0的值,但近似可以如此表示)
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict;
在蓝牙于后台被杀掉时,重连之后会首先调用此方法,可以获取蓝牙恢复时的各种状态
如何连接
在扫面的代理方法中,我们连接外设名是MI的蓝牙设备
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{
NSLog(@"advertisementData:%@,RSSI:%@",advertisementData,RSSI);
if([peripheral.name isEqualToString:@"MI"]){
[self.centralManager connectPeripheral:peripheral options:nil];//发起连接的命令
self.peripheral = peripheral;
}
}
连接的状态 对应另外的CBCentralManagerDelegate
代理方法 连接成功的回调
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
连接失败的回调
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
连接断开的回调
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
连接成功之后并没有结束,还记得CBPeripheral
中的CBService
和CBService
中的CBCharacteristic
吗,对数据的读写是由CBCharacteristic
控制的。
我们先用lightblue连接小米手环为例,来看一下,手环内部的数据是不是我们说的那样。
其中ADVERTISEMENT DATA
显示的就是广播信息。
iOS蓝牙无法直接获取设备蓝牙MAC地址,可以将MAC地址放到这里广播出来
FEEO
是ServiceUUIDs
,里面的FF01
、FF02
是CBCharacteristic的UUID
Properties
是特征的属性,可以看出FF01
具有读的权限,FF02
具有读写的权限。
特征拥有的权限类别有如下几种:
typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties{
CBCharacteristicPropertyBroadcast = 0x01,
CBCharacteristicPropertyRead = 0x02,
CBCharacteristicPropertyWriteWithoutResponse = 0x04,
CBCharacteristicPropertyWrite = 0x08,
CBCharacteristicPropertyNotify = 0x10,
CBCharacteristicPropertyIndicate = 0x20,
CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
CBCharacteristicPropertyExtendedProperties = 0x80,
CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100,
CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200
};
如何发送并接收数据
通过上面的步骤我们发现CBCentralManagerDelegate
提供了蓝牙状态监测、扫描、连接的代理方法,但是CBPeripheralDelegate
的代理方法却还没使用。别急,马上就要用到了,通过名称判断这个代理的作用,肯定是跟Peripheral
有关,我们进入系统API,看它的代理方法都有什么,因为这里的代理方法较多,我就挑选几个常用的拿出来说明一下。
1、代理方法
//发现服务的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;
//发现特征的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;
//读数据的回调
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
//是否写入成功的回调
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
2、步骤
通过这几个方法我们构建一个流程:连接成功->获取指定的服务->获取指定的特征->订阅指定特征值->通过具有写权限的特征值写数据->在didUpdateValueForCharacteristic
回调中读取蓝牙反馈值
解释一下订阅特征值:特征值具有Notify权限才可以进行订阅,订阅之后该特征值的value发生变化才会回调didUpdateValueForCharacteristic
3、实现上面流程的实例代码
//连接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
//连接成功之后寻找服务,传nil会寻找所有服务
[peripheral discoverServices:nil];
}
//发现服务的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
if (!error) {
for (CBService *service in peripheral.services) {
NSLog(@"serviceUUID:%@", service.UUID.UUIDString);
if ([service.UUID.UUIDString isEqualToString:ST_SERVICE_UUID]) {
//发现特定服务的特征值
[service.peripheral discoverCharacteristics:nil forService:service];
}
}
}
}
//发现characteristics,由发现服务调用(上一步),获取读和写的characteristics
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
for (CBCharacteristic *characteristic in service.characteristics) {
//有时读写的操作是由一个characteristic完成
if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]) {
self.read = characteristic;
[self.peripheral setNotifyValue:YES forCharacteristic:self.read];
} else if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_WRITE]) {
self.write = characteristic;
}
}
}
//是否写入成功的代理
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
if (error) {
NSLog(@"===写入错误:%@",error);
}else{
NSLog(@"===写入成功");
}
}
//数据接收
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]){ //获取订阅特征回复的数据
NSData *value = characteristic.value;
NSLog(@"蓝牙回复:%@",value);
}
}
比如我们要获取蓝牙电量,由硬件文档查询得知该指令是 0x1B9901
,那么获取电量的方法就可以写成
- (void)getBattery{
Byte value[3]={0};
value[0]=x1B;
value[1]=x99;
value[2]=x01;
NSData * data = [NSData dataWithBytes:&value length:sizeof(value)];
//发送数据
[self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];
}
如果写入成功,我们将会在didUpdateValueForCharacteristic
方法中获取蓝牙回复的信息。
如何解析蓝牙数据
如果你顺利完成了上一步的操作,并且看到了蓝牙返回的数据,那么恭喜你,蓝牙的常用操作你已经了解大半了。因为蓝牙的任务大部分就是围绕发送指令,获取指令,将蓝牙数据呈现给用户。上一步我们已经获取了蓝牙指令,但是获取的却是0x567b0629
这样的数据,这是什么意思呢。这时我们参考硬件协议文档,看到这样一段:
那么我们就可以得出设备电量是 60%。
对数据解析的流程就是:
- 判断校验和是否正确
- 是不是一条正确的数据->该条数据是不是我们需要的电量数据
- 即首字节为
0x567b
->根据定义规则解析电量,传给view显示。
- 其中第一步校验数据,视情况而定,也有不需要的情况。