⭐️ はじめに
スマートフォンアプリ開発では、画像の扱いがユーザーの体験に大きく影響します。特に大容量画像を使うと、メモリをたくさん使ってしまい、アプリが重くなったり、場合によってはクラッシュする時もあります。
そんな時、役立つのが「Downsampling」です。これは画像を必要なサイズに縮小して、メモリーの無駄遣いを防ぐ方法です。Downsamplingを上手に使えば、アプリの動きがスムーズになり、安定して動かせるようになります。
この記事では、画像のメモリ管理の大切さと、iOSでDownsamplingを簡単に使う方法をわかりやすく紹介します。
👨💻 詳細
WWDC18では、「590KBの画像を画面に表示するためには、約10MBのメモリが使用される」と説明されています。
画像はJPG形式で圧縮されたデータです。
この画像が表示されるまでの処理の流れは、以下の通りです。
-
Load(読み込み)
画像データ(JPGファイル)をメモリに読み込みます。 -
Decode(デコード)
読み込んだ画像をGPUが扱える形式に変換(デコード)します。
このとき、圧縮されていたJPGファイルが解凍され、約10MBのメモリを消費します。 -
Render(描画)
デコードされた画像を画面に描画し、ユーザーが見ることができる状態にします。
以下は約14MBの画像を単純に読み込むコードです。
struct ContentView: View {
@State private var uiImage: UIImage? = nil
var body: some View {
VStack {
if let uiImage = uiImage {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}
Button("Load Original Image") {
loadOriginalImage()
}
}
}
private func loadOriginalImage() {
guard let url = Bundle.main.url(forResource: "BigSizeImage", withExtension: "jpg") else { return }
if let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
self.uiImage = image
}
}
}
上記のコードを実行して画像を読み込むと、メモリ使用量が200MB以上になることが確認できます。
WWDC18では、画像をUIGraphicsImageRendererを使ってリサイズする方法と、
Image I/Oを使ってDownscalingする方法が紹介されています。
UIGraphicsImageRendererを用いた画像リサイズ
まずは、UIGraphicsImageRendererを使って画像をリサイズするコードを見てみます。
import SwiftUI
struct ContentView: View {
@State private var uiImage: UIImage? = nil
var body: some View {
VStack {
if let uiImage = uiImage {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}
Button("Load Resized Image") {
loadResizedImage()
}
}
}
private func loadResizedImage() {
guard let url = Bundle.main.url(forResource: "BigSizeImage", withExtension: "jpg"),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else { return }
let newSize = CGSize(width: 1024, height: 1024 * image.size.height / image.size.width)
let renderer = UIGraphicsImageRenderer(size: newSize)
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
self.uiImage = resizedImage
}
}
上記のコードを実行してみると、以下のような結果が得られます。
「Load Resized Image」ボタンを押すと、メモリ使用量が一時的に増加するのが確認できます。
その理由は、loadResizedImageという関数で、まずUIImage(data:)という方法を使って、元の大きな画像をすべてメモリに読み込みます。
画像のサイズがとても大きい場合でも、まずはそのままのサイズで画像を読み込むことになります。
(上で説明した、画像の読み込み(Load)とデコード(Decode)の処理は通常通り行われ、最後のRenderの段階だけ、リサイズ処理として実行されることになると思います。)
そのあとに、UIGraphicsImageRendererを使って、画像を小さくリサイズします。
でも、リサイズする前にすでに大きな画像がメモリにあるため、たくさんのメモリが使われてしまいます。
これもまた、メモリを効率的に使う方法ではないということが分かります。
Image I/Oを用いた画像のDownsampling
WWDC18で紹介された上の画像を見ると、
UIImageを使うサイズ処理には多くのコストがかかると説明されています。
また、Image I/Oを使用することで、画像をより効率的にリサイズできるとも述べられています。
画像をリサイズするための最も効率的な方法は、Image I/Oを使用することです!!
以下のコードは、Downscalingの例です。
import SwiftUI
struct ContentView: View {
@State private var uiImage: UIImage? = nil
var body: some View {
VStack {
if let uiImage = uiImage {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}
Button("Load DownSampling Image") {
loadDownSamplingImage()
}
}
}
private func loadDownSamplingImage() {
guard let url = Bundle.main.url(forResource: "BigSizeImage", withExtension: "jpg") else { return }
let maxPixelSize: CGFloat = 1024
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { return }
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
kCGImageSourceShouldCacheImmediately: true
]
if let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) {
self.uiImage = UIImage(cgImage: cgImage)
}
}
}
上記のコードを実行してみると、以下のような結果が得られます。
「Load DownSampling Image」ボタンを押して画像を読み込んでも、メモリがほとんど使用されていないことが分かります。
これは、画像を読み込む段階であらかじめ必要なサイズにリサイズ(Downsampling)しているため、元の高解像度画像をそのままデコードする必要がなくなり、使用するメモリ量が大幅に削減されているためです。
通常、JPG画像は読み込んだ後にデコードされ、画面に表示するためのピクセル情報が展開される際に大量のメモリを消費します。
しかし、Downsamplingを行うことで、最初から小さなサイズでデコードするため、展開されるデータ量自体が少なくなり、メモリ使用量を抑えることができます。
💬 まとめ
画像をリサイズする方法には大きく分けて2つあり、それぞれに特徴と注意点があります。
📌 UIGraphicsImageRendererの場合
この方法では、まず元の大きな画像をすべてメモリに読み込んでから、リサイズ処理を行います。
そのため、一時的にでも非常に多くのメモリが使われてしまうことがあり、特に大きな画像を扱う場合は、アプリのパフォーマンスに悪影響を与える可能性があります。
📌 Image I/Oを使ったDownsamplingの場合
こちらは、画像を読み込む前にあらかじめ縮小サイズを指定して読み込むという仕組みです。
そのため、最初から少ないメモリで効率よく画像を取り扱うことが可能になります。
結果として、よりスムーズに画像を表示でき、メモリ使用量も最小限にできます。
結論として、大きな画像を扱う場合はImage I/Oを使ったDownsamplingが圧倒的におすすめです。
メモリを効率的に使用するため、この方法を使用するのが良いと思います。
参考資料