Pull to refresh

Comments 20

Быстродействие? Или хотя бы количество fps и конфигурация тестовой тушки.
Тестировал на HTC Desire HD. Система ограничивает fps до 60. На эмуляторе подтормаживает, но это скорее всего из за самого эмулятора.
На практике сталкивался со следующей проблемой: В приложении рисование делал с помощью стандартного view. Все рисовалось довольно плавно и fps тоже был около 60. В определенный момент в иерархию view-элементов добавлялся TextView, анимированный с помощью стандартных средств Android (tween animation). Эта анимация длилась 2 секунды и после этого TextView удалялся. И вот на эти 2 секунды, пока шла анимация, вся отрисовка начинала тормозить. По завершении анимации, всё опять становилось плавно. После того, как начал использовать SurfaceView вместо простого View, данная проблема возникать перестала.
TextView сам по себе тяжеловатый элемент.

Можете добавить еще штук 20 вращаемых элементов и провести тест на живой машинке? Очень интересны результаты.
Добавил массив матриц и рисую картинку с помощью каждой из них. При 30-ти вращающихся картинках fps также остается равным 60. Если их число увеличить до 50 штук — частота кадров уменьшается примерно до 50.
Неплохо тянет. Подойдет даже для простых игр.
Спасибо за статью — пока ни разу не приходилось использовать данный компонент.
Один минорный комментарий по коду (может кому пригодятся для будущей реализации):

Лучше, чтобы передача UI поля потоку происходила посредством WeakReference (или где-то устанавливался бы референс в null). Иначе, в случае зависания потока будет мемори лик целой активити со всем наполнением потому как потоки не обязательно сразу удаляются мусоросборщиком, а могут еще жить неопределенное время. В этом случае, если запускать приложение несколько раз, размер свободного пространства на хипе может быстро сократиться, что может привести к ООМ эксепшену.

Пишу это не потому что охото придраться к коду, а потому, что в своем большом проекте уже большое кличество времени потратили на избавление от подобных мемори ликов.
Спасибо за совет. До этого не сталкивался с WeakReference. Буду иметь ввиду, что указанная вами проблема может возникнуть.
Спасибо за статью.

Однако не мог не заметить одну маленькую деталь, а именно очень очень много холостых прогонов в цикле while (run). Это очень сильно будет нагружать процессор, расходовать батарею. Возможно, стоило бы воспользоваться какими-нибудь средствами блокировки потока?
Народ, а подскажите, если кто знает: вообще, игры на SurfaceView нынче делают, или все перешли на GL? У меня есть желание попробовать что-нибудь этакое сделать. Начать то начал, но сразу заметил следующую проблему: если при обновлении экрана сперва перерисовывать фон, а затем — все спрайты,
ощутимо «дергаются» и мельтешат объекты. Нутром чую — это крайне неверный подход. А как сделать корректно — найти не могу. Пример простой, битмапный фон и мячик летает по нему.

Вот код в моем «потомке» SurfaceView
    public void run() {
        while (mRunning) {
            Canvas canvas = null;
            try {
                canvas = mSurfaceHolder.lockCanvas();
                synchronized (mSurfaceHolder) {
                    updateObjects(); 
                    refreshCanvas(canvas); 

                }
                sleep(20);
            } catch (Exception e) {
            } finally {
                if (canvas != null) {
                    mSurfaceHolder.unlockCanvasAndPost(canvas);
                }
            }
        }
    }

    private void refreshCanvas(Canvas canvas) {
        canvas.drawBitmap(mBackground, 0, 0, null);
        mBall.draw(canvas);
    }


Работает это довольно некрасиво, дергано, и по карям мячика — мельтешение от перерисовки фона.
Так, что-то я не могу никак привести мысли в порядок.
п.1) Есть у меня класс-модель
п.2) Есть у меня класс-представление, которое знает как «рисовать» класс-модель
п.3) Есть фоновый поток, который, обрабатывая данные «из вне», передает в представление итоговый объект класса-модели и вызывает у представления invalidate().

