Apple 的照片应用程序中的过滤器 UIScrollView/UICollectionView 是如何实现的,以至于打开速度如此之快?

How is filters UIScrollView/UICollectionView in Apple's Photos app implemented that it opens so fast?

我问的不是确切的代码,而是总体思路。

这是我的问题:我正在尝试创建类似于在照片应用中选择 UI 过滤器的东西。我尝试了多种方法,但所有方法都有其缺点。

1) 我试过将 OperationOperationQueue 与集合视图一起使用,启用了预取。这会快速加载 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])

       }
  }

现在我只使用一个过滤器。 SepiaCIFilter 的子类。我将它创建为一个子类,因为将来我要从中创建一个自定义的。这是它的实现:

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()
    }
}