디지털 장인정신

블로그 이미지
WebKit개발자의 혼자쓰는 블로그
스페로

Article Category

All (71)
Technology (47)
Creativity (19)
Private (4)

Recent Comment

Recent Trackback

Calendar

«   2018/08   »
      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  

Archive

My Link

  • Total331,196
  • Today11
  • Yesterday70
  1. 2010.05.08
    Android에서 View를 bitmap으로 정장하는법 (1)
  2. 2010.05.04
    Android View.getDrawingCache() 분석 (2)
결론은 두가지 방법이 있다.
1. View의 canvas가 그림을 framebuffer가 아닌 bitmap에 그리게 하기
2. View.getDrawingBuffer() 이용하기

우선 성능 측면에서는 1번이 약간 앞선다. 두가지 방법의 메카니즘을 설명해보자

1. View의 canvas가 그림을 framebuffer가 아닌 bitmap에 그리게 하기

우선 사용법은

View를 상속받아  onDraw를 overriding하면 그림을 screen(framebuffer겠죠?)에 그리지 않고 bitmap에 그릴수 있다

public void onDraw(Canvas canvas){
canvas.setBitmap(bitmap);
bitmap.eraseColor(Color.WHITE);
super.onDraw(canvas);
}

위의 코드처럼 bitmap을 하나 잡아서 그것을 canvas에 장착시키면 그림을 bitmap에 그린다. screen에는 안그리므로 까만화면이 나올 뿐이다.

View ----------- canvas ----framebuffer
              Draw                     |---bitmap

즉 canvas 가 그리는 곳을 bitmap으로 살짝 틀어줌으로서 그림이 그려진 memory를 얻을수 있다.
참고로 Draw가 onDraw를 다시 콜한다. 지난 포스트를 참고하자.


2. View.getDrawingBuffer() 이용하기

우선 getDrawingCache의 내부구현을 보면
View.getDrawingCache
                canvas = softref.get(); //cavas를 softReference로 만들어놓고 쓸때 꺼냅니다. 
                bitmap = createBitmap(width, height, config); //이놈이 jni로 c로 가서 SkBitmap을 새로 alloc합니다. 가장 병목인 곳
canvas.setBitmap(bitmap);
bitmap.eraseColor(Color.WHITE);
draw(canvas);
즉 bitmap공간을 새로 잡고 그것을 별도로 사용하는 canvas에 붙여서 그림을 bitmap에 직접 그린다. 전자와 mechanism은 같지만 bitmap을 새로 alloc한다는 단점이 있다.

앞에 그림에서 앞에 몇가지 과정이 더붙은 모양세다

View ---------------------------------- View ----------- canvas ---- bitmap
          getDrawingCache -- canvas.setBitmap                        Draw 

원래 View의 그림도 그린다면 getDrawingCache다 View.draw를 콜하고 원래 그림을 그리는 쓰래드도 View.draw를 콜하므로 똑같은 그림을 그리기 위해 두번 연산하게 된다.



참고로
 - softReference란 cache를 만들때 java에서 잘 사용하는 기법인데, 오직 메모리가 모자를때 garbage collector에 의해 메모리가 해제됩니다. 즉 메모리를 많이 차지하면서 계속써야되는놈을 softReference로 만들어놓으면 out of memory로 app이 중지하는것을 방지할 수 있습니다.
 - Android의 canvas, bitmap, paint 등의 그림을 그리는 class들이 skia의 SkCanvas, SkBitmap, SkPaint들과 거의 1:1로 매핑되어있네요.
Trackback 0 and Comment 1
getDrawingCache가 어떻게 그림을 복사하는지 내부 mechanism을 살펴보자

우선 사용예제는
 webView.setDrawingCacheEnabled(true);
 webView.setDrawingCacheBackgroundColor(Color.WHITE);
 webView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_AUTO);
 webView.getDrawingCache(true)
결론은 c단에서 skia bitmap이 실제로 그려지는 device의 actual pixel memory에 바로 붙어서 그 시점의 memory를 카피하는 것이라 생각된다. 확신을 못하는 이유는 skia쪽 분석을 하다 막혔기 때문이다. 추후에 더 완벽한 분석을 하여 보강하겠다.


frameworks/base/core/java/android/view/View.java 를 보면 getDrawingCache() 가 buildDrawingCache를 콜하면서 일을 한다. buildDrawingCache는 자기가 전에 저장해 놓은 bitmap이 갱신이 않되었으면 그것을 쓰고 아니면 다음을 수행한다.
bitmap = Bitmap.createBitmap(width, height, quality);
이 bitmap을 reference에 저장하여(?) getDrawingCache()로 불러쓴다.
mDrawingCache = new SoftReference<Bitmap>(bitmap);


