ページ 11

OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 20:53
by とら
こんばんは。
前回Androidのゲーム開発について質問させてもらった者です。
色々なサイト等巡ってみましたが、どうしてもOutOfMemoryエラーが解決できなかったのでここで質問させていただきます。

MenyActivity初回起動時:メモリ使用率85%

HowToPlayActivity起動時:メモリ使用率52%

MenyActivity再起動時:メモリ使用率65%

GameMainActivity起動時:メモリ使用率65%

MenyActivityを起動しようとするとOutOfMemoryで落ちる

となってしまいます。
どのアクティビティ移行時も、メモリ使用率が90%以上になります。

前回ご指摘いただいたように、onStart、onStop時にBitmapの読み込み、解放を行うようにしています。
そのほかにstaticの使用率を減らしたり等いろいろと試みたのですが、どうしても上のタイミングでOutOfMemoryが発生してしまいます。
個人的にはMenyActivity初回起動時のメモリ使用率が異様に高いのが気になっている(GCを走らせてもこのまま、Bitmapやオブジェクトの数はGameMainActivity>>>MenyActivity>HowToPlayActivity)
一度MenyActivityのBitmapを全く読み込まずに起動させてみたところ、同様のエラーが出たためBitmapが原因ではないとは思うのですが・・・

以下がMenyActivityの全貌となります。

コード:

public class MenuActivity extends Activity {
	private MenuView _menuView;
	private PointF touchPt = new PointF();
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
		requestWindowFeature(Window.FEATURE_NO_TITLE);

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);//画面のタイムアウト防止
        
        _menuView = new MenuView(this);
		setContentView(_menuView);
	}
	
	@Override
	protected void onStart() { // アクティビティが動き始める時呼ばれる
		super.onStart();
		_menuView.setMemory(getResources());
	}
	
	@Override
	protected void onStop() { // アクティビティが動き始める時呼ばれる
		super.onStop();
		_menuView.releaseMemory();
	}
	
	@Override 
	protected void onDestroy() { 
		super.onDestroy(); 
		_menuView.setBackgroundDrawable(null);
		_menuView = null;
	}
	
	@Override
	public boolean onTouchEvent( MotionEvent event )
	{
		// タッチした位置を取得
		touchPt.x = event.getX();
		touchPt.y = event.getY();

		switch( event.getAction() ){
			case MotionEvent.ACTION_DOWN:
				switch(_menuView.touchScreen(touchPt)) {
				case 1:
					gameStart();
					break;
				case 2:
					goHowTo();
					break;
				case 3:
					applicationFinish();
					break;
				}
				break;
		}
		return true;
	}
	
	// ゲームスタート(ゲームメインActibityに遷移)
	private void gameStart(){
		Intent intent = new Intent( this, DecoyMouseActivity.class );
		intent.setAction( Intent.ACTION_VIEW );
		startActivity( intent );
	}
	
	// 操作説明画面に移動(HowToActibityに遷移)
	private void goHowTo(){
		Intent intent = new Intent( this, HowToActivity.class );
		intent.setAction( Intent.ACTION_VIEW );
		startActivity( intent );
	}
	
	//
	private void applicationFinish(){
		finish();
	}
} 
 

