【未解決】大きいDataの配列から繰り返しUIImage(data:)を生成するとメモリリークする話【Swift】

未解決じゃーーーーー!!!

どうもこんにちは。Reoです。タイトルの通りなんですが、ワケがわからんすぎて頭がおかしくなりそうです。

 

Dataの配列からfor文で1つずつ取り出してUIImageを作成していきたいのですがどうもうまくいかない。メモリがあびゃーする。

配列から取り出してリサイズする処理でメモリリークしてると思っていたのだけれど、どうやら違うみたいで・・・

        //これでメモリリークする・・・・
        for data in dataArray{
            autoreleasepool{
                let image = UIImage(data:data)
            }
        }

Dataの入っている配列から1個ずつ取り出してUIImageを生成するというだけでメモリリークするのです。。。

 

色々実験してみたのでとりあえずは忘れないようにメモ。

 

以下のように書いて調べてみると、メモリはちゃんと解放される。

        while true{
            autoreleasepool{
                  let image = UIImage(data:data)
            }
        }

dataは固定の1つです。

 

同じUIImageをリサイズする処理をしてみてもメモリはちゃんと解放される。

        let dataArray = Array(repeating: UIImage(named:"image.png"), count: 3000)
        for i in 0 ..< 3000{
            autoreleasepool{
                let resize = self.resizeImage(image: dataArray[i]!, contentSize: movieSize)
            }
        }

 

Dataの入った配列を入れて繰り返しやってみると、メモリリークする。

        for data in dataArray{
            autoreleasepool{
                let image = UIImage(data:data)
            }
        }

 

おそらくこのDataのサイズが大きすぎせいなのかなーとは思っているんですが、だったら同じdataから何度もUIImageを生成してもメモリリークするのでは?と思ったんですが、そうでもないみたいです。

        for i in 0 ..< dataArray.count{
            autoreleasepool{
                let image = UIImage(data:dataArray[0])
            }
        }

これだとセーフ。

 

具体的にDataは6935815 bytesぐらい。全部そのぐらいあって、それを1つずつ取り出して300回ぐらい回したいのです。というかどれだけ回してもメモリリークしないようにしたいのです。

 

sleep()を使ってfor文の中をゆっくり見てみるとあることに気づきました。

for文の途中まではメモリはちゃんと解放されているのに、あるところを境に急に蓄積されるようになるんですね。

 

もっと詳しく調べてみることに

        for i in 0 ..< 10000{
            autoreleasepool{
                let k = i % 2   //10までは大丈夫 11からは蓄積される
                let image = UIImage(data:dataArray[k])
                let resize = self.resizeImage(image: image!, contentSize: movieSize)
            }
            sleep(1)
        }

1種類のdataでメモリリークしないなら、2種類にしてみたらどうかなと思って、交互にdataArrayから取り出されるようにしてみました。

これだとメモリリークは全くしないです。ちゃんと解放されています。

 

kの値をどんどん変えてみると、kが最大10のときはメモリリークは起きませんでした。

そいで11にしてみたところ、ちょうどdata1つ分くらいサイズのメモリがガバッと増えました。20MBだったのが26MBぐらいにどかっと増える感じで、それ以上はどれだけループしてもそこが上限です。

11以上にしてみると、あぶれた分だけ蓄積されるようになりました。

 

約6~7MBくらいあったDataではなく、サムネイルとして別に保存しておいた約5KBくらいのDataを使ってみました。

そうするとメモリが右肩上がりになることはなくなりました。枚数がもっと多くなると違うかもしれないですが・・・。

 

ウーーーーーーン。ここまで書いてみると、どうみてもオリジナルのDataが大きすぎるのが原因ですよねえ。。。

とは言っても結局はこいつを取り出してリサイズしないとどうにもできない。

アプリのデータを一度リセットしても良いのなら、写真を撮ったときにリサイズしてやれば、大きいデータをまとめて全部取り出す必要もないんですが、そういうわけにもいかないんですよね。

 

う〜〜ん。別にどこかに表示しているわけでもないのに、UIImageとして召喚しただけでメモリが蓄積されていくのは非常に困る。リサイズしようと思ってもUIImageからリサイズする方法しかわからない・・・。

 

とりあえずはタイルがどうやらっていう記事を見つけたのでもう少し頑張ってみます。

解決したらまた続きを書きます。

 

解決法が分かる方がいたらぜひ・・・・・

 

 

 

2018/04/14追記 原因はCoreDataにあった
この話、CoreDataに保存している画像のDataをfor文で1つずつ取り出してUIImageに変換し、それを動画に継ぎ足すっていう処理をするところで使っています。

これは多分NSManagedObjectContextの問題で、
        let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
        let context:NSManagedObjectContext = appDelegate.managedObjectContext
        context.reset()     //resetしておく
        let child = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        child.perform{
            for imageID in imageIDs{
                let fetchRequest:NSFetchRequest = Image.fetchRequest()
                let predicate = NSPredicate(format:"%K == %@","imageID",imageID)
                fetchRequest.predicate = predicate
                fetchRequest.fetchLimit = 1
                do{
                    let fetchData = try context.fetch(fetchRequest)
                    if(!fetchData.isEmpty){
                        autoreleasepool{
                            let image = UIImage(data:fetchData[0].imageData! as Data)
                            let resize = image.resize(size:size)
                            //画像を継ぎ足し
                            CGImageDestinationAddImage(destination,(resize.cgImage)!,frameProperties as CFDictionary?)
                        }
                        //ここでcontextをリセットしてやるとうまくいく
                        context.reset()
                    }
                }catch{
                    print("error\(error))")
                }
            }
            DispatchQueue.main.async {
                //メインの処理
            }
        }
ちょっと見づらいしこれだけで動く処理ではないんですが、少しだけアプリから抜粋してみました。 context.reset()っていうやつが肝なんですね。 この1行(最初にもしているので2行)を付け足すだけで、どれだけ多い枚数の動画でも作れるようになりました。 一度MovieCreatorを作った時に、どんだけ大量の画像でもautoreleasepool{}を書くだけで全てが解決したんです。 なのにいざ自分のRobinの方でautoreleasepool{}を書いたところでなんの意味もなくメモリがアビャーしてしまうんですね。 結局自分の場合は、このcontext.reset()で解決することができたんですが、もし同様にautoreleasepool{}を書いてるにも関わらずメモリが解放されない!ってなった時は多分原因が別のところにあるんだと思います。 CoreDataとかと組み合わせて使ってると特にそうですね(´・ω・`) 調べに調べてなんとか生み出したほぼ確実に落ちない書き方なんですが、意外と普通に常識的な書き方とかがあるのかもしれないです。 一度CoreDataもまとめて勉強しないとダメかなぁ。。。。
Swift

Comments...

コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS
more