진짜 일을 하는 놈이 Bitmap.createBitmap 인걸로 밝혀졌으니 그놈은 무엇을 하는지 알아보자
frameworks/base/graphics/java/android/graphics/Bitmap.java 를 살펴보면
public static Bitmap createBitmap(int width, int height, Config config) {
    Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true);
    bm.eraseColor(0);    // start with black/transparent pixels
    return bm;
}
jni로 c 함수를 콜하는것을 알수 있다.
frameworks/base/core/jni/android/graphics/Bitmap.cpp를 살펴보자.
 static JNINativeMethod gBitmapMethods[] = {^M
     {   "nativeCreate",             "([IIIIIIZ)Landroid/graphics/Bitmap;",^M
         (void*)Bitmap_creator },^M
nativeCreate의 구현은 Bitmap_creator이다.
이런 안쪽 구현은 Skia로 해놨군.. 잘 모르니 이제부터 설명은 틀릴가능성이 농후하다....
 static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,^M
                               int offset, int stride, int width, int height,^M
                               SkBitmap::Config config, jboolean isMutable) {^M
//위에서 jColors는 null, offset은 0으로 넘어왔다는것을 기억하자
     if (width <= 0 || height <= 0) {^M
         doThrowIAE(env, "width and height must be > 0");^M
         return NULL;^M
     }^M
 ^M
     if (NULL != jColors) {^M
         size_t n = env->GetArrayLength(jColors);^M
         if (n < SkAbs32(stride) * (size_t)height) {^M
             doThrowAIOOBE(env);^M
             return NULL;^M
         }^M
     }^M
 ^M
     SkBitmap bitmap;^M
 ^M
     bitmap.setConfig(config, width, height);^M
//가장 중요한 부분. bitmap이 actual pixel memory에 access할 수 있게 셋팅하는 부분이다. 부차적으로 bitmap에 메모리 alloc하고 pixel환경(?)을 잡아주는것 같다. VM이 그 메모리를 관리(?)할수 있도록 셋팅해주는것 같은데... 
     if (!GraphicsJNI::setJavaPixelRef(env, &bitmap, NULL, true)) {^M
         return NULL;^M
     }^M
 ^M
//pixel color(?)를 잡아주는것 같은데 우리는  null이 넘어왔으므로 pass 
     if (jColors != NULL) {^M
         GraphicsJNI::SetPixels(env, jColors, offset, stride,^M
                                0, 0, width, height, bitmap);^M
     }^M
 ^M
//결국 return에서 일을 하는 것이군.. SkBitmap는 메모리만 다시 잡아주는듯...
     return GraphicsJNI::createBitmap(env, new SkBitmap(bitmap), isMutable,^M
                                      NULL);^M
 }^M
이번글에서 가장 중요한 GraphicsJNI::setJavaPixelRef를 보자
frameworks/base/core/jni/android/graphics/Graphics.cpp
augments가 ctable=NULL, reportSizeToVM = true로 넘어왔음을 기억하자
bool GraphicsJNI::setJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                  SkColorTable* ctable, bool reportSizeToVM) {
    Sk64 size64 = bitmap->getSize64();
    if (size64.isNeg() || !size64.is32()) {
        doThrow(env, "java/lang/IllegalArgumentException",
                     "bitmap size exceeds 32bits");
        return false;
    }

    size_t size = size64.get32();
    jlong jsize = size;  // the VM wants longs for the size
    if (reportSizeToVM) {
        //    SkDebugf("-------------- inform VM we've allocated %d bytes\n", size);
//VM에 메모리 관련 뭔가를 하는것 같은데 이번글과 긴밀하게 관련 없는것 같으니 패스
        bool r = env->CallBooleanMethod(gVMRuntime_singleton,
                                    gVMRuntime_trackExternalAllocationMethodID,
                                    jsize);
        if (GraphicsJNI::hasException(env)) {
            return false;
        }
        if (!r) {
            LOGE("VM won't let us allocate %zd bytes\n", size);
            doThrowOOME(env, "bitmap size exceeds VM budget");
            return false;
        }
    }
    // call the version of malloc that returns null on failure
//actual pixel memory에서 bitmap을 복사할 storage 공간을 잡는다.    
void* addr = sk_malloc_flags(size, 0);
    if (NULL == addr) {
        if (reportSizeToVM) {
            //        SkDebugf("-------------- inform VM we're releasing %d bytes which we couldn't allocate\n", size);
            // we didn't actually allocate it, so inform the VM
            env->CallVoidMethod(gVMRuntime_singleton,
                                 gVMRuntime_trackExternalFreeMethodID,
                                 jsize);
            if (!GraphicsJNI::hasException(env)) {
                doThrowOOME(env, "bitmap size too large for malloc");
            }
        }
        return false;
    }
//SkMallocPixel로 이미지를 가져올 storage인 addr을 감싼다. 내부적으로 어떻게 하는지는 모르겠지만 화면에 그려질 pixel memory를 addr에 복사하는것 같다. reportSizeToVM이 true이므로 AndroidPixelRef실행. 하지만 AndroidPixelRef의 구현은 VM이 정상인지(?) 체크만 하고 다시 SKMallocPixelRef를 콜한다. SkMallocPixelRef:SkPixelRef 관계이고 void* fStorage를 추가적으로 관리한다. 아무래도 내부적으로 어떻게인지는 모르겠지만 화면에 그려질 pixel memory를 fStorage에 카피하는것 같다.
    SkPixelRef* pr = reportSizeToVM ?
                        new AndroidPixelRef(env, addr, size, ctable) :
                        new SkMallocPixelRef(addr, size, ctable);
//SkPixelRef 을 최종적으로 그림을 담을 bitmap의 fPixelRef에 pr을 설정하고 fPixels에는 pr.pixels()를 통해 PixelRef의 void* fPixels를 지정한다.
    bitmap->setPixelRef(pr)->unref();
    // since we're already allocated, we lockPixels right away
    // HeapAllocator behaves this way too
//하일라이트다. actual pixel memory를 카피한다(어떻게인지는 모르겠음). SkBitmap.lockPixel() -> SkPixelRef.lockPixel() -> SkPixelRef.onLockPixels -> SkMallocPixelRef.onLockPixels 를 연쇄적으로 콜한다. SkMallocPixelRef.fStorage가 SkPixelRef.fPixels에 카피되고 그것이 다시 SkBitmap.fPixels에 카피된다.
  bitmap->lockPixels();
     return true;
 }