Чтобы переработать исходное приложение в соответствии с указанной статьей:

а) теперь DrawThread должен знать как «рисовать» класс-модель, а не класс-представление. Меня как-то это напрягает. Ведь если DrawThread действительно знает как «рисовать» класс-модель, то зачем нужен SurfaceView?
б) теперь нужно из фонового потока п.3 вызывать DrawThread c переданным в него объектом класса-модели, чтоб тот его и нарисовал.

Я правильно все понимаю? Это даст прироста производительности действительно?

Короче, все переделал в соответствии с постом и выявил такой баг. Если перерисовывается весь экран, то есть если происходит в потоке вызов onDraw(locked_canvas), то все хорошо, а если дорисовывать на канве еще что-то, используя locked_canvas.draw(...), то экран мерцает.

зачем оно тогда все это нужно, если можно аналогично перерисовывать все, используя invalidate() и обычный View?! Поговаривают, это происходит на версии Андройда ниже 2.2.
У меня происходит так — поток просто крутится и вызывает методы update и render из SurfaceView, т.е. он вовсе не знает, как и что рисовать. View я не использовал, но мне кажется метод с SurfaceView дает больше гибкости — например позволяет установить FPS и реализовать механизм пропуска кадров(если кадр не успевает перерисоваться за заданный ему промежуток времени, на следующей итерации отрисовка не вызывается, идет вызов только метода update, а как только «наверстали» отставание перерисовываем актуальное состояние экрана)
«поток просто крутится и вызывает методы update и render из SurfaceView»
да, именно так сейчас и сделано в одном из вариантов, так как именно SurfaceView должен знать как «правильно» рисовать, а вот когда ему рисовать, это уже определяет этот фоновый поток рисования.

Задача была какая — Нужно чтоб на канве оставалось все ранее нарисованное, а при новом вызове просто дорисовывалось поверх. при этом, во всех моих уже «рабочих» вариантах тестовой программы (которые не меорцают) приходится
1) перерисовывать весь канвас. то есть хранить все ранее нарисованные элементы в стеке объектов и при перерисовывании формы их всех последовательно и рисовать.
2) поток рисования работает в пустую — «маслает» себе и «маслает» бесконечно, я вот не пойму не уже ли никак нельзя асинхронно вызвать какой-то метод рисования на канве, то есть не просто поток висит и рисует что-то в соответствии с изменившимся стеком объектов, а вызывать по-требованию. сейчас я просто по требованию вызываю новый поток, который и перерисовывает канву в соответствии с хранящимся стеком объектов и завершается по окончании перерисовывания.

наверно так и надо подходить к решению такой задачи. вариант задачи (у меня она в этой части похожая, только я по меню не счелкаю, за меня вызывает отрисовку фоновый поток)
выбрал в меню «нарисовать квадрат» — объект добавился в стрек и вызвался поток перерисовки канвы в соответствии со всем стеком
выбрал «треугольник красного цвета» — объект добавился в стрек и вызвался поток перерисовки канвы в соответствии со всем стеком

а вот смежная задача — если я хочу чтобы несколько потоков рисовали на одной канве одновременно кривые линии, как будто это два человека рисуют мелом — красным и синим например. мне что — стек всех точек хранить и перерисовывать? ужос :) Да, использование Path для отрисовки «кривой» по точкам подходит, но опять же прийдется хранить стек точек. Эту задачу я приводил лишь как пример смежной с моей задачи. А если параллельно рисоваться должны несколько линий независимо (несколькими фоновыми потоками), то это нужно уже отслеживать массив стеков точек этих кривых. Но все равно это очень как-то неправильно. Ведь для того чтоб просто дорисовать очередную точку из фонового процесса без торможения интерфейса — нужно хранить весь стек и его перерисовывать.

Сейчас pvshnik мне предложил, наверно, единственно верное решение от холостой работы фонового потока рисования — использование Android Message System в фоновом потоке. Попробую внедрить его.

