2017-03-02 5 views
2

Ich habe this example von Apple gefolgt, um ein sehr großes Bild zu verkleinern Ich bin downloading from a remote location. Ich habe den Code in Swift neu geschrieben. Es funktioniert anscheinend, aber wenn ich MTKTextureLoader.newTexture rufe, stürzt die Anwendung mit einem EXC_BAD_ACCESS in _loadCGImage ab. Es gibt keinen anderen Hinweis, aber ich vermute, die Bilddaten wurden bereits freigegeben oder etwas ...Verkleinern sehr großes Bild zu MTLTexture

Alle Hinweise, warum würde es ohne eine richtige Fehlermeldung abstürzen?

Dies ist der Top-Level-Code ist

// this is an extension of MTKTextureLoader 
// [...] 
if let uiImage = UIImage(contentsOfFile: cachedFileURL.path) { 
    let maxDim : CGFloat = 8192 
    if uiImage.size.width > maxDim || uiImage.size.height > maxDim { 
     let scale = uiImage.size.width > maxDim ? maxDim/uiImage.size.width : maxDim/uiImage.size.height 
     if let cgImage = MTKTextureLoader.downsize(image: uiImage, scale: scale) { 
      return self.newTexture(with: cgImage, options: options, completionHandler: completionHandler) 
     } else { 
      anError = TextureError.CouldNotDownsample 
     } 
    } else { 
     return self.newTexture(withContentsOf: cachedFileURL, options: options, completionHandler: completionHandler) 
    } 
} 

Und die downsize Methode,