결국 그림을 복사하는것은 SkPixelRef가 담당한다. SkPixelRef이 이 포스트에 결론이다. 근데 mechanism을 잘 모르겠다. http://skia.googlecode.com/svn/trunk/docs/html/index.html 를 보면 

This class is the smart container for pixel memory, and is used with SkBitmap. A pixelref is installed into a bitmap, and then the bitmap can access the actual pixel memory by calling lockPixels/unlockPixels.

This class can be shared/accessed between multiple threads.

라고 설명을 한다. 즉 lockPixels이 그림을 복사해 준다는 것인데..

external/skia/src/core/SkBitmap.cpp 를 보면 과연 SkMallocPixelRef는 addr을 감싸기만 한다는것 을 알 수 있다.
SkMallocPixelRef::SkMallocPixelRef(void* storage, size_t size,
                                   SkColorTable* ctable) {
    SkASSERT(storage);
    fStorage = storage;
    fSize = size;
    fCTable = ctable;
    ctable->safeRef();
}      
가장 중요한 lockPixels를 분석하자
external/skia/src/core/SkBitmap.cpp
void SkBitmap::lockPixels() const {
    if (NULL != fPixelRef && 1 == ++fPixelLockCount) {
//SkPixelRef의 lockPixel을 콜한다.
        fPixelRef->lockPixels();
        this->updatePixelsFromRef();
    }
    SkDEBUGCODE(this->validate();)
}  

SkPixelRef의 lockPixels을 다시 부른다.
void SkPixelRef::lockPixels() { 
    SkAutoMutexAcquire  ac(*fMutex);
         
    if (1 == ++fLockCount) {
        fPixels = this->onLockPixels(&fColorTable);
    }
}   
SkPixelRef의 onLockPixels는 virtual이므로 자식인 SkMallocPixelRef의 onLockPixels를 보면
void* SkMallocPixelRef::onLockPixels(SkColorTable** ct) {
    *ct = fCTable;
    return fStorage;
}  
단순히 fStorage를 반환한다... 이럴수가 SkMallocPixelRef, SkBitmap, SkPixelRef사이에서 변수를 주고받기만 하다 끝나버렸다.  어디서 화면에 그려질 pixel memory를 가져오는지 알수가 없다. 다만 최종 소스는 fStorage임을 알 수 있을 뿐이다...


그러므로 frameworks/base/core/jni/android/graphics/Graphics.cpp 로 고고씽
변수개수가 약간 다른데 왜 그런지는 모르겠고,  결과인 bitmap을 jni를 통해 다시 Java로 돌려준다.

gBitmap_class는 android/graphics/Bitmap Class이고 gBitmap_constructorMethodID 는 생성자이다.
jobject GraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, bool isMutable, jbyteArray ninepatch, int density)
{
    SkASSERT(bitmap != NULL);
    SkASSERT(NULL != bitmap->pixelRef());

    jobject obj = env->AllocObject(gBitmap_class);
    if (obj) {
        env->CallVoidMethod(obj, gBitmap_constructorMethodID,
                            (jint)bitmap, isMutable, ninepatch, density);
        if (hasException(env)) {
            obj = NULL;
        }
    }
    return obj;
}
한마디로 Bitmap(new SkBitmap(bitmap), isMutable=true, null, null); 을 콜한것인데... Java로 고고씽
 private Bitmap(int nativeBitmap, boolean isMutable, byte[] ninePatchChunk,
         int density) {
     if (nativeBitmap == 0) {
         throw new RuntimeException("internal error: native bitmap is 0");
     }
     // we delete this in our finalizer
     mNativeBitmap = nativeBitmap;
     mIsMutable = isMutable;
     mNinePatchChunk = ninePatchChunk;
     if (density >= 0) {
         mDensity = density;
     }
 }
c에서 jni를 통해 넘어온 bitmap을 wrapping해준다.




Trackback 0 and Comment 2