Tistory View

Android Develop/helper

안드로이드 Choreographer 사용하기

God Dangchy What should I do? 2021. 3. 31. 19:42

 

인스턴스 생성하기

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프래임 뒤에 그려진다.

 

Choreographer 동작 방식

이 시간 값(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프레임 뒤에 호출되는 경우도 꽤 있다.

Frame dropped by delayed call

 

 

 

NDK에서 사용하기

이 글에 넣기에는 내용이 너무 길어져서 다른 글로 대체한다.

jamssoft.tistory.com/232

 

안드로이드 Native에서 Choreographer 사용하기

우선 Choreographer의 기본 방식을 모르는 독자는 다음의 링크를 보고 오기를 바란다. 기본적인 내용과 Java코드가 있다. jamssoft.tistory.com/231 안드로이드 Choreographer 사용하기 인스턴스 생성하기 Choreog.

jamssoft.tistory.com

 

 

근데, Choreographer를 써야하는 가...?

실제 구글 문서를 보면, Choreographer를 사용하기 전에 다른 대체안들이 문서의 첫 부분에 나온다.

View.onDraw()

     Custom View를 만들어서 사용한다면 이 함수가 화면에 그리는 내용을 그리는 데 사용된다. 이 함수가 호출되는 시점이 Vsync에 맞춰져 있기 때문에 굳이 Choreographer를 따로 구현할 필요는 없다. 단지 화면에 그리려면, View.postInvalidateOnAnimation함수만 호출하면 된다.

ValueAnimator.start()

      애니메이션을 구현 할 때는이 것을 이용하는 것이 좋다. 실제 Choregrapher로 힘들게 만드는 것보다 아주 부드럽게 동작한다.

 

 

필자처럼 그냥 Choregrapher를 쓰고 싶은 사람이나, 위에 항목들에 해당되지 않는 상황에서만 쓰기를 구글은 권장하고 있다.

 

 

 

Java 예제

 

Java example of Choreographer

 

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

 

 

developer.android.com/ndk/reference/group/choreographer#group___choreographer_1ga0a25e2b9a3dd71b1fba13c362182e020

Replies
Reply Write