Но вот сама задача фонового «дорисовывания» на канве объекта, вынуждающая хранить стек всех нарисованных ранее объектов, как-то у меня не решается без мерцания экрана. При чем при обычном подходе к этой задаче, как в вышеописанном примере, мерцание делится на четное и нечетное, при нечетной блокировке рисуются все объекты нечетного вызова, при четной — только четного вызова. А вот так чтоб рисовались все и «четные» и «нечетные» на одном экране — не выходит, выходит только если хранить их стек и перерисовывать все каждый раз заново.

Пример программы с различной логикой вариантов рисования на экране скину позже (фоновый поток есть/нет, стек есть/нет, фоновый поток один/каждый раз новый). Логика механизма рисования определяться будет значением конфигурационной переменной.
Итак, вот пример программы с различной логикой вариантов рисования на экране. Смотрите комментарии и меняйте значение mySurfaceView.JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT
В комментариях отписал, что и как вызывается и что мне не очень нравится в том или ином варианте.
MainActivity
package com.example.surfaceviewexample;

import android.os.Bundle;
import android.app.Activity;
import android.graphics.Canvas;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.widget.Toast;

public class MainActivity extends Activity {

	MySurfaceView mySurfaceView;
	BackgroundThread backgroundThread;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mySurfaceView = new MySurfaceView(this);
        setContentView(mySurfaceView); 
        
    }    

	//BackgroundThread knows about only MainActivity and call this.parent.draw
    //but MainActivity knows about MySurfaceView and delegate draw to its "draw" method 
    public void draw(int number) {
    	switch(mySurfaceView.JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT){
			case(1):
			case(2):
			case(3):
			case(5):
			case(6):{
				mySurfaceView.draw(number);
				break;			
			}
			case(4):{				
				final int i = number;
				new Thread(new Runnable(){
					public void run(){
						mySurfaceView.post(new Runnable(){
							public void run(){
								mySurfaceView.draw(i);
							}
						});
					}
				}).start();			
			}
    	}	
    	
	}
    @Override
    protected void onStart() {

      	super.onStart();
        backgroundThread = new BackgroundThread(this);
        backgroundThread.start();
    }
    @Override
    protected void onStop() {
      	super.onStop();
      	if(backgroundThread!=null){
      		backgroundThread.stop();
      		backgroundThread = null;
      	}
    	
    }
    @Override
    protected void onDestroy() {
    	super.onDestroy();  	
  		if(backgroundThread!=null){
  			backgroundThread.destroy();
  			backgroundThread = null;
  		}      
    }
}

BackgroundThread
package com.example.surfaceviewexample;

public class BackgroundThread extends Thread {
	//It knows about only MainActivity and call this.parent.draw
	MainActivity parent;
	int index = 1;
	public BackgroundThread(MainActivity parent) {
		this.parent = parent;
 	}