コード:

 
public MenuView(Context context) {
		super(context);
		getHolder().addCallback(this);
		cnt=0;
		rctVd = new Rect(0, 0, Config.VD_WIDTH, Config.VD_HEIGHT);
		
		r_start = new Rect((int)(240-60*1.3), (int)(525-25*1.3), (int)(240+60*1.3), (int)(525+25*1.3));
		r_howto = new Rect(110-60, 580, 110+60, 580+50);
		r_exit = new Rect(365-60, 580, 365+60, 580+50);
		
		paint.setTextSize(20);
		paint.setColor(Color.BLACK);
		paint_rect.setColor(Color.RED);
		paint_rect.setStyle(Style.STROKE); //枠組みのみ表示
		
		GameScore _gameScore = new GameScore(context);
		
		allscore = _gameScore.getScore();
		for(rankcnt=0; rankcnt<Config.RANKNUM; rankcnt++) {
			if(allscore < Config.RANKUPSCORE[rankcnt]) break;
		}
	}

	@SuppressLint("WrongCall")
	@Override
	public void run() {
		while (_thread != null) {	//メインループ
			try{
				cnt++;
				if(cnt%300<180)		 mouseState=0;
				else if(cnt%300<240) mouseState=1;
				else 				 mouseState=2;
				onDraw(getHolder());
			}catch(NullPointerException e){
			}
		}
	}
	
	private void onDraw(SurfaceHolder holder) {
		try{
			// 仮想画面の下地を生成
			Canvas canvas = new Canvas(imgVdGame);
			canvas.drawColor(Color.WHITE); // 白で塗りつぶす

			//タイトル
			rcSrc.set(0, img_tytle.getHeight()/3*(cnt/8%3), img_tytle.getWidth(), img_tytle.getHeight()/3*(cnt/8%3+1));
			rcDst.set(0, 30, 480, 250);
			canvas.drawBitmap(img_tytle, rcSrc, rcDst, null);
			
             (以下同様の描画)
	
			
			//surfaceCreated()が実行されていないと、holder.lockCanvas()はnullを返す
			Canvas c = holder.lockCanvas();
			// 背景を塗りつぶし
			c.drawColor(Color.BLACK);
			// 生成したbmp(仮想画面)を画面に表示
			c.drawBitmap(imgVdGame, rctVd, rctRd, null);
			holder.unlockCanvasAndPost(c);
		}catch(NullPointerException e){}
	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
		// 仮想画面と実画面の比率を算出
		float mult_width = (float)Config.VD_WIDTH / (float)width;
		float mult_height = (float)Config.VD_HEIGHT / (float)height;

		// 実画面のサイズに引き延ばす辺(幅または高さ)を判断する
		if(mult_width < mult_height){
			mult = mult_height;
		} else{
			mult = mult_width;
		}

		// 仮想画面を拡大して張り付ける実画面上の座標を算出する
		int x2=(int)((float)Config.VD_WIDTH / mult);
		int y2=(int)((float)Config.VD_HEIGHT / mult);
		int x1=(int)((float)width  / (float)2 - (float)x2 / (float)2);
		int y1=(int)((float)height / (float)2 - (float)y2 / (float)2);
		x2 += x1;
		y2 += y1;

		// 実画面の座標を記録
		rctRd = new Rect(x1, y1, x2, y2);
	}

	@Override
	public void surfaceCreated(SurfaceHolder arg0) {
		_thread = new Thread(this);		//別スレッドでメインループを作る
		_thread.start();
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder arg0) {
		_thread = null;
	}

	public int touchScreen( PointF touchPt )
	{
		if(DiagramCalcr.isHitPR(touchPt, DiagramCalcr.remakeRect(r_start, rctRd.left, rctRd.top, mult))){
			// ゲームをスタート
			return 1;	
		}else if(DiagramCalcr.isHitPR(touchPt, DiagramCalcr.remakeRect(r_howto, rctRd.left, rctRd.top, mult))){
			// 操作説明画面を開く
			return 2;
		}else if(DiagramCalcr.isHitPR(touchPt, DiagramCalcr.remakeRect(r_exit, rctRd.left, rctRd.top, mult))){
			// このアプリを終了させる
			return 3;
		}
		return 0;
	}
	
	public void setMemory(Resources r) {
		imgVdGame = Bitmap.createBitmap(Config.VD_WIDTH, Config.VD_HEIGHT, Bitmap.Config.ARGB_8888);
		img_tytle = BitmapFactory.decodeResource(r, R.drawable.tytle);
		
        (以下略)

	}
	
	public void releaseMemory() {
		releaseBitmap(imgVdGame);
		releaseBitmap(img_tytle);
		
        (以下略)

	}
	
	private void releaseBitmap(Bitmap b) {
		if (b != null) { 
			b.recycle();
			b = null;
		}
	}
}
 
