2015.11.29 22:36


startActivityForResult() 메서드를 실행시킬 경우 Activity로 돌아올 때 onResume() 메서드와 onActivityResult() 메서드의  우선순위는?

먼저 Activity의 생명 주기를 확인해보겠습니다.



사진 찍기 이후와 갤러리를 다녀오고 난 다음에 Activity로 돌아와 onActivityResult() 메서드가 실행되는 것을 로그로 찍어 봤는데 다음과 같은 경우가 있었습니다.

Case 1

onPause()
onStop()
————————— (갤러리나 사진을 찍으러 다녀옴)
onActivityResult()
onRestart()
onStart() ← Point View
onResume() ← Point View

Case 2

onPause()
onStop()
————————— (갤러리나 사진을 찍으러 다녀옴)
onRestart()
onActivityResult()
onStart() ← Point View
onResume() ← Point View

Case 3

onPause()
onStop()
onDestroy()
————————— (갤러리나 사진을 찍으러 다녀옴)
onCreate()
onStart() ← Point View
onActivityResult()
onResume() ← Point View

정리

뭔가 일정하지 않습니다. 하지만 정리해보면 아래와 같습니다.

onActivityResult() 메서드는

  1. onStart() 메서드 전·후로 실행 된다.
  2. onResume() 메서드 전에 꼭 실행 된다.

결론

onResume() 메서드는 Activity로 돌아올 경우 꼭 실행이 되는 메서드입니다. startActivityForResult() 메서드를 실행시킬 경우 Activity로 돌아올 때onResume() 메서드와 onActivityResult() 메서드 중 어떤 것이 우선순위가 있는 것인지 확인해보지 않고서는 헷갈리기 쉬운 부분 입니다.

로그를 찍어 확인해 봤으니 이젠 잊지 말아야겠네요. (잊을까봐 여기에 정리해놓습니다… 흑…)

startActivityForResult() 메서드가 먼저 실행되고 onResume() 메서드가 나중 실행된다!


신고


Posted by injunech
2015.10.14 00:53



Eclipse 상으로 Project 를 불러온 이후에 (시작하기02 참고) Visual Studio 나 다른 개발 Tool 을 이용하여 Project 의 Root 폴더상에 Classes 의 코드를 수정 한 이후에 .so build를 해주어 .so 파일 생성을 한다.

ProjectName/proj.android 폴더 상에 존재하는 build_native.py 파일을 실행. (해당 Script 를 이용하기 위해서는 python 환경 설정 되어 있어야 합니다.)


정상적으로 so 빌드가 되지 않는 경우 (couldnt find "libcocos2dcpp.so" 에러 발생)

ProjectName/proj.android/jni 폴더에 Android.mk 파일에 Build Script 에서 Classes 폴더의 모든 CPP 파일이 정상 빌드 되도록 수정하여 야합니다. (시작하기02 참고)





신고


Posted by injunech
2015.10.12 01:51


이 글에서는 이클립스를 사용한 Cocos2d-x 3.2 프로젝트 빌드 및 발견된 빌드 문제 해결 방법을 다룹니다


커멘드창을 이용하지 않고 이클립스를 사용하는 이유는 다음과 같습니다:

  • 세팅이 된 다음엔 빌드 및 스마트폰에의 실행이 아주 쉽다

  • 이클립스를 통해 디버깅이 가능

  • 이클립스를 통해 개발이 가능

  • 최신 버전의 서브시스템에 (ndk10...)에 간단히 대응

    • 굳이 최신 버전을 고집할 필요는 없지만(ndk9로 충분. 대부분은 ndk9를 사용하는 것 같습니다)

    • 그러나 최신 버전을 고집하면 프로젝트의 생명주기가 길어지죠.

  • 빌드에 사용될 cocos 라이브러리, 안드로이드 라이브러리를 언제든 쉽게 수정 가능



빌드 환경



  • Intel 64bit CPU
  • Windows7 64bit
  • Visual Studio 2013
  • Python 2.7
  • cocos2d-x 3.2
  • ndk10
  • ant1.9.4
  • Eclipse Luna
  • Android API 10 (2.3.3)
  • Java7



시작하기 전에



cocos2d-x 3.2를 안드로이드에 맞추어 빌드하는 과정은 굉장히 손이 많이 가는 작업입니다.

아래의 과정 중 단 하나라도 놓치면 빌드에 성공할 수 없습니다.

인내심을 가지고 아래의 과정을 하나하나 밟아가세요.





이클립스에 프로젝트 추가





File-Import-Existing Projects into Workspace를 선택하고

프로젝트 위치는 해당 게임 프로젝트\proj.android를 선택하면 됩니다.


Copy projects into workspace 옵션은 사용하지 마세요! 소스코드는 언제든지 변경 가능해야 하니까요.





프로젝트 설정



Import된 프로젝트는 기본적으로 설정이 맞게되어 있지 않습니다.

이클립스 환경에 맞게 프로젝트를 변경하도록 합니다.


1. cocos2d-x Andoird Library Project 추가

**이 1번 과정은 딱 한번만 하면 됩니다. 다른 게임 프로젝트를 다루게 되더라도 반복할 필요 없습니다.


File-Import-Existing Projects into Workspace를 선택하고


프로젝트 위치는 cocos2d-x 설치 디렉토리\cocos\platform\android\java를 선택하면 됩니다.

예) C:\cocos2d-x\cocos\platform\android\java


Copy projects into workspace 옵션을 선택하세요! 코코스 엔진 원본 소스코드는 건드려봤자 좋을게 없겠죠.




2. 프로젝트 설정 변경


여러분이 작성한 게임 프로젝트를 선택하고 Properties로 들어갑니다.

그리고 아래의 스크린샷을 따라서 라이브러리 설정을 고칩니다.








3. 문제 있는 소스코드 변경


3-1> 엔진 오타 수정


프로젝트/cocos2d/cocos/3d/CCBundleReader.h의 tell()함수 부분을

long int tell(); 에서

ssize_t tell(); 로 변경하세요


왜 그런지 모르겠지만 cocos2d-x 3.2자체에 문제가 있네요. 반환형이 잘못되어 있습니다 --;



3-2> 자동 CPP 소스파일 추적 및 추가

프로젝트/jni/Android.mk 파일을 수정합니다

LOCAL_SRC_FILES := hellocpp/main.cpp \
../../Classes/AppDelegate.cpp \
../../Classes/HelloWorldScene.cpp
부분을