	public void run() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}		
		while (true) {
			this.parent.draw(index++);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

MySurfaceView
package com.example.surfaceviewexample;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
	DrawingThread drawingThread;
	ArrayList<Integer> stack = new ArrayList<Integer>();
	public int JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT = 6;///1,2,3,4,5,6
	//1 - start drawingThread.draw - drawingThread is only one - not work - canvas blinks
	//1 - запуск в лоб drawingThread.draw - drawingThread один - экран мерцает четный/нечетный
	//2 - start drawingThread.draw in new Thread - it may be very difficult for comprehension - drawingThread is only one - not work - canvas blinks
	//2 - запуск в новой нити исполнения drawingThread.draw - это сложно для понимания, но кто-то так просто посоветовал попробовать - drawingThread один - экран мерцает четный/нечетный
	//3 - create every time new DrawingThread and start him  - drawingThread not declared and there are many DrawingThread objects - not work - canvas blinks
	//3 - каждый раз создается и стартуется новый DrawingThread - drawingThread вообще не объявляется - экран мерцает четный/нечетный
	//4 - using existing method "post" of SurfaceView of Runnable interface, see MySurfaceView.draw case 4 - экран мерцает четный/нечетный
	//4 - используется существующий метод "post" суперкласса SurfaceView, см. MySurfaceView.draw вариант 4 - экран мерцает четный/нечетный
	//5 - work well - there is one DrawingThread object and it have to store stack of all drawing Objects - I dont't like existing stack and idling process, not call on demand
	//5 - работает по интерфейсу пользователя как надо - и drawingThread только один, и есть стек отображаемых объектов - Мне не нравится, что для того чтоб дорисовать объект, нужно хранить и перерисовывать весь стек, есть холостой ход фонового потока рисования, а хочется отрисовку объекта по требованию
	//6 - work well - but there are more DrawingThread objects and it have to store stack of all drawing Objects - I dont't like existing stack. There is call on demand, although some people say that it is no good use multithread access to UI.
	//6 - работает по интерфейсу пользователякак надо - каждый раз создается и стартуется новый DrawingThread, и есть стек всех отображаемых объектов - Мне не нравится, что для того чтоб дорисовать объект, нужно хранить и перерисовывать весь стек. Но реализована отрисовка по требованию, хотя многие разработчики предостеригают от создания множества нитей, используемых для рисования на одной канве.
	//7 шутки ради
	public MySurfaceView(Context context){
		super(context);
		getHolder().addCallback(this);		
	}

	public void surfaceChanged(SurfaceHolder holder, int format, int width,
			int height) {		
	}


	//BackgroundThread knows about only MainActivity and call this.parent.draw
    //but MainActivity knows about MySurfaceView and delegate draw to its "draw" method
    //but MySurfaceView knows how to draw object well, but delegates draw to "DrawingThread.draw" method
	public void draw(int index){
		switch(this.JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT){
			case(1):
			case(4):{
				drawingThread.draw(index);
				break;			
			}
			case(2):{
				final int i = index;
				new Thread(new Runnable(){
					public void run(){
						drawingThread.draw(i);
					}
				}).start();
				break;			
			}
			case(3):{
				(new DrawingThread(getHolder(),this,index)).start();
				break;			
			}			
			case(5):{
				stack.add(index);
				break;			
			}			
			case(6):{
				stack.add(index);
				(new DrawingThread(getHolder(),this)).start();
				break;			
			}
		}			
	}
	public void draw(Canvas canvas, int number){
		//super.onDraw(canvas);
		switch(this.JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT){
			case(1):
			case(2):
			case(3):
			case(4):{
				Paint p = new Paint();
				p.setTextSize(canvas.getHeight()/50);
				p.setColor(Color.RED);
				canvas.drawText(""+number, p.getTextSize(), p.getTextSize()*(number), p);
				break;			
			}
			case(5):
			case(6):{	
				//in case 5 variable "number" is not using - this is stack 
				Paint p = new Paint();
				p.setTextSize(canvas.getHeight()/50);
				p.setColor(Color.RED);
				for(Integer int_item : stack){
					canvas.drawText(""+int_item, p.getTextSize(), p.getTextSize()*(int_item), p);
				}
				break;
			}
		}
	}
	
	public void surfaceCreated(SurfaceHolder holder) {
		switch(this.JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT){
			case(1):
			case(2):
			case(4):
			case(5):{
				drawingThread = new DrawingThread(getHolder(),this);
				drawingThread.start();
				break;			
			}
			case(3):
			case(6):{
				//Nothing else matter
				break;
			}
		}
	}

	public void surfaceDestroyed(SurfaceHolder holder) {		
		switch(this.JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT){
			case(1):
			case(2):
			case(4):
			case(5):{
				drawingThread.destroy();
				break;			
			}
			case(3):
			case(6):{
				//Nothing else matter
				break;
			}
		}		
	}
}

DrawingThread
package com.example.surfaceviewexample;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceHolder;