長くなってしまい申し訳ありませんが、以上のコードになにかメモリの無駄遣いな点がありましたら教えていただけるとありがたいです。
また、OutOfMemoryが起こったことのある方で、どのように対処したか覚えている方がいらっしゃいましたらぜひその時の対処法を教えてください。
よろしくお願いいたします。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 21:47
by へにっくす
Androidでのメモリリーク回避
Android Developers BlogよりAvoiding memory leaks
これ参考になりますかね?とくにAndroid Developers Blogにあるページ(日本語訳もあるよ)。
これ見る限り、やってはいけないことをやってますね。
ヒント:onCreate。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 22:57
by ISLe
基本的なことですがreleaseBitmapの引数bにnullを代入しても呼び出し元の変数はnullにならないですよ。

あとはスレッドが二重・三重に起動してたりしないですかね。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 23:07
by ISLe
へにっくす さんが書きました:これ見る限り、やってはいけないことをやってますね。
ヒント:onCreate。
基本的にViewはActivityと一蓮托生だと思うのでthisを渡すことに問題はない気がします。

とうぜんViewに余計な参照(あるいはスレッドとか)を持たせて生き長らえさせると問題になります。
その辺はViewもActivityと同じですね。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 23:12
by へにっくす
ISLe さんが書きました:基本的にViewはActivityと一蓮托生だと思うのでthisを渡すことに問題はない気がします。
リンク先のコードを見てもそんなこと言えますか?
以下、該当すると思われる文言。
Avoiding memory leaks さんが書きました:

コード:

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  
  TextView label = new TextView(this);
  label.setText("Leaks are bad");
  
  setContentView(label);
}
これは、ビューがアクティビティ全体への参照を持つことを意味する。結果、アクティビティが保持しているものすべてへの参照を持つことになる。言い換えると、ビューの階層全体とそのリソースすべてということになる。したがって、このContextをリークしてしまうと、大量のメモリリークになってしまう (ここで言うリークとは、参照を保持してしまった結果、GCが回収するのを妨害することを指す)。注意していないと、アクティビティ全体をリークしてしまうことは本当に簡単に起きてしまう。
そもそもonCreateでおそらく85%もメモリとってるんだよね?
ん?
あり?
リークとは関係ないか…?
ビットマップがそれだけでかいってことか。

的外れかもしれません。
失礼しました。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 23:19
by ISLe
へにっくす さんが書きました:リンク先のコードを見てもそんなこと言えますか?
以下、該当すると思われる文言。
Avoiding memory leaks さんが書きました:

コード:

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  
  TextView label = new TextView(this);
  label.setText("Leaks are bad");
  
  setContentView(label);
}
これは、ビューがアクティビティ全体への参照を持つことを意味する。結果、アクティビティが保持しているものすべてへの参照を持つことになる。言い換えると、ビューの階層全体とそのリソースすべてということになる。したがって、このContextをリークしてしまうと、大量のメモリリークになってしまう (ここで言うリークとは、参照を保持してしまった結果、GCが回収するのを妨害することを指す)。注意していないと、アクティビティ全体をリークしてしまうことは本当に簡単に起きてしまう。
そもそもonCreateでおそらく85%もメモリとってるんだよね?
言えますよ。
リンク先ではそのコードに問題があるとは言っていません。
contextがリークするのは次に書かれているコードです。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 23:23
by へにっくす
ISLe さんが書きました:リンク先ではそのコードに問題があるとは言っていません。
contextがリークするのは次に書かれているコードです。
失礼しました。

んー恥。
!!!orz!!!

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 23:36
by ISLe
MenuViewの中でリークしてる可能性はありますね。
提示されたコードはMenuViewのフィールド宣言部が省略されているので分かりませんけど。

ViewとActivityのライフサイクルがごっちゃになっているところも気になります。


コンストラクタで受け取ったコンテキストしか使わないようになっていれば問題にならないはずです。
それとへにっくすさんの指摘した内容のようにstaticに記録しないこと。