FILE_LIST := $(wildcard $(LOCAL_PATH)/../../Classes/*.cpp)
LOCAL_SRC_FILES := $(FILE_LIST:$(LOCAL_PATH)/%=%)
LOCAL_SRC_FILES += hellocpp/main.cpp


로 변경합니다.


Classes폴더 안에 있는 모든 cpp파일을 자동으로 빌드 시에 포함시키도록 변경하는 것입니다.

물론 LOCAL_SRC_FILES부분을 늘려나가도 되겠지만 그런 바보같은 수고를 할 사람은 없겠죠.




4. 실행


에뮬레이터가 아닌 실제 안드로이드 스마트폰을 사용하시는 걸 추천합니다

Ctrl+B를 눌러 빌드하시고

프로젝트 우클릭->Run AS->Android Application을 누릅니다




5. 끝!


이제 여러분이 개발한 게임이 안드로이드에서 실행되는걸 볼 수 있습니다

끝없이 해결해야할 게임버그는 잠시 잊으시고 행복해 하셔도 좋습니다.





PLUS+

혹시 Visual Studio에서는 잘됐는데 다른 플랫폼에서 한글이 깨지거나

빌드 도중 error: format string is not a string literal (potentially insecure) [-Werror,-Wformat-security] 메시지가 발생하면

아래의 추가 문제 해결을 참조하세요




추가 문제 해결



1. 로그로 인해 발생하는 빌드 에러 해결

error: format string is not a string literal (potentially insecure) [-Werror,-Wformat-security] 에러 해결 방법입니다.


프로젝트/jni/Application.mk 파일의


APP_CPPFLAGS := -frtti -DCC_ENABLE_CHIPMUNK_INTEGRATION=1 -std=c++11 -fsigned-char

APP_LDFLAGS := -latomic

부분을


APP_CPPFLAGS := -frtti -DCC_ENABLE_CHIPMUNK_INTEGRATION=1 -std=c++11 -fsigned-char

APP_CPPFLAGS += -Wno-error=format-security

APP_LDFLAGS := -latomic

로 변경하세요


[문제가 생긴 이유]




2. 한글 깨짐 해결


Visual Studio에서는 잘만 출력되던 한글이 다른 플랫폼(안드로이드,IOS)에서 깨진다면

파일의 저장 형식이 잘못되었을 확률이 높습니다.


STEP1>

Visual Studio에서 한글이 들어있는 소스코드를 띄웁니다. 그리고 나서 파일->고급 저장 옵션을 열어

인코딩

유니코드(서명 있는 UTF-8)

줄 끝

Unix

로 설정하세요.



STEP2>

또한 이클립스 프로젝트의 Properties을 아래와 같이 설정합니다.



출처 : http://makerj.tistory.com/156



----------------------------------



위 작업을 완료 한 뒤에 추가로 발생 하는 아래와 같은 에러는

다음 방법으로 해결 ㅎ


1) cannot find : cocod2d-x libcocos2dcpp.so

2) The import org.cocos2dx.lib cannot be resolved





1) cannot find : cocod2d-x libcocos2dcpp.so


NDK 빌드

일단 명령프롬프트(윈도우버튼 > 프로그램파일검색 창에서 cmd)를 엽니다. 다음과 같이 cd 명령을 수행해서 앞서 새로 만든 프로젝트의 proj.android폴더로 이동합니다.(역시 폴더 이름은 각자의 것에 맞게)

이제 NDK 빌드를 하기 위해 파이썬 스크립트를 수행합니다.

> build_native.py

그럼 컴파일이 쫙쫙쫙 수행됩니다. 빌드 후 메시지가 다음과 같이 나오며 proj.android\libs\armeabi에libcocos2dcpp.so 파일이 생성되면 NDK 빌드가 완료 된 것입니다.


설정 파일 변경

이제 libcocos2dcpp.so를 Java와 연결하고 빌드하여 APK 파일을 생성하면 됩니다. 그 전에 설정 좀 바꿔줘야 할 것이 있습니다.

일단 AndroidManifest파일의 수정이 필요합니다. 안드로이드 프로젝트의 속성 파일이라고 생각하시면 됩니다. proj.android 디렉터리에 있는 AndroidManifest.xml 파일을 문서편집기로 열어 다음과 같이 sdk 버젼이 9로만 되어 있는 것을 

<uses-sdk android:minSdkVersion="9"/>

다음과 같이 target을 14로, min을 10으로 바꾸어줍시다.

<uses-sdk android:minSdkVersion="10" android:targetSdkVersion="14"/>

여기서 가리키는 SdkVersion은 앞서 ADT에서 설치했던 API 레벨을 의미합니다. 수정 전에 입력되어 있던 9는 전설속에만 존재하는 API고, 우린 전설을 믿지 않으니 수정해 주는겁니다. 기왕이면 target SDK를 구글에서 권장하는 14레벨로 설정합니다. API14는 4.0 아이스크림샌드위치인데 그럼 그 이하 버젼은 지원을 안하는 것이냐!? 그건 걱정하지 맙시다. 하위 OS 호환성을 위해서 targetSdkVersion과 minSdkVersion이 따로 있습니다. 최소 지원 SDK 값을 10으로 지정해줘서 2.3.3 생강빵 이상에서 돌아가게 해줍니다. 사실상 현존하는 최소 버젼은 API8 2.2 프로요이긴 하지만 target을 14로 설정하면 8의 설정과 충돌나는 부분이 있어서 애석하게도 사용하지 못합니다. 2.2 프로요는 아직 점유율이 2.7%나 되서 버리기에는 조금 아깝긴합니다. 하지만 타겟을 4.0 아이스크림샌드위치로 설정해야 태블릿에서 검색 가능하기때문에 살을 내주고 뼈를 취하는게 낫습니다.



2) The import org.cocos2dx.lib cannot be resolved



proj.android/src 폴더에 들어가 보시면 org/cocos2dx   폴더가 있을겁니다 이 내부가 비어 있는건 아닌지 확인해 보세요.

원래 create-android-project.sh를 실행했을때 이부분이 자동으로 채워져야 하는데 생성이 되지 않는 경우가 있습니다. 이런경우엔 수동으로 복사해 주면 되는데요.

 cocos2d-x-3.6/cocos/platform/android/java/src/에 들어가보시면 같은 구조의 디렉토리가 존재합니다.   이안에 있는 org 폴더를 통채로 복사해서 proj.android/src/ 에 그대로 복사해 붙여 넣은후 eclipse 에서 f5버튼으로 reflash 한후에 다시 빌드


신고


Posted by injunech
2015.10.12 01:33


Cocos2D-x 3.2에는 파이썬 스크립트 project-creator가 사라졌습니다 ㅠㅠ

따라서 콘솔 명령으로 생성해야 합니다.


 > Project name : GAME01 

 > Package name : com.cij.game01

 > Project directory : C:\Workspace\COCOS2D_Proejct\

 

라고 가정하고 생성하는 예시입니다


C:\cocos2d-x-3.6\tools\cocos2d-console\bin>

위와 같이 cocos2d-x 설치한 폴더 경로 내에 consol 폴더 내부 bin 에서 커맨드 창 실행하고 다음과 같은 명령어로 프로젝트를 생성 합니다.

python cocos.py new GAME02 -p com.cij.game01 -l cpp -d C:\Workspace\COCOS2D_Proejct\


위 방법은 cocos 에서 제공하는 Python 스크립트를 이용하여 프로젝트 생성하는 방법이며 다음 방법으로도 생성 가능합니다. (cocos 설치 폴더 환경 설정 완료 이후.)

cocos new GAME01 -p com.cij.game01 -l cpp -d C:\Workspace\COCOS2D_Proejct\

 

# 명령어 의미

'cocos new 게임이름 ?p 패키지명 ?l 사용할언어 ?d 프로젝트저장경로'



에러가 발생하지 않았다면 새 프로젝트 생성에 성공한 것입니다.



프로젝트 폴더 내의 proj.win32안에 있는 Visual Studio Solution파일을 실행하면 이제 Visual Studio에서 제작이 가능해집니다.



신고


Posted by injunech
2015.09.20 15:23


안드로이드 앱을 만들 때 가장 많이 하는 일 중에 하나가 사진을 찍어서 올리거나 인터넷에 있는 사진을 받아다 사용자에게 보여주는 일입니다. 스마트폰이 가진 중요한 기능 중의 하나가 카메라가 내장되어 있다는 것이고 이것이 인터넷과 연결되어 다른 사용자들에게 쉽게 공유할 수 있다는 것이기 때문이죠. 그래서 모바일 앱의 주요 컨텐츠 생산이나 소비 형태도 사진으로 많이 이루어져 있습니다.

드로이드 개발자 공식 사이트에서도 상당히 앞쪽 부분에 사진을 다루는 비트맵에 대한 트레이닝 세션을 만들어 놓았습니다. 공부차 공식 사이트의 내용을 간단히 추려서 옮겨 봅니다. 원문과 샘플코드 다운로드는 Displaying Bitmaps Efficiently에서 보실 수 있습니다. 


출처: 안드로이드 개발 사이트(http://develper.android.com). 번역: 용현택


안드로이드에서 처음에 아무생각없이 Bitmap 오브젝트를 이용해 사진을 불러오려다. 

java.lang.OutofMemoryError: bitmap size exceeds VM budget

라는 메세지와 부딪히게 될 것이다. 당연히 메모리가 부족해서 나는 에러 이고 몇가지 이유가 있을 수 있는데 대강 다음의 이유로 뻗는 것이다. 

- 안드로이드 장비의 메모리는 무한정이 아니다. 안드로이드 장비가 하나의 앱에 허용하는 메모리는 16MB이하를 사용하도록 하고 있다. 대부분의 디바이스들에서는 16MB보다 높게 제한이 걸려있긴 하지만 이보다 작은 메모리를 사용하도록 최적화 해야 할 것이다. 따라서 이 제한을 넘게되면 에러가 난다. 

- 이미지는 메모리를 굉장히 많이 잡아 먹는다. 특히 요즘폰들은 화소가 높아서. 갤럭시 넥서스 같이 500만 화소 카메라로 찍게되면 2592*1936픽셀의 사진이 생겨나는데 ARGB_8888로 셋팅되어 있다면 한 픽셀당 4바이트를 사용해서 결과적으로 19MB를 차지하게 된다.한두장만 사용해도 장비의 메모리 제한은 가벼웁게 넘어버린다. 

- 여러개의 이미지를 한방에 로드하려는 경우가 많다. 예를들어 ListView나 GridView, ViewPager 같은 경우에는 한 화면에 여러개의 이미지를 보여주어야 하고 스크롤에 대비해서 미리 이미지들을 가지고 있어야 되는 경우도 많기 때문이다. 

그러면 큰 이미지를 어떻게 로드해서 화면에 보여주는게 효과적일지, 디스크나 인터넷으로 부터 비동기식으로 이미지를 받아와 처리하는 방법, 이미지를 캐싱하는 방법에 대해서 설명하도록 하겠다.

1. 큰 이미지를 (효율적으로)화면에 보여주자.

위에서도 얘기했지만 고해상도의 이미지를 불러와 그대로 메모리에 넣고 화면에 보여주는 것은 바로 OutOfMemory로 가는 지름길이다. 그러면 어떻게 할 것인가. 원래 이미지의 해상도와 상관없이 화면에 보여질 해상도 만큼 줄여서 읽어들이면 되지 않을까? 그러려면 원래 이미지의 해상도와 화면에 보이려는 ImageView의 해상도를 알아내어 품질을 떨어뜨리면 된다. 

읽어드리려는 이미지의 해상도를 알아내기 위해서는 BitmapFactory.Options의 inJustDecodeBounds = true로 셋팅해놓고 읽어 들이면 된다. 그러면 이미지를 메모리에 올려놓지 않고 해상도만 알아낼 수 있다. 다음의 코드를 보자. 

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

이미지를 메모리에 로드하지 않고 크기만 알아내는 코드이다. 여기서 decodeResource 함수는 decodeFile처럼 BitmapFactory.decode* 메소드들로 바꾸어서 쓸 수 있다. 이제 BitmapFactofy.Options의 inSampleSize 파라메터를 이용해서 이미지의 해상도를 줄일 수 있다. inSampleSize는 이미지의 몇분의 1로 해상도를 줄일지를 나타낸다. 예를들어 inSampleSize를 4로 셋팅하면 2048x1536의 이미지를 읽어들일 때 512*384로 가로세로 1/4크기로 줄여들여 읽어들인다.(면적은 1/16로) 자 이제 다음 코드처럼 이미지를 보여줄 화면의 크기만큼 해상도를 떨어뜨려 읽을 수 있다. 

 // Get the dimensions of the View
int targetW = mImageView.getWidth();
int targetH = mImageView.getHeight();
  
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
  
// Determine how much to scale down the image
int scaleFactor = Math.min(photoW/targetW, photoH/targetH);
  
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
  
Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
mImageView.setImageBitmap(bitmap);

위와 같이 실제 이미지를 읽어들일 때는 inJustDecodeBounds를 false로 해둔다. inSampleSize의 경우 2의 배수로 사용할 경우 퍼포먼스가 가장 좋긴 하지만 퍼포먼스보다 메모리를 조금이라도 아끼는게 중요하니 2의 배수가 아니더라도 줄일 수 있으면 더 줄이는게 좋다. 

2. 멀티 쓰레드로 이미지를 읽어들이자. 

이미지를 디스크나 네트워크로 부터 읽어들일 때는 메인 UI 쓰레드에서 읽어들이면 안된다. 시간이 얼마나 걸릴지도 모르는 것이고 마냥 기다리게 되면 사용자가 터치를 해도 반응이 없어서 닫으려고 할 것이기 때문이다. 따라서 이미지를 읽어들이는 작업은 별도의 쓰레드에서 해야 하는데 가장 쉬운 방법은 AsyncTask를 이용하는 것이다. 

다음은 AsyncTask를 이용하여 이미지를 읽어들이는 예제이다. 

 class BitmapWorkerTask extends AsyncTask {
    private final WeakReference imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

별도의 쓰레드에서 진행되므로 작업중에 사용자가 다른창으로 이동하거나 할 수 있다. ImageView의 WeakReference를 가지고 있게하여 사용자가 다른창으로 이동하거나 하면 작업이 끝나지 않았더라도 ImageView가 가비지 콜렉팅되게 하였다. 다만 onPostExcute에서 WeakReference에서 ImageView를 가져와 ImageView가 아직 살아있는지 여부를 체크하게 하였다. 

이제 이미지를 별도의 쓰레드에서 비동기식으로 로딩하려면 다음과 같이 실행해주면 된다. 

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

일반 뷰에서는 위와 같이 읽어 들이면 되지만 ListView나 GridView에서 사용하게 될 경우 문제가 발생하게 된다. ListView나 GridView는 메모리를 아끼기 위해 유저가 스크롤 하거나 할 때 이미 만들어 놓았던 뷰를 재활용하기 때문이다. 따라서 이미지 로딩 작업이 끝났을 때 업데이트 하려는 뷰가 이미 다른 이미지를 로딩하며 재활용된 뷰일 수도 있고 이미지 로딩이 언제 끝나느냐에 따라서 이미지를 업데이트 하는 작업이 꼬일 수도 있다. 

 Multithreading for Performance에서는 이 이미지 뷰를 가장 최근에 건드렸던 AsyncTask가 어떤 놈인지 기억해 두었다가 작업이 끝났을 때 그 AsyncTask가 가장 최근놈이 맞는지를 체크하는 방법으로 해결한다. 우리도 비슷한 방법으로 해결해 보자. 

아까 만들었던  workertask를 저장할 수 있는 Drawable클래스를 하나 만든다. 이경우에는 ImageView가 가지고 있는 BitmapDrawable을 사용할 거고 따라서 작업이 끝났을 때 ImageView는 자신이 갖고 있는 BitmapDrawable을 통하여 어느 workertask가 가장최근 놈인지 알 수 있다. 

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

그리고 BitampWorkerTask를 실행하기 전에, 타겟 ImageView를 가지고 있는 AsyncDrawable을 하나 만들거다. 

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

여기서 cancelPotentialWork 메소드에서 이미 다른 task가 ImageView를 참조하고 있고 돌아가고 있는지를 체크할 수 있다. 그래서 아직 돌고 있는 놈이 있으면 새로운 task를 실행하기 전에 돌고 있는 task를 cancel시켜 버린다. 다음은 cancelPotentialWork의 구현이다. 

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}

위의 코드에서 getBitmapWorkerTask() 메소드는 이 이미지뷰를 업데이트하기 위해 아직 실행중인 task를 리턴하는 메소드이다. 

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

마지막으로 작업이 끝나면 BitmapWorkTask에서 이미 cancel됐는지 여부를 체크하고 그렇지 않을 때는 업데이트 하도록 한다. 

class BitmapWorkerTask extends AsyncTask {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

이제 뷰를 재활용하는 ListView나 GridView에서  있더라도 이 샘플을 사용할 수 있게 되었다. 이제 loadBitmap을 getView()와 같은 곳에서 콜해서 이미지를 로드하면 된다. 

3. 이미지를 캐싱하자.

ListView나 GridView, ViewPager에서는 보통 여러장의 이미지를 동시에 로딩하게 된다. 그리고 사용자가 스크롤을 하게 되면 그 중에 화면에 보이지 않게 되는 이미지의 뷰는 재활용 될 것이고, 메모리가 별로 없기 때문에(이것들이 메모리를 많이 차지하는 작업들이라..)  예전에 로딩된 이미지는 곧 가비지 콜렉팅 될 것이다. 그럼 매번 다시 스크롤해서 올라가고 하면 하게 될 때마다 예전에 불렀던 이미지를 새로 다시 로드하고 하게 하는건 네트웍이 느린 안드로이드 폰에서는 뻘짓이다.  그래서 메모리나 디스크에 캐싱하는 방법을 사용한다. 

- 메모리 캐시를 사용해보자

안드로이드에서는 LruCache를 제공한다. LruCache는 LinkedHashMap을 사용하여 최근에 사용된 object의 strong reference를 보관하고 있다가 정해진 사이즈를 넘어가게 되면 가장 최근에 사용되지 않은 놈부터 쫓아내는 LRU 알고리즘을 사용하는 메모리 캐시다. 예전에는 bitmap cache에 SoftReference나 WeakReference를 사용하는 방식을 썼으나 안드로이드 2.3부터 가비지 콜렉터가 공격적으로 이놈들의 메모리를 가비지 콜렉팅하면서 몹쓸 방법이 됐다.  게다가 이 방법은 3.0이전 버전에서 메모리 해제를 제대로 못해 크래쉬문제도 있다. 

그러니 LruCache를 사용하자. LruCache의 캐시사이즈를 정하기 위해서는 다음 요소들을 고려해야 한다. 

  • 우리 앱에서 앞으로 메모리를 얼마나 써야 되는가. 
  • 얼마나 많은 이미지들이 한 화면에 보일 것인가. 얼마나 많은 이미지들이 다음에 보여주기 위해 준비되어야 하는가. 
  • 화면 해상도가 어떻게 되는가. 
  • 각 이미지마다 메모리를 얼마나 차지하는가. 
  • 이미지는 얼마나 자주 액세스 되는가. 
  • 질보다 양? 양보다 질? 어떤 경우에는 많은 양의 저해상도 이미지를 미리 보여주고 백그라운드로 고해상도 이미지를 로드하는 방법이 좋을 수도 있다. 

딱히 어느정도 캐시사이즈가 적당한지 공식은 없고 앱의 메모리 사용량을 측정해보면서 적당히 정해야 한다. 당연한 말이겠지만 너무 사이즈를 작게하면 괜히 오버헤드만 발생하게 되고 사이즈를 너무 크게했다가 OutOfMemory Exception을 보게 될거다. 아래는 LruCache를 사용한 예제이다.

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get memory class of this device, exceeding this amount will throw an
    // OutOfMemory exception.
    final int memClass = ((ActivityManager) context.getSystemService(
            Context.ACTIVITY_SERVICE)).getMemoryClass();

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = 1024 * 1024 * memClass / 8;

    mMemoryCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in bytes rather than number of items.
            return bitmap.getByteCount();
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

이 예제에서 앱에서 사용 가능한 메모리의 1/8을 캐시에 할당하였다. 일반적으로 hdpi 디바이스에서는 이 크기가 4MB(32/8)정도 된다. 800x480이미지가 1.5MB정도 되므로 GridView를 사용한다면 2.5 페이지의 이미지를 메모리에 캐싱할 수 있게 된다. 

이미지를 로드할 때 LruCache에서 먼저 찾아보고 있으면 그걸로 바로 업데이트 하고 아니면 백그라운드 쓰레드에서 로딩한다. 

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

BitmapWorkerTask도 메모리 캐시를 사용하는 걸로 변경. 

class BitmapWorkerTask extends AsyncTask {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

- 디스크 캐시 이용하기

메모리 캐시는 빠르기는 하지만 메모리가 얼마 되지 않기 때문에 이것만 가지고는 부족하다. 그리고 전화가 오거나 하면 앱이 백그라운드로 가버리면서 캐시가 사라져 버리게 된다. 디스크 캐시를 사용하면 데이터가 지속적으로 남아있고 용량도 좀 더 많이 쓸 수 있다. 하지만 당연히 메모리 캐시보다 느리다. 그래도 네트웍에서 읽어들이는 것에 비하면 어딘가. 그리고 ContentProvider를 사용하는게 자주 액세스 되는 이미지를 캐시에 저장하기에 좋다. 아래 코드는 위의 메모리 캐시에 디스크 캐시를 추가한 버전이다. 

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (mDiskCacheLock) {
            File cacheDir = params[0];
            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting = false; // Finished initialization
            mDiskCacheLock.notifyAll(); // Wake any waiting threads
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // Wait while disk cache is started from background thread
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

메모리 캐시에서의 로딩은 메인 쓰레드에서 진행된 반면 디스크 캐시는 백그라운드 로딩을 사용하였다. 디스크 작업은 언제 끝날지 모르므로 백그라운드를 사용하자. 그리고 이미지 프로세싱이 끝나면 메모리 캐시에도 업데이트를 해주었다.

=======================================================

이상입니다.  나머지 부분은 위에서 만든 코드를 실제 GridView나 ViewPager에 적용하는 내용인데 이건 직접 해보시면 될 것 같습니다. 사실 이 부분은 많이 쓰이는 부분이기 때문에 구글에서 검색하면 수많은 오픈소스를 만나실 수 있습니다.

오픈소스를 사용하셔도 되지만 위의 샘플코드를 가져다가 쓰는게 그렇게 어렵지 않고 또 이미 이해한 코드이기 때문에  커스터마이즈 하기도 좀더 수월하지 않을까 싶습니다. 또 위의 내용을 읽다보면 안드로이드가 이미지(비트맵)을 다루는 다양한 방식과 한계에 대해서도 이해하게 되는 부분이 있으리라고 생각합니다. 도움이 되셨길 바랍니다.

신고


Posted by injunech
2015.09.13 00:41


[Intro]

 

Android에서 사용하는 이미지는 Bitmap이라는 클래스에서 다~ 알아서 해줍니다.
그리고 이런 Bitmap Object를 쉽게 만들 수 있도록 도와주는 
BitmapFactory 클래스 라는 것도 있습니다.

 

BitmapFactory는 여러가지 소스로 부터 Bitmap Object를 만들어 주는 일을 하는데,
전부 static이며 decodeXXX 라는 이름을 가진 메소드들로 이루어져 있습니다.

XXX에는 어떤 것으로 부터 decode를 하여 
Bitmap Object를 만들어 낼지에 대한 말들이 들어 가겠죠.

 


[Decoding Methods]

 

BitmapFactory.decodeByteArray() 메소드는 Camera.PictureCallback 으로 부터 받은
Jpeg 사진 데이터를 가지고 Bitmap으로 만들어 줄 때 많이 사용 합니다.
Camera.PictureCallback에서 들어오는 데이터가 byte[] 형식이기 때문에
저 메소드를 사용 해야 하는 것이죠.

 

BitmapFactory.decodeFile() 메소드는 파일을 그대로 읽어 옵니다.
내부적으로는 파일 경로를 가지고 FileInputStream을 만들어서 decodeStream을 합니다.
그냥 파일 경로만 쓰면 다 해주는게 편리 한 것이죠.

 

BitmapFactory.decodeResource() 메소드는 Resource로 부터 Bitmap을 만들어 내며
BitmapFactory.decodeStream() 메소드는 InputStream으로 부터 Bitmap을 만들어 냅니다.
뭐 그냥 이름만 봐도 알 수 있는 것들이지요.

 


[OutOfMemoryError??]

 

보통 이미지 파일을 읽어서 Resizing을 해야 할 때가 있는데, 
그럴때는 BitmapFactory로 읽어서 Bitmap.createScaledBitmap() 메소드를 사용하여 줄이면

간단하게 처리 할 수 있습니다.

 

그런데 BitmapFactory를 사용할 때 주의해야 할 점이 있습니다.
아래의 예를 한번 보시죠.

Bitmap src = BitmapFactory.decodeFile("/sdcard/image.jpg");
Bitmap resized = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, true);

이미지 파일로부터 Bitmap을 만든 다음에

다시 dstWidth, dstHeight 만큼 줄여서 resized 라는 Bitmap을 만들어 냈습니다.
보통이라면 저렇게 하는게 맞습니다.

 

읽어서, 줄인다.

 

그런데 만약 이미지 파일의 크기가 아주 크다면 어떻게 될까요?
지금 Dev Phone에서 카메라로 촬영하면
기본적으로 2048 x 1536 크기의 Jpeg 이미지가 촬영된 데이터로 넘어옵니다.
이것을 decode 하려면 3MB 정도의 메모리가 필요 할 텐데,

과연 어떤 모바일 디바이스에서 얼마나 처리 할 수 있을까요?

 

실제로 촬영된 Jpeg 이미지를 여러번 decoding 하다보면

아래와 같은 황당한 메세지를 발견 할 수 있습니다.

java.lang.OutOfMemoryError: bitmap size exceeds VM budget

네... OutOfMemory 입니다.
더 이상 무슨 말이 필요 하겠습니까...
메모리가 딸려서 처리를 제대로 못합니다.

 

이것이 실제로 decoding 후 메모리 해제가 제대로 되지 않아서 그런 것인지, 
하더라도 어디서 Leak이 발생 하는지에 대한 정확한 원인은 알 수 없습니다.
이것은 엔지니어들이 해결해야 할 문제 겠죠...

 

하지만 메모리 에러를 피할 수 있는 방법이 있습니다.

 


[BitmapFactory.Options.inSampleSize]

 

BitmapFactory.decodeXXX 시리즈는 똑같은 메소드가 두 개씩 오버로딩 되어 있습니다.
같은 이름이지만 Signature가 다른 메소드의 차이점은
BitmapFactory.Options를 파라메터로 받느냐 안받느냐의 차이죠.

BitmapFactory.Options를 사용하게 되면 decode 할 때 여러가지 옵션을 줄 수 있습니다.


여러가지 많지만 저희가 지금 사용할 것은 inSampleSize 옵션 입니다.

 

inSampleSize 옵션은,
애초에 decode를 할 때 얼마만큼 줄여서 decoding을 할 지 정하는 옵션 입니다.

 

inSampleSize 옵션은 1보다 작은 값일때는 무조건 1로 세팅이 되며,
1보다 큰 값, N일때는 1/N 만큼 이미지를 줄여서 decoding 하게 됩니다.
즉 inSampleSize가 4라면 1/4 만큼 이미지를 줄여서 decoding 해서 Bitmap으로 만들게 되는 것이죠.

 

2의 지수만큼 비례할 때 가장 빠르다고 합니다.
2, 4, 8, 16... 정도 되겠죠?

 

그래서 만약 내가 줄이고자 하는 이미지가 1/4보다는 작고 1/8보다는 클 때,
inSampleSize 옵션에 4를 주어서 decoding 한 다음에,

Bitmap.createScaledBitmap() 메소드를 사용하여 한번 더 줄이면 됩니다.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap src = BitmapFactory.decodeFile("/sdcard/image.jpg", options);
Bitmap resized = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, true);

당연한 이야기 이겠지만,
내가 원하고자 하는 사이즈가 딱 1/4 크기라면

Bitmap.createScaledBitmap() 메소드를 쓸 필요가 없지요.

 

inSampleSize 옵션을 잘 활용하면 메모리 부족 현상을 대략적으로 해소 할 수 있습니다.
참고로 제가 저 옵션을 사용한 뒤로는 메모리 에러를 본적이 한~번도 없답니다.

 


[Appendix]

 

inSampleSize 옵션을 사용하면

SkScaledBitmapSampler Object (Library Level) 를 생성 하게 되는데,
Object를 만들때 정해진 SampleSize 만큼 축소하여 width와 height를 정한 뒤에 만들게 됩니다.
그러니까 애초에 축소된 사이즈로 이미지를 decoding 하는 것이죠.

 


[Outro]

 

Android의 기본 어플리케이션 소스를 분석 하다보면
상당히 테크니컬한 기법들을 많이 얻을 수 있습니다.
어떻게 이런 방법으로 만들었나 싶을 정도로 매우 정교하고 복잡하게 만들어져 있지요.
참 대단한 것 같습니다.





출처 - http://blog.naver.com/visualc98

신고


Posted by injunech
2015.09.08 02:24


안드로이드 앱을 만들때 배경이미지를 넣어야 하는 경우 OutOfMemoryError에 직면하는 경우가 많다.. 


특히 사이즈가 큰 배경이미지 일수록... 자주 발생한다...


즉, OutOfMemoryError는 이름처럼 이미지를 로딩할때 메모리가 부족해서 발생하는것이다...


이럴경우 그원인이 되는 이미지 사이즈 자체를 줄이거나 로딩할때 아래와 같이 사이즈를 줄여서 불러오면 일단 해결은 된다.



 


BitmapFactory.Options options = new BitmapFactory.Options();

options.inSampleSize = 2;


Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.opening, options);





하지만 이방법은 치명적 단점이 있다.


사이즈를 줄이므로 이미지가 선명하지 못하고 깨져서 보이며, 보통 LinearLayout과 같은 배경으로 많이 쓰이는 view에는 setBackgroundDrawable() 뿐이라서 bitmap을 Drawable로 변환해야 하는 번거로움이 있다...


이 두가지 단점을 한번에 해결하는 방법은 과연 없을까?


있다. 나도 항상 위와 같은 방법을 쓰다가 영 마음에 안들어서 여러 테스트를 거쳐 알아냈다...

물론 나중에 안거지만 다른분들 포스팅을 보니 이미 그렇게 사용하고 계시는 분들이 있으셨다..

역시 세상에는 코딩신님들이 많은것 같다ㅎㅎ 난 무한 샆질끝에 알아냈다고 좋아했는데ㅎㅎㅎ


방법은 무지 간단하다...





LinearLayout layout = (LinearLayout)findViewById(R.id.BgLayout);


layout.setBackgroundDrawable(new BitmapDrawable(getResources(), BitmapFactory.decodeResource(getResources(), R.drawable.opening)));





이렇게 로딩을 해서 바로 집어넣고 onDestroy()에서 메모리를 풀어주면 된다...





@Override

public void onDestroy() {

recycleView(findViewById(R.id.BgLayout));

}


private void recycleView(View view) {

if(view != null) {

Drawable bg = view.getBackground();

if(bg != null) {

bg.setCallback(null);

((BitmapDrawable)bg).getBitmap().recycle();

view.setBackgroundDrawable(null);

}

}

    }





참고로


xml에서 android:background="@drawable/opening" 로 바로 넣거나 layout.setBackgroundResource(R.drawable.opening) 또는  getResources().getDrawable(R.drawable.opening) 로 이미지를 가져오면 원본을 그대로 사용하는것이라서 메모리도 많이 잡아먹고 recycle를 하게 되면 다음번 실행때 에러가 난다...


결론은...


반드시 위와 같이 new로 복사본을 새로 생성해서 사용해야 메모리도 적게 먹고 recycle도 할수 있어서 OutOfMemoryError를 예방할 수 있다....






출처 : http://gyjmobile.tistory.com/

신고


Posted by injunech
2015.09.02 07:08


1. 함수의 원형

본격적으로 내용을 다루기에 앞서 함수 포인터의 원형과 포인터의 대상이 될 함수와의 원형은 같아야 한다.
이 점은 일반 서적에서 다루고 있는 내용이라 다 알 것이라고 생각되므로 함수의 원형에 대하여 간단하게
Q/A 식으로 짚고 넘어가겠다. 이해가 안되면 무조건 외우는 것도 한가지 방법이라고 생각됨.
마지막으로 함수 포인터는 함수가 아니라 원하는 함수의 주소를 담고 있는 변수이다. 근본적으로는 포인터
(변수 포인터)에 다른 변수의 주소를 넣는 것과 별로 다를 것이 없다. 다만 함수 포인터의 경우는 변수의
주소가 아닌 함수의 주소라는 점만 다를 뿐이다.

int a;
char c[5];
float* f;
"Hello, World"

Q) 위의 코드에서 a, c, f,
"Hello, World"의 원형은?
A)
int, char [], float *, const char *

만일 이 문제들을 못맞추었다면 다시 한번 책을 속독하기를 권함.

void foo();


const char* bar(int num);


class MyClass {
public:
   
void Func1(const string& txt);
   
static void Func2(const string& txt);
};

Q) foo, bar, MyClass::Func1, MyClass::Func2의 원형은?
A)
void (*)(), const char* (*)(int), void (MyClass::*)(const string&), void (*)(const string&)


2. 함수 포인터(전역 함수 포인터) / 멤버 함수 포인터

가. 어떻게 정의/선언하고 사용하는가?

    함수 포인터의 대상이 되는 함수의 원형을 적고 괄호안의 * 연산자의 뒤에 원하는 이름을 붙여서 사용.
    (괄호가 있는 이유는 없을 경우 함수의 리턴형이 * 연산자에 의해 원래 리턴형의 포인터로 처리되기
    때문에, 강력한 우선 순위를 가지는 괄호로 함수 포인터임을 명시.)

    예 1)

   
void foo();
   
void (*foo_ptr)();             // 로 정의/선언 후
    foo_ptr = &foo;                
// 로 함수의 주소를 대입 cf) void (*foo_ptr)() = &foo;
    (*foo_ptr)();                  
// 로 사용 cf) foo_ptr(); 도 가능

   
const char* bar(int num);
   
const char* (*bar_ptr)(int);   // 혹은 const char* (*bar_ptr)(int num);로 정의/선언
    bar_ptr = &bar;                
// 호출하려는 함수의 주소를 넣고
    (*bar_ptr)(5);                
// 로 사용 cf) bar_ptr(5); 도 가능

    예 2)

   
class MyClass {
   
public:
       
void Func1(const string& txt);
       
static void Func2(const string& txt);
    };

   
void (MyClass::*Func1Ptr)(const string&) = &MyClass::Func1;
   
void (*Func2Ptr)(const string&) = &MyClass::Func2;        

    MyClass mc;
    (mc->*Func1Ptr)(
"abc");
    (*Func2Ptr)(
"abc");            // 형태로 사용 cf) Func2Ptr("abc"); 도 가능

    예 3)

    만일 MyClass::Func1, MyClass::Func2에 대한 함수 포인터를 MyClass의 멤버 변수로 하려면

   
class MyClass {
   
public:
       
void Func1(const string& txt);
       
static void Func2(const string& txt);
       
void (MyClass::*Func1Ptr)(const string&); // 내부라도 MyClass 스코프임을 밝혀야 함.
       
void (*Func2Ptr)(const string&);
    };

    MyClass mc;
    mc.Func1Ptr = &MyClass::Func1;
// 클래스내에서 setter를 사용하면 그냥 &Func1
    mc.Func2Ptr = &MyClass::Func2;
// 편의상 public으로 지정해서 대입했음.

    (mc.*(mc.Func1Ptr))(
"abc");    // 또는 (mc.*mc.Func1)("abc");
    (*mc.Func2Ptr)(
"abc");         // 전역 함수 포인터와 유사하므로 mc.Func2Ptr("abc") 가능.

    cf) 다른 멤버 함수에서 멤버 함수 포인터를 사용할 때는
    (
this->*Func1Ptr)("abc");
    Func2Ptr(
"abc");               // (*Func2Ptr)("abc")

    물론 꼭 MyClass의 멤버 변수로 존재할 필요는 없다. 다른 클래스의 멤버 변수로도 가능하다.

    주의 : 위의 경우는 꼭 필요한 상황인지, 설계상의 문제는 없는지 다시 한 번 고려할 것.
           다른 방법으로도 충분히 해결되는 문제임.


나.
typedef의 사용법 & 함수 포인터 배열

    함수 포인터의 배열은 무척 간단하다. 위의 예제의 foo 함수의 경우에는

   
void (*foo_ptr[])() = {foo1, foo2, foo3, ...};     // 선언&정의 + 대입
    (*foo_ptr[i])();                                  
// 사용

    로 하면 된다. 다른 함수 포인터도 유사하다.

    위에서 밝힌대로 함수 포인터는 변수이다. 변수는 형을 가진다. 그러면 함수 포인터의 형은 무엇인가?
    호출하려는 함수의 원형이 그 함수 포인터의 형이다. 즉 
typedef를 사용할 수 있다는 말이 된다.
    foo 함수에 대한 함수 포인터의 경우를 예로 들면

   
typedef void (*foo_ptr)();

    형태로 사용한다. 다만
void (*foo_ptr)();는 foo_ptr이 함수 포인터의 이름인데 반해
   
typedef void (*foo_ptr)(); 에서 foo_ptr은 함수 포인터의 형, void (*)()을 뜻하게 된다.
   
int a처럼 선언하듯이 typedef를 쓰면 foo_ptr fp; 뿐만 아니라 배열 선언을 foo_ptr fp[5];
    처럼 쓸 수 있고, 한결 깔끔하고 편리해지게 된다.
    개인적으로 함수 포인터와
typedef는 꼭 같이 쓰기를 권유한다.

    예 1)

   
class MyClass {
   
public:
       
void Func1(const string& txt);
       
static void Func2(const string& txt);
    };

   
typedef void (MyClass::*Func1Ptr)(const string&);
   
typedef void (*Func2Ptr)(const string&);

    Func1Ptr fptr = &MyClass::Func1;      
// 한결 더 의미가 명확해지고 코드 중복을 피할 수 있다.
    Func2Ptr ftpr2 = &MyClass::Func2;

    MyClass mc;
    (mc.*fptr)(
"abc");
    (*fptr2)(
"abc");

    예 2)

   
class MyClass {
       
typedef void (MyClass::*Func1Ptr)(const string&);
       
typedef void (*Func2Ptr)(const string&);
   
public:
       
void Func1(const string& txt);
       
static void Func2(const string& txt);
        MyClass() : fptr(Func1), fptr2(Func2) { }
       
void Invoke(const string& txt)
        {
            (
this->*fptr)(txt);
            (*fptr2)(txt);
        }
   
private:
        Func1Ptr fptr;
        Func2Ptr fptr2;
    };

    MyClass mc;
    mc.Invoke(
"abc");


참고 사이트
http://www.newty.de/fpt/fpt.html#defi

신고


Posted by injunech
2015.09.02 06:50


< 함수 포인터 >

먼저 이 글은 포인터에 대한 이해를 필요로 한다.

포인터에 대한 기본지식이 있다고 가정하고 글을 쓰도록 하겠다.


int GetAreaEx( int x, int y )
{
    return x * y;
}



우선 이런 간단한 함수가 있다. 우리는 이 함수를 호출하기 위해 명시적으로

GetAreaEx( x, y );

이런식으로 기술해야 한다.

하지만 예를 들어 GetArea2, GetArea3, ..., GetAreaN 이런식으로 비슷한 함수가 존재하고

이를 상황에따라 다르게 호출해야 한다면 이 방식으로는 관리도 어려울 뿐더러 효율성도 떨어지고 코드량도 많이질 것이다.

또한 외부(스크립트 등)에서 어떤 특정한 함수를 호출하려 할때도 방법이 묘연할 것이다.


int (*GetArea)( int, int );

이 선언은 무엇일까?

언뜻보기에는 함수를 선언하는 것 같다.

이 선언은 함수에 대한 포인터를 선언한 것이다.

변수의 주소를 담는 포인터와 마찬가지로 함수포인터는 함수의 주소를 담는다.

GetArea = GetAreaEx; // 함수포인터 GetArea에 GetAreaEx()의 주소를 담는다
int nArea = (*GetArea)( x, y ); // (*GetArea)( x, y ); 로 GetAreaEx()함수를 호출하고 리턴받은 값을 nArea에 대입



이런식으로 GetAreaEx를 호출할 수 있다.

유의할점은 *GetArea를 꼭 ()로 감싸주어야 한다는 사실이다.

빼먹으면 컴파일러가 함수포인터를 통한 호출로 인식하지 못한다.


int (*GetArea[])( int, int ) = { GetAreaEx, GetArea2, GetArea3, ..., GetAreaN };

이것은 함수포인터 배열을 정적으로 선언한 것이다. 이렇게 배열로 기능이 비슷한 함수들을 묶어놓았다.

void CallFunc( int nState, int x, y )
{
    int nResult = (*GetArea[nState])( x, y );
}



그리고 그 함수들을 상황에 맞게 호출한다.

만약 함수포인터를 쓰지 않는다면

void CallFunc( int nState, int x, int y )
{
    int nResult;
    switch( nState )
    { 
         case STATE_EX:
              nResult = GetAreaEx( x, y );
         break;
         case STATE_2:
              nResult = GetArea2( x, y );
         break;
         case STATE_3:
              nResult = GetArea3( x, y ); 
         break;
    }
}


위와 같이 기술해야 할 것이다.

두 방식의 차이점과 함수포인터의 이점을 알 수 있겠는가

그렇다면 함수포인터 배열을 동적으로 할당하는 방법은 없을까?

다음과 같은 방법으로 할당할 수 있다.

int (**GetArea)( int, int ); // 함수포인터의 포인터
GetArea = new (int (*[N])( int, int )); // N은 배열의 크기



그리고 다음과 같이 사용하면 된다.

GetArea[0] = GetAreaEx;
GetArea[1] = GetArea2;
GetArea[2] = GetArea3;
...

int nResult = (*GetArea[nState])( x, y );



물론 사용후 delete [] GetArea; 해서 해제하는것을 잊으면 안된다.



< 클래스 멤버함수의 함수포인터화 >

함수포인터는 함수의 주소값을 담는다고 했다.

그렇다면 클래스 멤버함수의 주소값도 단순히 함수포인터에 담아서 호출할 수 있지 않을까?

int (*func)();
func = CFunc::GetArea;


하지만 이 방법은 GetArea()멤버함수가 static으로 선언되었을 때만 가능하다.

static으로 선언되지 않은 멤버함수(멤버변수를 건들여야 하는 멤버함수)를 이 방법으로 담으려 한다면 컴파일 에러가 뜰 것이다.

여기에 다음과 같은 해결방법이 있다.

첫번째 방법은

class CFunc
{
public:
    static int GetArea( CFunc * cls, int x, int y );
};



위와 같이 선언하고 호출할때 해당 인트턴스의 포인터를 넘겨줘서

int GetArea( CFunc * cls, int x, int y )
{
    int a = cls->GetZ();
}



이런식으로 멤버변수를 읽거나 쓸수 있겠지만 이 방식으로는 한계가 있다.

Get, Set 같은 public 외부함수로 억세스하지 않으면 private나 protected안에 선언되어 있는

멤버변수는 건드릴 수 없다.

두번째는 멤버함수의 소속을 명시화하는 방법이다.

int (CFunc::*func)( int, int );
func = CFunc::GetArea;
CFunc A;
(A.*func)( x, y );



위와 같은 방법으로 해결가능하다. 물론 호출할 인스턴스가 명확해야 한다.

세번째는 클래스 안에 함수포인터를 멤버변수로 두고 별도의 함수포인터를 컨트롤하는 멤버함수를 만드는 방법이 있다.

이 방법이 멤버함수 관리가 가장 쉬우며 효율적이다.

class CFunc
{
public:
    int (CFunc::*pFunc)( int, int );
    int GetArea( int x, int y );
    void CallFunc( void ) { (this->*pFunc)( x, y ); } // CallFunc 함수호출시 자체 오버헤드를 줄이기 위해 inline
    CFunc();
    ~CFunc() {}
};

CFunc::CFunc()
{
    pFunc = GetArea;
}
int CFunc::GetArea( int x, int y )
{
    return x * y;
}


위와 같다면 CallFunc(); 로 GetArea 호출이 가능해진다.

지금은 단순히 한개의 멤버함수 호출만 할뿐 의미가 없다. 이제 실제 효율적으로 쓰이게 배열을 써보자.

class CFunc
{
public:
    int (CFunc::**pFunc)( int, int );
    int GetArea( int x, int y );
    void CallFunc( int nState, int x, int y ) { (this->*pFunc[nState])( x, y ); }
    CFunc();
    ~CFunc();
};

CFunc::CFunc()
{
    // init
    pFunc = new (int (CFunc::*[10])( int, int )); // 동적할당, 10에는 원하는 멤버함수 갯수만큼
    // 0번은 남겨둔다.

    pFunc[1] = GetArea;
    pFunc[2] = GetAreaEx;
    pFunc[3] = GetArea2;
    pFunc[4] = GetArea3;
    ...
    pFunc[9] = GetArea9;
}
CFunc::~CFunc()
{
    delete [] pFunc; // 해제
}
int CFunc::GetArea( int x, int y )
{
    return x * y;
}


자, 이제 함수하나의 호출로 상황에따라 여러 멤버함수를 호출할 수 있는 기반이 마련되었다.

CFunc A;
A.CallFunc( nState, x, y );



이렇게...

어떠한가. 함수포인터의 위력이 느껴지는가?




< STL을 이용한 함수포인터 관리 >

우리는 지금까지 함수포인터를 동적으로 배열을 할당해서 써왔다.

함수 포인터를 STL(Standard Template Library)을 써서 관리해보자.

클래스의 멤버함수의 함수포인터화에서 3번째 방법을 조금 개선시켜 보겠다.



단순히 인덱스(숫자)를 이용한 관리라면 deque정도가 괜찮을듯 싶으나,

만약 함수의 이름을 문자열로 호출하고 싶다면 map을 써볼 수 있다.

(만약 FuncCall( "GetArea", x, y ); 이런식으로 멤버함수를 호출하고 싶다면)

map은 내부적으로 트리구조를 가지고 있다.

그래서 따로 정적/동적으로 배열을 할당하지 않아도 입력된 값을 비교해서 스스로 자신의 크기를 늘린다.

mapValue["GetArea"] = 99;

이런식으로 []안에 숫자 뿐만아니라 비교할 수 있는 모든 것이 들어갈 수 있다.

먼저 map을 사용하기 위해

#include < map >
using namespace std;



를 선언한다. map은 표준 네임스페이스를 사용하므로 std의 이름공간을 활용한다.

map< []안에 들어갈 타입, 입력될 데이터타입, 비교 논리 > mapFunctor;

선언방법은 이렇게 되는데 비교 논리는 첫번째 인수가 클래스이고 안에 비교오퍼레이터가 있다면 생략가능하다

자, 이제 해보자.



struct ltstr
{
    bool operator() ( const char * s1, const char * s2 ) const
    {
         return strcmp( s1, s2 ) < 0;
    }
};

class CFunc
{
public:
    typedef int (CFunc::*_Func)( int, int );
    map< const char *, _Func, ltstr > mapFunctor;
    int GetArea( int x, int y );
    void CallFunc( const char * szFuncName, int x, int y ) 
    { 
         (this->*mapFunctor[szFuncName])( x, y ); 
    }

    CFunc();
    ~CFunc();
};

CFunc::CFunc()
{
    // init
    mapFunctor["GetArea"] = GetArea;
    mapFunctor["GetAreaEx"] = GetAreaEx;
}
CFunc::~CFunc()
{
    // map 클리어
    mapFunctor.clear();
}
int CFunc::GetArea( int x, int y )
{
    return x * y;
}


char * 대신 string을 사용한다면 string안에 내부적으로 비교 오퍼레이터함수가 있기 때문에

map< string, _Func > mapFunctor;

이렇게 선언하고 사용할 수 있을 것이다.

이제 A.CallFunc( "GetAreaEx", x, y ); 란 호출로 GetAreaEx를 호출할 수 있다.

이 방식은 여러가지로 응용가능한데 스킬명에 의한 화면효과 호출이라던지

C로 미리 작성된 내부 함수를 외부 스크립트로 호출한다던지 할때 유용하게 쓰일 수 있다.

(스크립트 호출일 경우 함수이름을 인덱스화 해서 deque를 쓰는게 속도상 더 유리할 듯 하다)






출처 : http://zeph.tistory.com/155



신고


Posted by injunech
2015.08.23 22:46


1. Node

cocos2d-x의 기본 단위

거의 모든 객체들은 이 Node클래스를 상속받는다. 이것만 잘 알아도 cocos2d-x의 많은 부분을 알수 있다.

http://www.cocos2d-x.org/reference/native-cpp/V3.0alpha0/d3/d82/classcocos2d_1_1_node.html

Node의 멤버들..

-position : 위치 값,

-scale : 크기 배율값,

-rotation : 회전 각도,

-anchor point : 앵커 포인터(위치값의 기준 점을 설정 0~1까지 float),

-size : width, height,

-visible : 보이는지 여부,

-z-order : 출력시 위에 보이느냐 아래 보이느냐를 결정하는 수치

node->setPosition(Point(0, 0));
//대부분의 속성값은 get / set으로 사용가능

부모자식관계

Node의 가장 중요한 특징은 부모자식관계라고 생각한다. 클래스 상속과는 별개의 영역이며, 햇갈려서는 안된다.

자식은 부모의 영역안에서 상대적인 좌표로 이동, 회전, 스케일된다. 예를 들어 부모노드가 100, 100 좌표에 위치한다면, 그 자식인 노드의 좌표값이 0, 0 이라도 부모노드의 상대적 좌표값이 적용되어 실제로 전체화면에서는 100, 100 좌표에 있는 것 처럼 보인다.

반대로 여러 자식들을 가지고 있는 부모노드의 위치 스케일 등을 변경하면 그 안에 들어있는 모든 자식노드들의 값이 한꺼번에 변경된다. 이 때문에 부모 노드는 자식 노드를 담는 컨테이너로 사용되기도 한다.

parent->setPosition(Point(100, 100)); // 부모를 100, 100위치에 놓는다.
auto child = Node::create(); //자식을 만든다.
child->setPosition(Point(0,0));//자식을 0,0에 놓는다.
parent->addChild(child) //자식이 100, 100자리에 그려진걸 보고서 당황하지 않는다.

스마트 포인터

Node를 상속받는 모든 클래스들은 CREATE_FUNC(ClassName)으로 등록하면, ClassName::create()하는 것만으로 자동으로 메모리를 할당하고 알아서 해제가 된다.

새로운 인스턴스 생성할때, virtual bool init()이라는 함수를 자동으로 호출하므로 반드시 init함수의 형태를 구현해두어야한다.

기본적으로 레퍼런스 카운트 방식을 사용한다. addChild(node)하면 알아서 카운트가 1이 증가하고, removeChild(node)하면 카운트가 1줄어든다. 카운트가 0이되면 사라진다.

만약 할당한 메모리를 유지하고 싶다면, 인스턴스 객체에 node.retain()을 걸어주면 수동으로 카운트가 1 증가한다. 반대로 node.release()를 걸어주면 수동으로 카운트가 1 줄어든다. 하지만 메모리 릭이 발생하지 않도록 주의해서 관리해야한다.

스케쥴

http://injakaun.tistory.com/102

노드를 상속받은 클래스들은 스케쥴을 통해 게임 루프를 처리해주는 스케쥴러에 특정 함수를 등록할 수 있다. 스케쥴로 등록한 함수는 게임 루프시마다 호출된다. 주로 각 객체의 update함수들이 등록되어 루프할때마다 호출된다.

schedule( schedule_selector( GameLayer::update ) , 0.1f );
//schedule_selector의 인자로 함수포인터를 넘겨주는데, 반드시 앞에 어떤 클래스의 함수인지를 써줘야한다.
//그리고 뒤에 이 함수가 실행되는 딜레이를 적어준다.
scheduleUpdate(); //귀찮으면 이걸 쓴다. 대신 update함수는 노드에서 제공하는 형식을 맞춰줘야한다.

무작정 스케쥴업데이트를 사용하면 업데이트 순서가 꼬일 수 있으니 주의하자. 자식노드의 업데이트를 부모노드의 업데이트에 몰아서 한큐에 처리하는 방식으로 꼬임 문제를 해결하였다. sheduleUpdateWithPriority를 사용하는 것도 방법이 될 수 있을 거라 생각한다.

unschedule을 통해서 스케쥴러에서 제외시킬 수 있고, pause와 resume을 통해 잠시 멈췄다가 다시 수행하게 할 수 있다. scheduleOnce를 잘쓰면 괜히 여러번 호출할 필요없이 한번만 불러오게 할 수도 있다.

*removeChild이나 changeScene을 하면 그 안에 있던 객체는 retain을 해서 남아있더라도 그 객체에서 shedule된 함수들이 스케쥴러에서 제외된다. 제외된 함수들은 다시 스케쥴러에 넣어주지 않는한 루프에 호출되지 않는다.

2.Node의 상속자들

cocos2d::Scene / cocos2d::Layer / cocos2d::Sprite

Director

디렉터는 Node의 상속자는 아니지만 다른 상속자들과 밀접한 관련이 있기에 미리 언급하고 넘어간다. 디렉터는 cocos2d-x 내부에 있는 싱글톤 클래스로 화면을 관리하는 역할을 담당한다. 주로 화면에 출력할 Scene을 선택하고 전환하는 역할로 사용되었다. 추가로 다양한 씬 전환기법도 제공한다.

http://injakaun.tistory.com/98

Director::runWithScene(menuScene); //일단 디렉터로 씬하나 띄워야 뭐가 실행된다.
Director::getInstance()->replaceScene(gameScene); //적당히 조건에 맞게 씬을 변경한다.
//replaceScene을 하면 기존에 있던 씬은 제거(refCount--)된다. 추가로 업데이트들도 스케쥴에서 제외되므로 주의한다.

Scene

씬은 cocos2d-x에서 가장 기본이 되는 화면이다. Node의 상속자들의 부모자식관계망에서 제일 꼭대기에 위치한다. Scene에는 직접 스프라이트같은걸 붙이지는 않는다. 세부적인 화면 형태는 Layer에서 결정하고, 그런 Layer들을 모아서 한번에 출력하는 컨테이너로 Scene을 사용하는게 편리하다.

Layer

Layer도 Scene이랑 크게 다를것은 없으나 Scene과는 달리 여러개가 동시에 한 화면에 올릴 수 있기 때문에 각 분류에 따라 만들어지는 경향이 있다. 우리 프로젝트에서도 UI, Game, Background Layer들로 나누어서 각각 자신의 처리를 할 수 있게 하였다. 그리고 각 레이어들의 z축 값을 다르게 줘서 특정 레이어를 위에 특정 레이어를 아래 배치할 수도 있다.

auto gameLayer = Layer::create(); //게임 레이어를 만들어줍니다.
auto uiLayer = Layer::create(); //UI레이어도 만들어요
auto gameScene = Scene::create(); //게임이 돌아가느 화면인 게임씬을 만들고
gameScene->addChild(gameLayer, 1); //게임 레이어를 게임씬에 붙입니다. z축 값을 1로줘서 
gameScene->addChild(uiLayer, 2); //z축 값을 2 로 줘서 들어간 ui레이어는 항상 게임레이어보다 위에 출력됩니다.
Director::runWithScene(gameScene); //이걸 디렉터한테 실행시킵니다.

Sprite

실제 이미지가 올라가는 대부분의 노드가 Sprite이다. Sprite도 다른 Sprite를 자식노드로 가질 수 있기 때문에, 플레이어스프라이트에 장비스프라이트를 자식 노드로 붙여 계속 플레이어를 따라다니는 것처럼 만들어 줄 수 있다.

그리고 스프라이트는 여러장의 이미지들을 계속 변경하는 에니메이션을 추가하여 실제 움직이는 객체처럼 만들어 줄 수도 있다.

신고


Posted by injunech

티스토리 툴바