public class DrawingThread extends Thread {
	SurfaceHolder surfaceHolder;
	MySurfaceView mySurfaceView;
	int number = 0;
	public DrawingThread(SurfaceHolder surfaceHolder,MySurfaceView mySurfaceView){
		super();
		this.surfaceHolder = surfaceHolder;
		this.mySurfaceView = mySurfaceView;
	}
	public DrawingThread(SurfaceHolder surfaceHolder,MySurfaceView mySurfaceView,int number){
		super();
		this.surfaceHolder = surfaceHolder;
		this.mySurfaceView = mySurfaceView;
		this.number = number;
	}
	//BackgroundThread knows about only MainActivity and call this.parent.draw
    //but MainActivity knows about MySurfaceView and delegate draw to its "draw" method
    //but MySurfaceView knows how to draw object well, but delegates draw to "DrawingThread.draw" method
    //DrawingThread dose not know how to draw object well, it blocks canvas and call necessary MySurfaceView.draw(Canvas canvas, int number)
	public void draw(int number){
		
		this.number = number;
		this.draw();
	}
	protected void draw(){
		Canvas canvas = this.surfaceHolder.lockCanvas();
		try{
			synchronized (surfaceHolder) {
				//in case 5 or 6 variable "this.number" is not using - this is stack 
				mySurfaceView.draw(canvas, this.number);
			}									
		}
		finally{			
			if(canvas!=null){
				surfaceHolder.unlockCanvasAndPost(canvas);
			}			
		}
	}
	public void run(){
		switch(mySurfaceView.JUST_CHANGE_DEFAULT_DRAWING_LOGIC_VARIANT){
			case(1):
			case(2):{
				while(true){}	
			}
			case(3):{
				this.draw();
				break;
			}
			case(4):{
				//Nothing else matter
				break;
			}
			case(5):{
				//Nothing else matter
				while(true){this.draw();}	
			}
			case(6):{
				//Nothing else matter
				this.draw();	
				break;
			}
		}			
	}

}


Если кто-то все же будет разгребать и у него не пойдет, пишите в личку, скину исходник архивом.
Честно, не смотрел. Но вижу два варианта(если правильно понял задачу):
1) (имхо самый правильный) хранить в памяти не стек, а Bitmap, тогда логика такая:
— Создаем битмап в памяти(по умолчанию пустой)
— Рисуем на нем
— Выводим его на экран
— С каждой новой итерацией дорисовываем на нем, что надо, и снова выводим
2) (я делаю так. когда ставлю игру на паузу) В поток добавляется переменная state и метод setState, через него изменяем state на true/false (прорисовка и пауза соотв). При добавлении объекта для отрисовки ставим в true, он дорисовывается наверх, и флаг скидывается потоком. Если же стоит false, то draw не вызывается
Так, ни у кого исключение по выходу из программы не выдает?
Неа, не выдавало, только когда решил поэкспериментировать и вместо SurfaceView поставил GlSurfaceView — работало но по выходе исключение было
Короче, все решаемо, но только через костыли.Мигание экрана можно решить 2 способами
1)либо блокировать именно ту часть экрана, где что-то нужно нарисовать, но если области не хватит, то нарисуется обрезанный объект и если на фоне объекта было уже что-то то это что-то затрется прямоугольной областью (которую собственно мы и блокируем и в которую вписывается рисуемый новый объект). пробовал устанавливать прозрачность канвы, но не выходит пока.
2)либо воспользоваться следующим мегарешением — все минусы и особенности первого варианта просто напрочь отсутствуют, не надо ничего рассчитывать, нужно просто лишний раз производить блокировку — "холостую блокировку" канвы «lockCanvas(new Rect(0,0,0,0))+unlock», а до или после уже блокировать lockCanvas(null) и рисовать, тогда четные и нечетные рисуютя на одном экране. и тогда кстати можно что хочешь дорисовывать по требованию.

вот такой костыль, не знаю — баг это или фича SDK или эмулятора.Но костыль этот учтите. Отчитался. всем спасибо.
Может кто-нибудь знает, можно ли по готовому инстансу Surface создать SurfaceHolder?
Или готовый SurfaceHolder сделать Parcelable?
Sign up to leave a comment.

Articles