リソース読み込むのに引数で何か渡さなければならないのはおかしいと思ってください。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月06日(土) 23:45
by へにっくす
ISLe さんが書きました:MenuViewの中でリークしてる可能性はありますね。
提示されたコードはMenuViewのフィールド宣言部が省略されているので分かりませんけど。

ViewとActivityのライフサイクルがごっちゃになっているところも気になります。
確かに宣言部が全くないな
imgVdGame、img_tytleの宣言がない。
まあでも、とりあえずreleaseBitmapやめてそこに展開してみたらどうだろう?

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月07日(日) 12:25
by とら
みなさま多数の回答ありがとうございます。
とりあえずですが、
・Activity内のthisをすべてgetApplication()に変更
・releaseBitmap()の内容をreleaseMemory()に一つずつ展開
のように書き換えてみました。

また、へにっくすさんに教えていただいたMATを使用してみたところ、
GameMainActivity起動時にはMenuViewは全く残っていなかったため、
参照が残っていて解放されないということはなさそうでした。

一つ気になったのが、GameMainActivityを起動した際にMATを使用し、
そのLeak Suspect欄(メモリリークの可能性が高い物のリストアップ、という認識でよろしいでしょうか)のところに
Bitmap(14%)とは別にandroid.content.res.Resources(21%)があるという点でした。

今のところはアンドロイドプログラミングの館のスタイルをお借りして
GameMainActivity→GameSurfaceView→GameMgr→プレイヤーや敵などのクラス
という形でコードを書いているのですが、
GameMgrにBitmapや音楽源(MediaPlayer)を置いておきたいので

コード:

public class GameMainActivity extends Activity {
//略
     @Override
	protected void onStart() { // アクティビティが動き始める時呼ばれる
		super.onStart();
		_view.setMemory(getResources(), getApplication());
	} 
//略
}

class GameSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
//略
     public void setMemory(Resources r, Context context) {
		_gameMgr.setMemory(r, context);
	}
//略
}

public class GameMgr {
//略
   public void setMemory(Resources r, Context context) {
		//略
		cheese = BitmapFactory.decodeResource(r, R.drawable.cheese);
          //略
     bgm = MediaPlayer.create(context, R.raw.loop_89);
     //略
  }
}

 
という遠まわしな方法で画像等を読み込んでいたのですが、これが原因なのでしょうか・・・?
ContextやResourceの変数は一切用意しておらず、getResources()などで参照したあとすぐに解放されると睨んでのこのこーどなのですが・・

どなたかご指摘お願いいたします。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月07日(日) 17:40
by ISLe
とら さんが書きました:という遠まわしな方法で画像等を読み込んでいたのですが、これが原因なのでしょうか・・・?
ContextやResourceの変数は一切用意しておらず、getResources()などで参照したあとすぐに解放されると睨んでのこのこーどなのですが・・
それはリークしますね。
へにっくすさんが紹介した記事に書かれているのはシンプルな例ですが、コンテキストを使って作ったオブジェクトも内部にコンテキストを所有します。
特に参照を保持しなくてもコンテキストは引き継がれていくので注意してください。

Activity#getResourcesが返すオブジェクトはActivityのコンテキストを持ちます。
とりあえず

getApplication().getResources()

にしたら良いのではないでしょうか。

GameMgrはコンストラクタでコンテキストを受け取る設計にした上で、アプリケーションコンテキストを渡すようにすると統一感が出ますけど。

Re: OutOfMemoryエラーが解決できない

Posted: 2013年4月07日(日) 20:42
by とら
ISLe さんが書きました: getApplication().getResources()

にしたら良いのではないでしょうか。
これに変えても最初は大きな変化が見られなかったのですが、
GameMainActivityのリソース解放のタイミングをonStop()時から
バックボタン、又はゲーム終了時に移したところ、
Resourcesの参照が残らなくなったようでOutOfMemoryで落ちることがなくなりました!

何度か画面を切り替えてもメモリが圧迫されることがなかったので、余計な参照は完全に切ることができたみたいです。
いままでご回答くださった皆様、本当にありがとうございました!
コンストラクタでコンテキストを受け取る設計などは、これからぼちぼちと整えていこうと思います。