private static func downsize(image: UIImage, scale: CGFloat) -> CGImage? { 
    let destResolution = CGSize(width: Int(image.size.width * scale), height: Int(image.size.height * scale)) 
    let kSourceImageTileSizeMB : CGFloat = 40.0 // The tile size will be (x)MB of uncompressed image data 
    let pixelsPerMB = 262144 
    let tileTotalPixels = kSourceImageTileSizeMB * CGFloat(pixelsPerMB) 
    let destSeemOverlap : CGFloat = 2.0 // the numbers of pixels to overlap the seems where tiles meet. 
    let colorSpace = CGColorSpaceCreateDeviceRGB() 
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) 
    guard let destContext = CGContext(data: nil, width: Int(destResolution.width), height: Int(destResolution.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { 
     NSLog("failed to create the output bitmap context!") 
     return nil 
    } 
    var sourceTile = CGRect() 
    sourceTile.size.width = image.size.width 
    sourceTile.size.height = floor(tileTotalPixels/sourceTile.size.width) 
    print("source tile size: \(sourceTile.size)") 
    sourceTile.origin.x = 0.0 
    // the output tile is the same proportions as the input tile, but 
    // scaled to image scale. 
    var destTile = CGRect() 
    destTile.size.width = destResolution.width 
    destTile.size.height = sourceTile.size.height * scale 
    destTile.origin.x = 0.0 
    print("dest tile size: \(destTile.size)") 
    // the source seem overlap is proportionate to the destination seem overlap. 
    // this is the amount of pixels to overlap each tile as we assemble the output image. 
    let sourceSeemOverlap : CGFloat = floor((destSeemOverlap/destResolution.height) * image.size.height) 
    print("dest seem overlap: \(destSeemOverlap), source seem overlap: \(sourceSeemOverlap)") 
    // calculate the number of read/write operations required to assemble the 
    // output image. 
    var iterations = Int(image.size.height/sourceTile.size.height) 
    // if tile height doesn't divide the image height evenly, add another iteration 
    // to account for the remaining pixels. 
    let remainder = Int(image.size.height) % Int(sourceTile.size.height) 
    if remainder > 0 { 
     iterations += 1 
    } 
    // add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. 
    let sourceTileHeightMinusOverlap = sourceTile.size.height 
    sourceTile.size.height += sourceSeemOverlap 
    destTile.size.height += destSeemOverlap 
    print("beginning downsize. iterations: \(iterations), tile height: \(sourceTile.size.height), remainder height: \(remainder)") 
    for y in 0..<iterations { 
     // create an autorelease pool to catch calls to -autorelease made within the downsize loop. 
     autoreleasepool { 
      print("iteration \(y+1) of \(iterations)") 
      sourceTile.origin.y = CGFloat(y) * sourceTileHeightMinusOverlap + sourceSeemOverlap 
      destTile.origin.y = (destResolution.height) - (CGFloat(y + 1) * sourceTileHeightMinusOverlap * scale + destSeemOverlap) 
      // create a reference to the source image with its context clipped to the argument rect. 
      if let sourceTileImage = image.cgImage?.cropping(to: sourceTile) { 
       // if this is the last tile, its size may be smaller than the source tile height. 
       // adjust the dest tile size to account for that difference. 
       if y == iterations - 1 && remainder > 0 { 
        var dify = destTile.size.height 
        destTile.size.height = CGFloat(sourceTileImage.height) * scale 
        dify -= destTile.size.height 
        destTile.origin.y += dify 
       } 
       // read and write a tile sized portion of pixels from the input image to the output image. 
       destContext.draw(sourceTileImage, in: destTile, byTiling: false) 
      } 
      // !!! In the original LargeImageDownsizing code, it released the source image here !!! 
      // [image release]; 
      // !!! I assume I don't need to do that in Swift?? !!! 
      /* while CGImageCreateWithImageInRect lazily loads just the image data defined by the argument rect, 
      that data is finally decoded from disk to mem when CGContextDrawImage is called. sourceTileImageRef 
      maintains internally a reference to the original image, and that original image both, houses and 
      caches that portion of decoded mem. Thus the following call to release the source image. */ 
      // http://en.swifter.tips/autoreleasepool/ 
      // drain will be called 
      // to free all objects that were sent -autorelease within the scope of this loop. 
     } 
     // !!! Original code reallocated the image here !!! 
     // we reallocate the source image after the pool is drained since UIImage -imageNamed 
     // returns us an autoreleased object. 
    } 
    print("downsize complete.") 
    // create a CGImage from the offscreen image context 
    return destContext.makeImage() 
} 

Edit:

Es stürzt ich jedes Mal versuchen, die MTLTexture mit einem initialisieren CGImage, auch wenn das Bild klein ist und in den Speicher passt. So scheint es in keinem Zusammenhang mit der Redimensionierung ... auch Dieser Code abstürzt,

func newTexture(with uiImage: UIImage, options: [String : NSObject]? = nil, completionHandler: @escaping MTKTextureLoaderCallback) { 
    if let cgImage = uiImage.cgImage { 
     return self.newTexture(with: cgImage, options: options, completionHandler: completionHandler) 
    } else { 
     completionHandler(nil, TextureError.CouldNotBeCreated) 
    } 
} 

bei ImageIO copyImageBlockSetWithOptions.

Edit2: Workaround basierend auf der Antwort von warrenm: Rufen Sie newTexture(with: cgImage) Sync anstelle von async. Zum Beispiel über die Funktion wird,

func newTexture(with uiImage: UIImage, options: [String : NSObject]? = nil, completionHandler: MTKTextureLoaderCallback) { 
    if let cgImage = uiImage.cgImage { 
     let tex = try? self.newTexture(with: cgImage, options: options) 
     completionHandler(tex, tex == nil ? TextureError.CouldNotBeCreated : nil) 
    } else { 
     completionHandler(nil, TextureError.CouldNotGetCGImage) 
    } 
} 

(Ich nenne tatsächlich die Synchronisierungsmethode Beachten Sie, dass ich die @escaping da jetzt entfernt haben.)

Der verkleinern Code richtig war. Es funktioniert jetzt mit dieser Problemumgehung :)

Antwort

2

Dies scheint ein Problem mit der Lebensdauer der CGImage zum Erstellen der Textur, und es kann ein Fehler in MetalKit sein.

Im Wesentlichen wird die vom Kontext zurückgegebene CGImage nur garantiert bis zum Ende des Gültigkeitsbereichs des Autorelease-Pools, in dem er erstellt wurde, gespeichert. Wenn Sie die asynchrone newTexture-Methode aufrufen, verschiebt MTKTextureLoader die Arbeit des Erstellens der Textur auf einen Hintergrundthread und operiert auf dem CGImage außerhalb des Bereichs seines umschließenden Autorelease-Pools, nachdem sein Sicherungsspeicher bereits freigegeben wurde.

Sie können dies umgehen, indem Sie entweder das Bild im Beendigungshandler erfassen (wodurch ARC seine Lebensdauer verlängert) oder die entsprechende synchrone Texturerstellungsmethode newTexture(with:options:) verwenden, die bis zur Textur im relevanten Bereich verbleibt ist vollständig initialisiert.

+0

Danke! Das hat perfekt funktioniert. Ich habe die Aufrufe von newTexture synchron gemacht, und sowohl der newTexture-with-uiImage als auch der Downsize-Code funktionieren jetzt :) Ich habe meine Frage so bearbeitet, dass sie die Problemumgehung enthält. Danke noch einmal! – endavid

Verwandte Themen