Tistory View
인스턴스 생성하기
Choreographer는 new를 통해 생성할 수도 없고, 다른 방법으로 만들어야 한다.
Choreographer choreographer = Choreographer.getInstance();
위와 같이 static함수를 호출하는 것으로 끝난다. 어짜피 화면에 그려지는 시간이라는 것은 정해진 거라 한 개만 있으면 충분하기 때문에 singleton방식으로 동작하게 만들어 두었고, 따라서 공유되는 자원이라 필요가 없어져도 일부러 삭제할 필요도 없다. 물론 최소한의 메모리는 차지하기 때문에 다음과 같이 하는 것으로 삭제작업을 하면 된다.
choregrapher = null;
Choreographer는 Looper를 이용하기 때문에 "Choreographer.getInstance()"를 호출하는 쓰레드에는 Looper가 반드시 있어야 한다. 따라서 postFrameCallback에 지정한 DoFrameCallback(doFrame함수)가 실행되는 Thread는 Choreographer.getInstance()를 호출한 쓰레드에서 실행된다.
Choreographer.FrameCallback
Choreographer.FrameCallback 인터페이스는 다음의 한가지 함수만이 있다.
void doFrame(long frameTimeNanos)
이 함수는 Vsync가 발생하면 호출되는 함수로, 최근에 발생한 Vsync 시간을 Nanosecond단위로 frameTimeNanos에 넣어준다. 즉 최근의 지나간 과거의 시간이다. 실제 그리기 작업이 화면에 그려지는 시간은 2프래임 뒤에 그려진다.
이 시간 값(frameTimeNanos)은 중요한 용도로 쓰이는 데, 애니메이션이 들어간 경우, 어떤 시간을 기준으로 화면에 그려질 그림을 생성할지 거의 정확한 시간을 계산할 수 있다. 단순의 현재의 시간값으로 할 경우 약간의 미세한 차이로 움직이는 화면이 뒤틀어 지겠지만, 이 값을 이용하면 가장 정확한 시간값으로 그려질 화면을 구성할 수 있다.
postFrameCallback함수는 여러 쓰레드에서 동시에 호출이 될 수 있다. 어짜피 Looper를 이용하기 때문에 짧은 시간에 여러 쓰레드에서 호출하면 doFrame함수는 같은 frameTimeNanos값을 가지고 여러번 실행이 될 수 있다. 대부분의 상황에서 한번만 호출되면 되기 때문에 다음과 같은 코드를 필자는 주로 사용한다.
// save last frameTimeNanos to prevent from prcessing twice
long mLastDoFrameCalledTimeNanos = 0L;
@Override
void doFrame(long frameTimeNanos) {
// check twice or more called
if( frameTimeNanos == mLastDoFrameCalledTimeNanos ) {
return;
}
mLastDoFrameCalledTimeNanos = frameTimeNanos;
// do something
}
doFrame이 호출되는 시간
postFrameCallback함수를 사용하여 바로 다음 Vsync시간에 DoFrame이 호출이 되지만, Looper를 이용하기 때문에 Looper사용에 따른 Delay가 발생하기도 하고, 느린 스마트폰의 경우 2~3프레임 뒤에 호출되기도 한다. 요즘 스마트폰들은 워낙 빨라서 문제가 없지만, 그래도 1~2프레임 뒤에 호출되는 경우도 꽤 있다.
NDK에서 사용하기
이 글에 넣기에는 내용이 너무 길어져서 다른 글로 대체한다.
근데, Choreographer를 써야하는 가...?
실제 구글 문서를 보면, Choreographer를 사용하기 전에 다른 대체안들이 문서의 첫 부분에 나온다.
View.onDraw()
Custom View를 만들어서 사용한다면 이 함수가 화면에 그리는 내용을 그리는 데 사용된다. 이 함수가 호출되는 시점이 Vsync에 맞춰져 있기 때문에 굳이 Choreographer를 따로 구현할 필요는 없다. 단지 화면에 그리려면, View.postInvalidateOnAnimation함수만 호출하면 된다.
ValueAnimator.start()
애니메이션을 구현 할 때는이 것을 이용하는 것이 좋다. 실제 Choregrapher로 힘들게 만드는 것보다 아주 부드럽게 동작한다.
필자처럼 그냥 Choregrapher를 쓰고 싶은 사람이나, 위에 항목들에 해당되지 않는 상황에서만 쓰기를 구글은 권장하고 있다.
Java 예제
Drawing Thread의 doFrame에서는 MainThread에 넘길 Bitmap을 그리고 FrameList에 넣고, 화면에 그리기 위해 invalidateOnAnimation을 호출해서 onDraw가 실행되게 한다. 또한 지속적으로 postFrameCallback을 호출해서 doFrame이 계속 호출되도록 한다.
onDraw에서 framelist에 남아있는 frame이 있으면 다시 onDraw가 호출되도록 invalidateOnAnimation을 호출하는데, 이 게 없으면, doFrame에서 invalidateOnAnimation이 가끔 너무 빨리 호출되서 frame-drop이 발생할 수 있기에 존재하는 코드다.
// if postInvalidateOnAnimation called too fast in from DrawingThread,
// onDraw(this function) is called just one-time, so, force redraw for next frame
if( frameCountInQueue > 0 ) {
this.postInvalidateOnAnimation();
}
실제 이 예제는 Choreographer의 사용법을 익히기위한 것일 뿐, 실제 앱을 만들 때는 이 방식을 쓰면 느리다.
실제 앱을 만들 때는 View를 사용하지말고 Surface를 당겨와서 lockCanvas와 unlockAndPost식으로 사용해야 할 것이다. 직접 DrawThread에서 그리는 방식을 이용하는 것이 좋다.
ChoreographerDrawView.java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Choreographer;
import android.view.View;
import java.util.LinkedList;
import java.util.Locale;
public class ChoreographerDrawView extends View {
private static final String TAG = "ChoreographerDrawView";
private static final int FRAME_COUNT = 3;
private static final float BoxWH = 50.0F;
private static final int BitmapWH = 500;
private Paint mDefaultPaint;
private Bitmap mDrawBitmap;
private Rect mViewport = new Rect();
private LinkedList< Bitmap > mQueueFrames = new LinkedList< Bitmap >();
// empty bitmap frames
private LinkedList< Bitmap > mBitmapPool = new LinkedList< Bitmap >();
private DrawingThread mDrawingThread;
private DrawingThreadHandler mHandler;
private boolean mActive;
private boolean mPrepared; // Indicator of Drawing Thread ready to run
public ChoreographerDrawView( Context context ) {
super( context );
this._init( context, null );
}
public ChoreographerDrawView(Context context, AttributeSet attrs ) {
super( context, attrs );
this._init( context, attrs );
}
private void _init( Context context, AttributeSet attrs ) {
mDefaultPaint = new Paint( Paint.ANTI_ALIAS_FLAG );
mDefaultPaint.setTextSize( 30.0F );
mDefaultPaint.setColor( 0xff000000 );
mViewport.left = 0;
mViewport.top = 0;
mViewport.right = BitmapWH;
mViewport.bottom = BitmapWH;
for( int i = 0 ; i < FRAME_COUNT ; i++ ) {
mBitmapPool.addLast( Bitmap.createBitmap( BitmapWH, BitmapWH, Bitmap.Config.ARGB_8888 ) );
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewport.left = 0;
mViewport.top = 0;
mViewport.right = Math.min( w, h );
mViewport.bottom = Math.min( w, h );
}
// activity relatives
public void onCreate() {
DisplayMetrics dm = this.getResources().getDisplayMetrics();
if( BuildConfig.DEBUG ) {
if( mDrawingThread != null ) {
Log.e( TAG, "onCreate() mDrawThread exist??? what??" );
}
}
mDrawingThread = new DrawingThread( this );
mDrawingThread.start();
try {
synchronized ( mDrawingThread ) {
while ( !mPrepared ) {
mDrawingThread.wait();
}
}
Log.d( TAG, "onCreate() Drawing Thread Started" );
} catch( InterruptedException e ) {
Log.e( TAG, "wait failrue" );
}
}
public void onResume() {
Log.d( TAG, "onResume" );
synchronized ( this ) {
mActive = true;
}
mDrawingThread.postRedrawVSync();
}
public void onPause() {
synchronized ( this ) {
mActive = false;
}
}
public void onDestroy() {
try {
synchronized ( this ) {
mActive = false;
}
if (mDrawingThread == null) {
throw new Exception("Drawing Thread not exist.");
}
if( mHandler == null ) {
throw new Exception("Drawing Thread Handler not exist.");
}
mHandler.getLooper().quit();
Log.d( TAG, "Wait for joinning Drawing Thread..." );
mDrawingThread.join();
Log.d( TAG, "Drawing Thread joined!" );
mDrawingThread = null;
}
catch ( InterruptedException e ) {
Log.e( TAG, "onDestroy() InterruptedException occurred" );
}
catch( Exception e ) {
Log.e( TAG, "onDestroy() " + ( e != null && e.getMessage() != null ? e.getMessage() : "Unknown Error" ) );
}
// ok remove all bitmaps( Render Cache, queued frames, and bitmap pool )
if( mDrawBitmap != null ) {
mDrawBitmap.recycle();
mDrawBitmap = null;
}
while( !mQueueFrames.isEmpty() ) {
Bitmap bmp = mQueueFrames.pop();
bmp.recycle();
}
while( !mBitmapPool.isEmpty() ) {
Bitmap bmp = mBitmapPool.pop();
bmp.recycle();
}
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas); // super.onDraw do nothing
//canvas.drawText( "hihihihi", 10, 100, mDefaultPaint );
Bitmap bmp2Draw = null;
Bitmap bmp2Recycle = null;
int frameCountInQueue = 0;
synchronized ( this ) {
if( !mQueueFrames.isEmpty() ) {
bmp2Draw = mQueueFrames.pop();
frameCountInQueue = mQueueFrames.size();
}
}
if( bmp2Draw != null ) {
bmp2Recycle = mDrawBitmap;
mDrawBitmap = bmp2Draw;
} else {
Log.d( TAG, "onDraw() drawing last cached frame, frame is not queued." );
}
// if postInvalidateOnAnimation called too fast in from DrawingThread,
// onDraw(this function) is called just one-time, so, force redraw for next frame
if( frameCountInQueue > 0 ) {
this.postInvalidateOnAnimation();
}
if( mDrawBitmap != null ) {
//canvas.drawBitmap(mDrawBitmap, 0, 0, null);
canvas.drawBitmap( mDrawBitmap, null, mViewport, null );
} else {
canvas.drawColor( 0xff000000 );
}
if( bmp2Recycle != null ) {
if( mBitmapPool.size() > 3 ) {
bmp2Recycle.recycle();
} else {
synchronized ( this ) {
mBitmapPool.addLast(bmp2Recycle);
}
}
}
}
class DrawingThread extends Thread implements Choreographer.FrameCallback {
private static final String TAG = "DrawingThread";
private static final float FPS_TEXT_SIZE = 25.0f;
private Choreographer mChoreographer;
private ChoreographerDrawView mRenderView;
private Canvas mCanvas;
private Paint mPaint;
private RectF mBoxRect;
private int mBoxColor = 0xffffffff;
// pixel per seconds
private float mVelocityX;
private float mVelocityY;
private long mLastFrameTimeNanos;
// fps
private float mLastFps;
private long mLastFpsCheckTimeNanos;
private int mFpsCounter;
public DrawingThread( ChoreographerDrawView renderView) {
mRenderView = renderView;
// next line report 0x0... hmm..
//Log.d( TAG, "" + mRenderView.getWidth() + "x" + mRenderView.getHeight() );
// initialize physics
mBoxRect = new RectF();
mBoxRect.left = (float)Math.random() * ( BitmapWH - BoxWH );
mBoxRect.right = mBoxRect.left + BoxWH;
mBoxRect.top = (float)Math.random() * ( BitmapWH - BoxWH );
mBoxRect.bottom = mBoxRect.top + BoxWH;
mVelocityX = (float)Math.random() * 300.0F + 200.0F;
mVelocityY = (float)Math.random() * 300.0F + 200.0F;
}
@Override
public void run() {
Looper.prepare();
mChoreographer = Choreographer.getInstance();
mHandler = new DrawingThreadHandler( this );
mCanvas = new Canvas();
mPaint = new Paint( Paint.ANTI_ALIAS_FLAG );
mPaint.setColor( 0xffffffff );
mPaint.setTextSize( FPS_TEXT_SIZE );
// notify DrawingThread prepared, wake up main UI thread
synchronized ( this ) {
mPrepared = true;
this.notifyAll();
}
Looper.loop();
}
public void postRedrawVSync() {
mChoreographer.postFrameCallback( this );
}
private void updatePhysics( long frameTimeNanos ) {
float deltaSeconds;
boolean changeBoxColor = false;
deltaSeconds = (float)( frameTimeNanos - mLastFrameTimeNanos) / 1000000000.0F;
//Log.d( TAG, "delta=" + ( deltaSeconds * 1000.0F ) + "ms" );
if( mBoxRect.left + mVelocityX * deltaSeconds < 0 ) {
mBoxRect.left = 0.0F;
mBoxRect.right = BoxWH;
mVelocityX = -mVelocityX;
changeBoxColor = true;
} else if( mBoxRect.right + mVelocityX * deltaSeconds > BitmapWH ) {
mBoxRect.left = BitmapWH - BoxWH;
mBoxRect.right = BitmapWH;
mVelocityX = -mVelocityX;
changeBoxColor = true;
} else {
mBoxRect.left += mVelocityX * deltaSeconds;
mBoxRect.right += mVelocityX * deltaSeconds;
}
if( mBoxRect.top + mVelocityY * deltaSeconds < 0 ) {
mBoxRect.top = 0.0F;
mBoxRect.bottom = BoxWH;
mVelocityY = -mVelocityY;
changeBoxColor = true;
} else if( mBoxRect.bottom + mVelocityY * deltaSeconds > BitmapWH ) {
mBoxRect.top = BitmapWH - BoxWH;
mBoxRect.bottom = BitmapWH;
mVelocityY = -mVelocityY;
changeBoxColor = true;
} else {
mBoxRect.top += mVelocityY * deltaSeconds ;
mBoxRect.bottom += mVelocityY * deltaSeconds ;
}
if( changeBoxColor ) {
mBoxColor = 0xff000000 | (int)( Math.random() * 0xffffff );
}
}
public void updateFps( long frameTimeNanos ) {
if( mLastFpsCheckTimeNanos == 0 ) {
mLastFpsCheckTimeNanos = frameTimeNanos;
}
float elapsedSec = (float)( frameTimeNanos - mLastFpsCheckTimeNanos) / 1000000000.0F;
if( elapsedSec > 1.0F ) {
mLastFps = mFpsCounter / elapsedSec;
mFpsCounter = 0;
mLastFpsCheckTimeNanos = frameTimeNanos;
}
mFpsCounter++;
}
private void drawOnBitmap( Bitmap bmp, long frameTimeNanos ) {
bmp.eraseColor( 0xff000000 );
mCanvas.setBitmap( bmp );
mPaint.setColor( mBoxColor );
mCanvas.drawRect( mBoxRect, mPaint );
mPaint.setColor( 0xffff0000 );
mPaint.setTextAlign( Paint.Align.RIGHT );
mCanvas.drawText( String.format( Locale.getDefault(), "Drawing Thread %.1ffps", mLastFps ), BitmapWH, FPS_TEXT_SIZE * 1.2f, mPaint );
mCanvas.setBitmap( null );
}
@Override
public void doFrame(long frameTimeNanos) {
//Log.d( TAG, "doFrame frameTimeNanos=" + frameTimeNanos );
Bitmap bmp = null;
int droppedCount = 0;
boolean active = false;
synchronized ( mRenderView ) {
active = mActive;
}
if( active ) {
this.mChoreographer.removeFrameCallback( this );
this.mChoreographer.postFrameCallback(this);
} else {
Log.d( TAG, "doFrame() mActive Flag is not set, next render will not be occurred." );
}
if (frameTimeNanos == mLastFrameTimeNanos) {
Log.e( TAG, "doFrame() same time reached" );
return;
}
if( mLastFrameTimeNanos == 0L ) {
mLastFrameTimeNanos = frameTimeNanos;
}
synchronized ( mRenderView ) {
if (!mBitmapPool.isEmpty()) {
bmp = mBitmapPool.pop();
}
}
if( bmp == null ) {
bmp = Bitmap.createBitmap( BitmapWH, BitmapWH, Bitmap.Config.ARGB_8888 );
Log.e( TAG, "doFrame() create bitmap on render loop, BitmapPoll is empty. Is resumed?" );
}
if( bmp == null ) {
Log.e( TAG, "doFrame() Can't create bitmap" );
return;
}
this.updatePhysics( frameTimeNanos );
this.updateFps( frameTimeNanos );
this.drawOnBitmap( bmp, frameTimeNanos );
synchronized ( mRenderView ) {
mRenderView.postInvalidateOnAnimation();
// if too many frame queued, remove frames
if( mQueueFrames.size() > 3 ) {
while( mQueueFrames.size() > 2 ) {
mBitmapPool.add( mQueueFrames.pop() );
droppedCount++;
}
}
mQueueFrames.addLast( bmp );
}
if( droppedCount > 0 ) {
Log.e( TAG, "doFrame() " + droppedCount + " frame(s) dropped!" );
}
mLastFrameTimeNanos = frameTimeNanos;
}
}
class DrawingThreadHandler extends Handler {
private DrawingThread mDrawingThread;
public DrawingThreadHandler( DrawingThread drawingThread ) {
super();
mDrawingThread = drawingThread;
}
@Override
public void handleMessage( Message msg) {
super.handleMessage(msg);
}
}
}
Activity
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class ChreographerActivity extends AppCompatActivity {
private static final String TAG = "ChreographerActivity";
private ChoreographerDrawView mChoreographerDrawView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_choreographer );
mChoreographerDrawView = this.findViewById( R.id.vw_choreographer );
mChoreographerDrawView.onCreate();
}
@Override
protected void onPause() {
super.onPause();
mChoreographerDrawView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mChoreographerDrawView.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
mChoreographerDrawView.onDestroy();
}
}
activity_choreographer.xml( in layout resource)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ChreographerActivity">
<com.tistory.jamssoft.blogcodetester.ChoreographerDrawView
android:id="@+id/vw_choreographer"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Reference
developer.android.com/reference/android/view/Choreographer
'Android Develop > helper' 카테고리의 다른 글
android prebuilt curl library (0) | 2021.12.22 |
---|---|
안드로이드 Native에서 Choreographer 사용하기 (0) | 2021.04.07 |
안드로이드 byte배열을 String 한글 변환(+charsetDecoder) (0) | 2020.12.30 |
Buffer(ByteBuffer, CharBuffer...) flip, compact, clear사용법 (0) | 2020.12.30 |
안드로이드 HttpUrlConnection POST 전송 #2 (3) | 2020.12.25 |
- Total
- Today
- Yesterday
- 아끼는 법
- 적금
- ComputeShader
- TTS
- 블로그
- 에어콘
- 금리
- 컴퓨트셰이더
- 재테크
- gpgpu
- Android
- 재태크
- choreographer
- 예금
- 경제보복
- OpenGLes
- 전기세
- 에어컨
- 안드로이드
- texture
- 전기료
- 텍스처
- 사용료
- 애드센스
- 컴퓨트쉐이더
- 티스토리
- 전기요금
- OpenGL ES
- 애드핏
- 공유 컨텍스트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |