Apple 的照片应用程序中的过滤器 UIScrollView/UICollectionView 是如何实现的,以至于打开速度如此之快?
How is filters UIScrollView/UICollectionView in Apple's Photos app implemented that it opens so fast?
我问的不是确切的代码,而是总体思路。
这是我的问题:我正在尝试创建类似于在照片应用中选择 UI 过滤器的东西。我尝试了多种方法,但所有方法都有其缺点。
1) 我试过将 Operation
和 OperationQueue
与集合视图一起使用,启用了预取。这会快速加载 viewController 但在滚动时会掉帧。
2) 现在我正在使用滚动视图和 GCD
但它加载 viewController 的时间太长(因为它会同时将所有过滤器应用于其中的所有按钮),但是然后它顺利滚动。
注意:要回答这个问题,没有必要阅读下面的部分(我相信),但是如果您对我如何尝试实现功能感兴趣,您欢迎阅读
为了实现所有过滤器,我使用了一个名为 Filters
的结构,它负责启动每个过滤器并将其附加到数组。
struct Filters {
var image: UIImage
var allFilters: [CIFilter] = []
init(image: UIImage) {
self.image = image
guard let sepia = Sepia(image: image) else {return}
allFilters.append(contentsOf: [sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia])
}
}
现在我只使用一个过滤器。 Sepia
是 CIFilter
的子类。我将它创建为一个子类,因为将来我要从中创建一个自定义的。这是它的实现:
class Sepia: CIFilter {
var inputImage: CIImage?
var inputIntensity: NSNumber?
@objc override var filterName: String? {
return NSLocalizedString("Sepia", comment: "Name of a Filter")
}
convenience init?(image: UIImage, inputIntensity: NSNumber? = nil) {
self.init()
guard let cgImage = image.cgImage else {
return nil
}
if inputIntensity != nil {
self.inputIntensity = inputIntensity
} else {
self.setDefaults()
}
let inputImage = CIImage(cgImage: cgImage)
self.inputImage = inputImage
}
override func setDefaults() {
inputIntensity = 1.0
}
override var outputImage: CIImage? {
guard let inputImage = inputImage, let inputIntensity = inputIntensity else {
return nil
}
let filter = CIFilter(name: "CISepiaTone", withInputParameters: [kCIInputImageKey: inputImage, kCIInputIntensityKey: inputIntensity])
return filter?.outputImage
}
}
在 viewController 的 viewDidLoad
中,我启动 Filters
结构:
self.filters = Filters(image: image)
然后我调用一个方法,该方法根据 filters.allFilters
数组中过滤器的数量配置一些视图 (filterViews
) 并遍历它们并调用一个获取缩略图的方法 UIImage
并对其应用过滤器,然后在完成处理程序中 returns 它(出于调试原因,我在其中使用 DispatchGroup
)。这是将过滤器应用于缩略图的方法:
func imageWithFilter(filter: CIFilter, completion: @escaping(UIImage?)->Void) {
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
guard let outputImage = filter.value(forKey: kCIOutputImageKey) as? CIImage, let cgImageResult = self.context.createCGImage(outputImage, from: outputImage.extent) else {
DispatchQueue.main.async {
completion(nil)
}
group.leave()
return
}
let filteredImage = UIImage(cgImage: cgImageResult)
DispatchQueue.main.async {
print (filteredImage)
completion(filteredImage)
}
group.leave()
}
group.notify(queue: .main) {
print ("Filteres are set")
}
}
上面的打印语句和过滤后的图片地址很快就打印出来了,但是图片并没有出现在视图中。
我曾尝试使用 Time Profiler
,但它给了我一些奇怪的结果。例如,它显示以下内容在回溯的根目录中执行需要很长时间:
当我尝试查看 Xcode 中的代码时,我得到以下信息,但没有太大帮助:
所以,这就是问题所在。如果您对它在照片应用程序中如何实现如此快速和响应有任何想法,或者如果您对我的实现有任何建议,我将非常感谢您的帮助。
问题似乎是如何尽可能快地显示从 Core Image CIFilter 生成的 CIImage——快到当视图控制器出现时它立即出现;如此之快,事实上,用户可以使用滑块等调整 CIFilter 参数,图像将实时重新显示并跟上调整。
答案是使用 Metal Kit,尤其是 MTKView。渲染工作转移到设备的 GPU 上,速度非常快,速度足以在设备屏幕的刷新率下进行,因此当用户旋转滑块时没有明显的延迟。
我有一个简单的演示,其中用户应用了一个名为 VignetteFilter 的自定义过滤器链:
随着用户滑动滑块,渐晕量(内圈)平滑变化。在滑动的每个瞬间,一个新的滤镜被应用到原始图像,并随着用户滑动滑块一遍又一遍地渲染滤镜,与用户的动作保持同步。
正如我所说,底部的视图是一个 MTKView。以这种方式使用 MTKView 并不难;它确实需要一些准备,但都是样板文件。唯一棘手的部分实际上是让图像出现在您想要的位置。
这是我的视图控制器的代码(除了滑块和过滤图像的显示,我省略了所有内容):
class EditingViewController: UIViewController, MTKViewDelegate {
@IBOutlet weak var slider: UISlider!
@IBOutlet weak var mtkview: MTKView!
var context : CIContext!
let displayImage : CIImage! // must be set before viewDidLoad
let vig = VignetteFilter()
var queue: MTLCommandQueue!
// slider value changed
@IBAction func doSlider(_ sender: Any?) {
self.mtkview.setNeedsDisplay()
}
override func viewDidLoad() {
super.viewDidLoad()
// preparation, all pure boilerplate
self.mtkview.isOpaque = false // otherwise background is black
// must have a "device"
guard let device = MTLCreateSystemDefaultDevice() else {
return
}
self.mtkview.device = device
// mode: draw on demand
self.mtkview.isPaused = true
self.mtkview.enableSetNeedsDisplay = true
self.context = CIContext(mtlDevice: device)
self.queue = device.makeCommandQueue()
self.mtkview.delegate = self
self.mtkview.setNeedsDisplay()
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
// run the displayImage thru the CIFilter
self.vig.setValue(self.displayImage, forKey: "inputImage")
let val = Double(self.slider.value)
self.vig.setValue(val, forKey:"inputPercentage")
var output = self.vig.outputImage!
// okay, `output` is the CIImage we want to display
// scale it down to aspect-fit inside the MTKView
var r = view.bounds
r.size = view.drawableSize
r = AVMakeRect(aspectRatio: output.extent.size, insideRect: r)
output = output.transformed(by: CGAffineTransform(
scaleX: r.size.width/output.extent.size.width,
y: r.size.height/output.extent.size.height))
let x = -r.origin.x
let y = -r.origin.y
// minimal dance required in order to draw: render, present, commit
let buffer = self.queue.makeCommandBuffer()!
self.context!.render(output,
to: view.currentDrawable!.texture,
commandBuffer: buffer,
bounds: CGRect(origin:CGPoint(x:x, y:y), size:view.drawableSize),
colorSpace: CGColorSpaceCreateDeviceRGB())
buffer.present(view.currentDrawable!)
buffer.commit()
}
}
我问的不是确切的代码,而是总体思路。
这是我的问题:我正在尝试创建类似于在照片应用中选择 UI 过滤器的东西。我尝试了多种方法,但所有方法都有其缺点。
1) 我试过将 Operation
和 OperationQueue
与集合视图一起使用,启用了预取。这会快速加载 viewController 但在滚动时会掉帧。
2) 现在我正在使用滚动视图和 GCD
但它加载 viewController 的时间太长(因为它会同时将所有过滤器应用于其中的所有按钮),但是然后它顺利滚动。
注意:要回答这个问题,没有必要阅读下面的部分(我相信),但是如果您对我如何尝试实现功能感兴趣,您欢迎阅读
为了实现所有过滤器,我使用了一个名为 Filters
的结构,它负责启动每个过滤器并将其附加到数组。
struct Filters {
var image: UIImage
var allFilters: [CIFilter] = []
init(image: UIImage) {
self.image = image
guard let sepia = Sepia(image: image) else {return}
allFilters.append(contentsOf: [sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia])
}
}
现在我只使用一个过滤器。 Sepia
是 CIFilter
的子类。我将它创建为一个子类,因为将来我要从中创建一个自定义的。这是它的实现:
class Sepia: CIFilter {
var inputImage: CIImage?
var inputIntensity: NSNumber?
@objc override var filterName: String? {
return NSLocalizedString("Sepia", comment: "Name of a Filter")
}
convenience init?(image: UIImage, inputIntensity: NSNumber? = nil) {
self.init()
guard let cgImage = image.cgImage else {
return nil
}
if inputIntensity != nil {
self.inputIntensity = inputIntensity
} else {
self.setDefaults()
}
let inputImage = CIImage(cgImage: cgImage)
self.inputImage = inputImage
}
override func setDefaults() {
inputIntensity = 1.0
}
override var outputImage: CIImage? {
guard let inputImage = inputImage, let inputIntensity = inputIntensity else {
return nil
}
let filter = CIFilter(name: "CISepiaTone", withInputParameters: [kCIInputImageKey: inputImage, kCIInputIntensityKey: inputIntensity])
return filter?.outputImage
}
}
在 viewController 的 viewDidLoad
中,我启动 Filters
结构:
self.filters = Filters(image: image)
然后我调用一个方法,该方法根据 filters.allFilters
数组中过滤器的数量配置一些视图 (filterViews
) 并遍历它们并调用一个获取缩略图的方法 UIImage
并对其应用过滤器,然后在完成处理程序中 returns 它(出于调试原因,我在其中使用 DispatchGroup
)。这是将过滤器应用于缩略图的方法:
func imageWithFilter(filter: CIFilter, completion: @escaping(UIImage?)->Void) {
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
guard let outputImage = filter.value(forKey: kCIOutputImageKey) as? CIImage, let cgImageResult = self.context.createCGImage(outputImage, from: outputImage.extent) else {
DispatchQueue.main.async {
completion(nil)
}
group.leave()
return
}
let filteredImage = UIImage(cgImage: cgImageResult)
DispatchQueue.main.async {
print (filteredImage)
completion(filteredImage)
}
group.leave()
}
group.notify(queue: .main) {
print ("Filteres are set")
}
}
上面的打印语句和过滤后的图片地址很快就打印出来了,但是图片并没有出现在视图中。
我曾尝试使用 Time Profiler
,但它给了我一些奇怪的结果。例如,它显示以下内容在回溯的根目录中执行需要很长时间:
当我尝试查看 Xcode 中的代码时,我得到以下信息,但没有太大帮助:
所以,这就是问题所在。如果您对它在照片应用程序中如何实现如此快速和响应有任何想法,或者如果您对我的实现有任何建议,我将非常感谢您的帮助。
问题似乎是如何尽可能快地显示从 Core Image CIFilter 生成的 CIImage——快到当视图控制器出现时它立即出现;如此之快,事实上,用户可以使用滑块等调整 CIFilter 参数,图像将实时重新显示并跟上调整。
答案是使用 Metal Kit,尤其是 MTKView。渲染工作转移到设备的 GPU 上,速度非常快,速度足以在设备屏幕的刷新率下进行,因此当用户旋转滑块时没有明显的延迟。
我有一个简单的演示,其中用户应用了一个名为 VignetteFilter 的自定义过滤器链:
随着用户滑动滑块,渐晕量(内圈)平滑变化。在滑动的每个瞬间,一个新的滤镜被应用到原始图像,并随着用户滑动滑块一遍又一遍地渲染滤镜,与用户的动作保持同步。
正如我所说,底部的视图是一个 MTKView。以这种方式使用 MTKView 并不难;它确实需要一些准备,但都是样板文件。唯一棘手的部分实际上是让图像出现在您想要的位置。
这是我的视图控制器的代码(除了滑块和过滤图像的显示,我省略了所有内容):
class EditingViewController: UIViewController, MTKViewDelegate {
@IBOutlet weak var slider: UISlider!
@IBOutlet weak var mtkview: MTKView!
var context : CIContext!
let displayImage : CIImage! // must be set before viewDidLoad
let vig = VignetteFilter()
var queue: MTLCommandQueue!
// slider value changed
@IBAction func doSlider(_ sender: Any?) {
self.mtkview.setNeedsDisplay()
}
override func viewDidLoad() {
super.viewDidLoad()
// preparation, all pure boilerplate
self.mtkview.isOpaque = false // otherwise background is black
// must have a "device"
guard let device = MTLCreateSystemDefaultDevice() else {
return
}
self.mtkview.device = device
// mode: draw on demand
self.mtkview.isPaused = true
self.mtkview.enableSetNeedsDisplay = true
self.context = CIContext(mtlDevice: device)
self.queue = device.makeCommandQueue()
self.mtkview.delegate = self
self.mtkview.setNeedsDisplay()
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
// run the displayImage thru the CIFilter
self.vig.setValue(self.displayImage, forKey: "inputImage")
let val = Double(self.slider.value)
self.vig.setValue(val, forKey:"inputPercentage")
var output = self.vig.outputImage!
// okay, `output` is the CIImage we want to display
// scale it down to aspect-fit inside the MTKView
var r = view.bounds
r.size = view.drawableSize
r = AVMakeRect(aspectRatio: output.extent.size, insideRect: r)
output = output.transformed(by: CGAffineTransform(
scaleX: r.size.width/output.extent.size.width,
y: r.size.height/output.extent.size.height))
let x = -r.origin.x
let y = -r.origin.y
// minimal dance required in order to draw: render, present, commit
let buffer = self.queue.makeCommandBuffer()!
self.context!.render(output,
to: view.currentDrawable!.texture,
commandBuffer: buffer,
bounds: CGRect(origin:CGPoint(x:x, y:y), size:view.drawableSize),
colorSpace: CGColorSpaceCreateDeviceRGB())
buffer.present(view.currentDrawable!)
buffer.commit()
}
}