うるおいらんど

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

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からリサイズする方法しかわからない・・・。

 

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

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

 

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

Additional Notes追記

原因は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もまとめて勉強しないとダメかなぁ。。。。

Comments

コメントはありません。

現在コメントフォームは工事中です。