vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity5】【iOS】UIImageのリサイズとResourcesのアンロード

以前作成したiOS用のUnityプラグインでは、特に何も考えずにデータを取得していました。

が、カメラロールから画像を選択する操作を何度か繰り返すだけでアプリがクラッシュしてしまうことに気づいたので今回はその対策について。

やったこと

  • カメラロールから取得した画像(UIImage)のサイズが大きい場合は縮小する。
  • カメラロールから画像を取得したタイミングで、以前のデータを削除する。

UIImageのサイズを縮小する

画面の向きを取得する

まず、画像の長辺が指定した最大サイズ(2048px)を超えているかを知るために、画面の向き(orientation)を取得します。

PhotoSelector.mm

〜省略〜
@property (nonatomic) BOOL                      isLandscapeMode;
@end
@implementation PhotoSelector

const static CGFloat kFltImgMaxSize = 2048;
- (void)Initialize
{
    _vwcUnityView = UnityGetGLViewController();
    // デバイスの向きが変わったら通知を受け取る.
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(RotationChanged:)
                                                 name:@"UIDeviceOrientationDidChangeNotification"
                                               object:nil];
    // 起動時のデバイスの向きを取得しておく
    [self CheckIsLandscapeMode];
}
- (void) RotationChanged: (id)sender
{
    [self CheckIsLandscapeMode];
}
- (void) CheckIsLandscapeMode
{
    // Landscape or Portraitでなければ無視.
    if(UIDeviceOrientationIsLandscape([[UIDevice currentDevice] orientation]))
    {
        _isLandscapeMode = YES;
    }
    else if(UIDeviceOrientationIsPortrait([[UIDevice currentDevice] orientation]))
    {
        _isLandscapeMode = NO;
    }
}
〜省略〜
  • 今回はデバイスが縦か横かだけが判断できれば良いので、「UIDeviceOrientationIsLandscape」を使用しています。
  • デバイスの向きが変わった時の通知は、縦横の他「UIDeviceOrientationFaceUp」のような上向き・下向きの場合にも受け取ることになるので、これらは無視するようにしています。

画像のサイズを変更する

PhotoSelector.mm

〜省略〜
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // カメラロールから選んだ写真のURLを取得.
    UIImage *imgOriginal = [info objectForKey:UIImagePickerControllerOriginalImage];
    UIImage *imgResized;
    BOOL isResized = NO;
    NSData *imageData;
    
    // TODO: 画像から回転情報を取得して、デバイスの向きに対して傾いていれば修正.
    NSLog(@"%long", imgOriginal.imageOrientation);
    
    CGFloat fltImageWidth = imgOriginal.size.width;
    CGFloat fltImageHeight = imgOriginal.size.height;
    
    // 画像の長辺が2048より大きければ縮小.
    if(_isLandscapeMode)
    {
        if(fltImageWidth > kFltImgMaxSize)
        {
            CGFloat fltScale = kFltImgMaxSize / fltImageWidth;
            
            UIGraphicsBeginImageContext(CGSizeMake(kFltImgMaxSize, fltImageHeight * fltScale));
            [myUIImage drawInRect:CGRectMake(0, 0, kFltImgMaxSize, fltImageHeight * fltScale)];
            imgResized = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            
            isResized = YES;
        }
    }
    else
    {
        if(fltImageHeight > kFltImgMaxSize)
        {
            CGFloat fltScale = kFltImgMaxSize / fltImageHeight;
            
            UIGraphicsBeginImageContext(CGSizeMake(fltImageWidth * fltScale, kFltImgMaxSize));
            [myUIImage drawInRect:CGRectMake(0, 0, fltImageWidth * fltScale, kFltImgMaxSize)];
            imgResized = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            
            isResized = YES;
        }
    }
    if(isResized)
    {
        imageData = UIImagePNGRepresentation(imgResized);
        imgOriginal = nil;
    }
    else
    {
        imageData = UIImagePNGRepresentation(imgOriginal);
    }
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    
    // 適当なファイル名をつける.
    NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"tmp.jpg"];
    [imageData writeToFile:filePath atomically:YES];
    
    _mstSelectedImage = (NSMutableString *)filePath;
    // 取得したパスをUnity側に送信する.
    UnitySendMessage("CtrlSetTexture", "OnCallbackIos", MakeStringCopy([_mstSelectedImage UTF8String]));
    // カメラロールを閉じる.
    [picker dismissViewControllerAnimated:YES completion:nil];
}
〜省略〜
  • 実際にはサイズを変更したUIImageを新たに作成している感じで、「UIGraphicsBeginImageContext」から「UIGraphicsEndImageContext」の間で実行しています。
  • 画像の長辺を指定のサイズ(2048px)に設定して、短辺は元の画像の長辺と2048pxからスケール値を出して設定しています。
  • UnityプロジェクトでもARCは有効になっているようですが、場合によっては手動での解放も必要…?

結果

上記を変更してみたところ、最初に比べると格段に改善されたように思いました。

しかし、やっぱりメモリは画像を読み込むたびに使用量が増え、10数回も繰り返すとクラッシュしてしまいました。

Resources.UnloadUnusedAssets

Unity側のスクリプトで「Resources.UnloadUnusedAssets」を実行することで現在キャッシュにあって未使用のアセットを解放することができます。

CtrlSetTexture.cs

public void SetNewTexture(string strPath, int intWidth, int intHeight)
{
    if (File.Exists(strPath))
    {
        Resources.UnloadUnusedAssets();
        // 取得したパスから画像を読み込んでマテリアルとして設定する.
        _mtrCube.mainTexture = ReadTexture(strPath, intWidth, intHeight);
    }
    else
    {
        // 取得したパスにアクセス出来ない場合はアラート表示.
        _ctrMain.ShowFileNotFoundAlert();
    }
}

これでメモリの使用量を抑えることができ、しばらく手元にあるiPhone5sで操作を繰り返しても、メモリ使用量が増え続けたりクラッシュすることはありませんでした。

難点は実行に時間がかかることで、今回は画像を選択する -> 読み込んでマテリアルにセットする という機能しかないため画像ロード時に実行していますが、使用状況に合わせてタイミングは変更した方が良いかもしれません...。

その他

特にiPhoneなどのカメラで撮った画像の場合、マテリアルとしてセットすると90度傾いて表示されることがあるようです。

この情報は UIImageのサイズを縮小するの「UIImage *imgOriginal = [info objectForKey:UIImagePickerControllerOriginalImage];」で、「imgOriginal.imageOrientation」とすれば取得できるようです(long値でPortraitから0 〜 3)。

これを元に、画像サイズを変更するときに一緒に回転もさせれば良さそうです。

参考

Orientation

UIImageのサイズ変更

Unityでのリソースの解放