記事一覧

ListViewなどのImageを多く表示するとjava.lang.OutOfMemoryErrorが発生する場合の対応方法 | Xamarin.Forms


今回はAndroid端末で多くのImageコントロールに少し大きめの画像をバインドすると、java.lang.OutOfMemoryErrorが発生してアプリが落ちる不具合の対応方法についてご紹介いたします。
最初はVisual Studio上のエラーメッセージで
An unhandled Exception occured.
と表示され、何のことかさっぱりわかりませんでした。
Visual Studio の出力ウィンドウを見てみると、「SIGSEGV」とか「java.lang.OutOfMemoryError」とエラーが表示されていることから、ネイティブなエラーではないかと推測できていましたが、他にも可能性を考えて、ImageSourceにバインドする際のコンバーターの不具合かと思いましたが、非同期にしてもTryCatchで囲っても何も変わらず、元々ListViewの動作もカタついていましたので、やはりカスタムレンダラーを採用するべきかと考えるに至り、採用してみたところ、すっきりと解消しました。

xamarin_image_outofmemory_01.png


前提条件
・Windows10
・Visual Studio 2015 Community Update3
・Xamarin 4.3.0.784 (NuGet Xamarin.Forms 2.3.4.224)
・macOS Sierra 10.12.4 / Xcode8.3.1 / Xamarin.iOS 10.4.0.123


1.対応方法

Androidプロジェクトに以下のファイルを配置してください。
画像を縮小していますので、画像サイズを変更したくないImageコントロールをアプリに含んでいる場合は、ExportRendererの第一引数のtypeof(Xamarin.Forms.Image)を別のImage継承型に変更すると良いでしょう。

CustomImageRenderer.cs
using System.IO;
using System.ComponentModel;
using System.Threading.Tasks;
using Android.Graphics;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms;
[assembly: ExportRenderer(typeof(Xamarin.Forms.Image), typeof(CustomImageRenderer))]
public class CustomImageRenderer : ImageRenderer
{
    private bool _isDecoded;
    protected override async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        //baseを呼ぶとOutOfMemmoryが発生しますのでコメントアウト
        //base.OnElementPropertyChanged(sender, e);

ImageSource source = Element.Source;

if (source == null)
{
//画像がクリアされた場合は再デコードする為
_isDecoded = false;
}

if (_isDecoded == true ||
source == null ||
e.PropertyName != "Source")
{
//画像が無い場合や他のプロパティの変更の場合は処理を終了する
return;
}

        //読み込み用のオプションオブジェクトを生成
        //この値をtrueにするとメモリに画像を読み込まず、画像のサイズ情報だけを取得することができます。
        var options = new BitmapFactory.Options { InJustDecodeBounds = true };

        //画像ファイル読み込み
        //オプションのInJustDecodeBoundsがtrueのため実際の画像はメモリに読み込まれません。
        await this.GetBitmapAsync(source, options);
           
        //コントロールの縦幅に合わせた画像サイズを再計算します。
        //The with and height of the elements will be used to decode the image
        var width = (int)Element.Width;
        var height = (int)Element.Height;
if (width > 0 && height > 0)
{
//コントロールの大きさに合わせて画像を縮小する為
options.InSampleSize = this.CalculateInSampleSize(options, width, height);
}
else
{
//元画像のまま表示する為
options.InSampleSize = this.CalculateInSampleSize(options, options.OutWidth, options.OutHeight);
}

        //画像をメモリに読み込む場合はfalseを指定
        options.InJustDecodeBounds = false;

        //これで指定した縮尺で画像を読み込めます。
        //容量も小さくなるのでOutOfMemoryが発生しにくいです。
        Bitmap bitmap = await this.GetBitmapAsync(source, options);

        //オリジナルのイメージコントロールに修正した画像ファイルをセットします。
        //Set the bitmap to the native control
        Control.SetImageBitmap(bitmap);

        _isDecoded = true;
    }

//イメージソースから非同期でビットマップを取得する
    private async Task<Bitmap> GetBitmapAsync(ImageSource source, BitmapFactory.Options options)
    {
        Bitmap bitmap = null;
        if (source.GetType() == typeof(StreamImageSource))
        {
            System.IO.Stream stream = this.GetStreamFromImageSource(source);
            bitmap = await BitmapFactory.DecodeStreamAsync(stream, new Rect(), options);
        }
        else if (source.GetType() == typeof(FileImageSource))
        {
            FileImageSource fis = (FileImageSource)source;
            bitmap = await BitmapFactory.DecodeFileAsync(fis.File, options);
        }
        return bitmap;
    }

//イメージソースからSystem.IO.Streamを取得する
public Stream GetStreamFromImageSource(ImageSource source)
{
StreamImageSource streamImageSource = (StreamImageSource)source;
System.Threading.CancellationToken cancellationToken = System.Threading.CancellationToken.None;
Task<Stream> task = streamImageSource.Stream(cancellationToken);
Stream stream = task.Result;
return stream;
}

    private int CalculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
    {
        // Raw height and width of image
        float height = options.OutHeight;
        float width = options.OutWidth;
        var inSampleSize = 1D;

        if (!(height > reqHeight) && !(width > reqWidth)) return (int)inSampleSize;
        var halfHeight = (int)(height / 2);
        var halfWidth = (int)(width / 2);

        // Calculate a inSampleSize that is a power of 2 – the decoder will use a value that is a power of two anyway.
        while ((halfHeight / inSampleSize > reqHeight) && (halfWidth / inSampleSize > reqWidth))
        {
            inSampleSize *= 2;
        }

        return (int)inSampleSize;
    }
}

参考URL
https://developer.xamarin.com/recipes/android/resources/general/load_large_bitmaps_efficiently/


配置するだけで、非常に簡単に解消できます。ポイントは、
(1)継承したOnElementPropertyChangedにてBaseをコールしないこと、
(2)画像を縮小して表示すること
これだけでパフォーマンスまで改善されました。
※TapGestureRecognizerなどがListViewに存在するとSourceの変更が正しく取れませんでした。

しかしながら、画像サイズがあまりにも大きい場合(高解像度の写真等)、ListViewにバインドした時点でOutOfMemoryになってしまいます。バインドする写真は予め縮小しておいたほうが良いでしょう。
サイズを縮小する関数は次回の記事にて説明しています。
http://itblogdsi.blog.fc2.com/blog-entry-166.html




当ブログの内容をまとめた Xamarin逆引きメニュー は以下のURLからご覧になれます。
http://itblogdsi.blog.fc2.com/blog-entry-81.html


関連記事

コメント

コメントの投稿
非公開コメント

アルバム

広告

プロフィール

石河 純


著者名 :石河 純
自己紹介:素人上がりのIT技術者。趣味は卓球・車・ボウリング

IT関連の知識はざっくりとこんな感じです。
【OS関連】
WindowsServer: 2012/2008R2/2003/2000/NT4
Windows: 10/8/7/XP/2000/me/NT4/98
Linux: CentOS RedHatLinux9
Mac: macOS Sierra 10.12 / OSX Lion 10.7.5 / OSX Snow Leopard 10.6.8
【言語・データベース】
VB.net ASP.NET C#.net Java VBA
Xamarin.Forms
Oracle10g SQLServer2008R2 SQLAnywhere8/11/16
ActiveReport CrystalReport ReportNet(IBM)
【ネットワーク関連】
CCNP シスコ技術者認定
Cisco Catalyst シリーズ
Yamaha RTXシリーズ
FireWall関連
【WEB関連】
SEO SEM CSS IIS6/7 apache2

休みの日は卓球をやっています。
現在、卓球用品通販ショップは休業中です。