제가 요즘 폰갭(PhoneGap)을 이용하여 하이브리드 형태의 안드로이드앱을 개발하고 있습니다. 폰갭은 하이브리드앱(웹앱) 개발 프레임워크 중 하나인데요, 웹페이지에서 폰의 다양한 기능/자원을 불러다 쓸 수 있게 해줍니다. 폰갭에서 이러한 기능들은 플러그인(plugin)이라 불리는 것으로 제공됩니다.

폰갭 플러그인 구분
폰갭 플러그인은 앱영역(java)에서 플러그인이 실행되는 방식에 따라 동기식 플러그인과 비동기식 플러그인으로 나눌 수 있습니다. 좀더 자세히 설명하자면, 웹영역(webview)에서 javascript 코드를 통해 플러그인을 호출한 후 플러그인의 실행이 완료되기를 기다리면서 블럭되면 동기식이고 그렇지 않으면 비동기식입니다.

폰갭 플러그인을 제작하려면 com.phonegap.api.Plugin 클래스를 상속받아야 하는데, 이 Plugin 클래스에 isSynch() 메소드가 있습니다. 기본적으로 false를 리턴합니다. 그러니까 이 메소드를 오버라이딩해서 true를 리턴하지 않는 이상 해당 플러그인은 비동기식으로 작동합니다.
비동기식 플러그인의 경우 플러그인 실행결과를 수신(콜백함수 호출)하기 위해 XMLHttpRequest를 이용하거나 또는 Polling 방식을 사용합니다.


 
동기식 플러그인 실행메커니즘


1. Javascript로 "window.plugins.플러그인명.액션명(...)"과 같이 플러그인 함수를 호출하게 되면, phonegap.js 내부적으로 PhoneGap.exec() 함수가 실행됩니다. PhoneGap.exec()에서는 prompt 함수를 실행하면서 플러그인 실행에 필요한 파라미터를 넘겨줍니다.

var r = prompt(xxx, "gap:xxx");


Javascript의 prompt() 함수가 호출되면 앱영역에서는 DroidGap$GapClient.onJsPrompt() 메소드가 실행됩니다. (아하, 폰갭은 Javascript -> Java 호출을 위해 prompt()를 활용하는군요.)

2. DroidGap$GapClient.onJsPrompt() 메소드에서는 prompt의 default value가 "gap:xxx" 형태의 문자열인 경우 PluginManager.exec(...) 메소드를 실행합니다. PluginManager.exec()에서는 아래와 같이 플러그인을 실행하고 그 결과를 JSON 스트링으로 리턴합니다.

// 우선 Plugin이 동기식인지 아닌지 판단 : Plugin.isSynch()
// 동기식인 경우 current thread에서 플러그인을 실행
// (아래 코드는 원래 코드를 단순화한 것입니다)

PluginResult cr = plugin.execute(xxx);
return cr.getJSONString();


3. DroidGap$GapClient는 PluginManager로부터 리턴받은 플러그인 실행결과 스트링을 prompt의 confirmation 응답으로 설정합니다.

jsPromptResult.confirm(플러그인 실행결과);


4. 비로소 웹영역에서는 앱영역에서의 플러그인 실행결과를 prompt 함수의 리턴값(var r 변수)으로 받아냈습니다. 동기식 플러그인의 경우 r 리턴문자열이 반드시 존재하며 r.status에 따라 success 또는 failure 콜백 함수를 호출합니다. 폰갭은 (일단 동기식 플러그인에 대해서) Java -> Javascript 콜백 호출을 위해 prompt 리턴값을 활용함을 알 수 있습니다.


비동기 플러그인 실행메커니즘(XMLHttpRequest 적용시)


1. com.phonegap.CallbackServer는 java.net.ServerSocket을 이용하여 간단한 폰내부 로컬 웹서버를 띄웁니다. 이 웹서버는 웹영역의 XMLHttpRequest(XHR) 요청에 응답합니다.

2. phonegap.js의 PhoneGap.JSCallback 함수에서 로컬 웹서버와 ajax 통신하기 위한 연결을 맺습니다.

xmlhttp.open("GET", "http://127.0.0.1:xxx/xxx", true);
xmlhttp.send(); 


이 커넥션은 오랫동안 살아있는(long-lived) 연결이며, CallbackServer는  연결을 계속 유지하기 위해 10초간격으로 비어있는 응답을 계속해서 송신합니다(callback ping).

3. (동기식 플러그인과 동일하므로 설명생략)

4. PluginManager.exec()에서는 아래와 같이 비동기 플러그인의 경우 별도의 쓰레드를 만들어서 플러그인을 실행하고 그 결과를 CallbackServer에 적재합니다.

// 우선 Plugin이 동기식인지 아닌지 판단 : Plugin.isSynch()
// 비동기식으로 정의된 경우 새로운 thread에서 플러그인을 실행
// (아래 코드는 원래 코드를 단순화한 것입니다)

new Thread(new Runnable() {
    public void run() {
       PluginResult cr = plugin.execute(xxx);
       ....
       ctx.sendJavascript(xxx);
    }
});
thread.start();
return ""; // 플러그인 실행이 끝날때까지 기다리지 않고 즉시 리턴

5. CallbackServer는 새로운 플러그인 실행결과(javascript문자열)가 공급되면 그것을 기존에 연결된 XHR에 응답으로 씁니다. 

// (아래 코드는 원래 코드를 단순화한 것입니다)
response = "HTTP/1.1 200 OK\r\n\r\n";
String js = this.getJavascript();
response += URLEncoder.encode(js, "UTF-8");
output.writeBytes(response); 


6. 앞서 콜백서버와 연결된 xmlhttp 자바스크립트객체는 응답으로 수신한 문자열을 통해 콜백 함수를 호출합니다(PhoneGap.JSCallback 함수 참조).

// (아래 코드는 원래 코드를 단순화한 것입니다)
if (xmlhttp.status === 200) {
    var t = eval(xmlhttp.responseText);
}



비동기 플러그인 실행메커니즘(Polling 적용시)


1. phonegap.js에서는 PhoneGap.JSCallbackPolling 함수를 주기적으로 실행합니다(기본 50ms 마다).

var msg = prompt("","gap_poll:");


2 ~ 3. (XHR 적용 비동기식 플러그인과 동일하므로 설명생략)

4. 폴링에 의해 호출된 DroidGap$GapClient.onJsPrompt() 메소드에서는 CallbackServer에 적재된 javascript문자열을 prompt의 confirmation 응답으로 설정합니다.

5. prompt 함수의 리턴값(var msg 변수)이 존재하면 그것을 evaluation하여 콜백 함수를 호출합니다.

XHR을 적용할 건가 Polling을 적용할 건가
폰갭은 비동기식 플러그인 실행결과를 수신하기 위해 default로 XMLHttpRequest를 이용합니다. 그러나 외부 웹서버로부터 로딩된 html에서는 결과를 수신하지  못하는 문제가 있습니다. 이는 cross domain 보안제약 때문이며, 안드로이드 WebView가 강제로 Ajax 응답연결을 끊어버려 javascript 콜백함수가 실행되지 않게 됩니다(file:// 주소에 의해 로딩된 html에는 이런 제약이 없습니다). 이 경우 XHR 방식 대신 Polling 방식을 적용해야 합니다.
즉, file:// 주소에 의해 로딩된 웹페이지가 아닌 경우 Cross-Domain Security 제한이 존재하므로 phonegap.js에서 PhoneGap.UsePolling 값을 true로 수정해주어야합니다.

PhoneGap.UsePolling = true;

 
Polling 방식 적용시 주의점
그러나 폴링방식 적용시 폰갭은 비동기식 플러그인의 실행결과를 수신하기 위해 매우 짧은 주기로 App 영역과 통신하므로, 폰갭 기능이 필요하지 않은 웹페이지에서는 phonegap.js를 포함하지 않는 것이 좋습니다. 이 문제는 향후 어떻게든 개선될 것으로 보입니다.

ps. 위 내용은 폰갭 0.9.5 버전을 분석한 것입니다.
 
Posted by 에코지오
,
android.text.InputFilter를 이용해서 사용자의 텍스트입력을 다양한 방식으로 필터링할 수 있습니다. 
입력문자를 모두 대문자로 바꾸거나(InputFilter.AllCaps 이용), 문자열의 길이를 제한(InputFilter.LengthFilter 이용)할 수 있죠. 그밖에 다양한 필터를 만들 수 있을 겁니다. (정규식을 적용한 필터 예제 : http://flysky.thoth.kr/blog/4208673)

그런데 안드로이드가 기본으로 제공하는 InputFilter.LengthFilter는, 글자수(캐릭터 수)로 문자열의 길이를 계산하기 때문에 바이트 수로 길이를 제한하고 싶은 경우에는 사용할 수 없습니다. (한글처럼 문자열에 non-ascii 글자가 포함되면 글자 수 < 바이트 수).

아래 ByteLengthFilter는 바이트 길이로 입력을 제한해주는 필터입니다.

import android.text.InputFilter;
import android.text.Spanned;

/**
 * EditText 등의 필드에 텍스트 입력/수정시 
 * 입력문자열의 바이트 길이를 체크하여 입력을 제한하는 필터.
 *
 */
public class ByteLengthFilter implements InputFilter {

    private String mCharset; //인코딩 문자셋

    protected int mMaxByte; // 입력가능한 최대 바이트 길이

    public ByteLengthFilter(int maxbyte, String charset) {
        this.mMaxByte = maxbyte;
        this.mCharset = charset;
    }

    /**
     * 이 메소드는 입력/삭제 및 붙여넣기/잘라내기할 때마다 실행된다.
     *
     * - source : 새로 입력/붙여넣기 되는 문자열(삭제/잘라내기 시에는 "")
     * - dest : 변경 전 원래 문자열
     */
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
            int dend) {

        // 변경 후 예상되는 문자열
        String expected = new String();
        expected += dest.subSequence(0, dstart);
        expected += source.subSequence(start, end);
        expected += dest.subSequence(dend, dest.length());

        int keep = calculateMaxLength(expected) - (dest.length() - (dend - dstart));

        if (keep <= 0) {
            return ""; // source 입력 불가(원래 문자열 변경 없음)
        } else if (keep >= end - start) {
            return null; // keep original. source 그대로 허용
        } else {
            return source.subSequence(start, start + keep); // source중 일부만 입력 허용
        }
    }

    /**
     * 입력가능한 최대 문자 길이(최대 바이트 길이와 다름!).
     */
    protected int calculateMaxLength(String expected) {
        return mMaxByte - (getByteLength(expected) - expected.length());
    }    
    
    /**
     * 문자열의 바이트 길이.
     * 인코딩 문자셋에 따라 바이트 길이 달라짐.
     * @param str
     * @return
     */
    private int getByteLength(String str) {
        try {
            return str.getBytes(mCharset).length;
        } catch (UnsupportedEncodingException e) {
            //e.printStackTrace();
        }
        return 0;
    }
}    

EditText에는 이렇게 적용하면 됩니다.

int maxByte = ...;
EditText editText = ...;

InputFilter[] filters = new InputFilter[] {new ByteLengthFilter(maxByte, "KSC5601")};
editText.setFilters(filters);

Posted by 에코지오
,

* SQLite 데이터베이스에서 ERD 추출하기
 - ERD 추출 지원툴 => SQLite Maestro : http://www.sqlmaestro.com/products/sqlite/maestro/

* LogCat 한글 메시지 보는 방법
 - 현재 이클립스에서는 불가하며, DOS 콘솔에서만 가능
 
* DOS 콘솔에서 sqlite 한글 데이터 보는 방법
 - Logcat 한글메시지 확인방법과 동일한 원리

* 네트워크 패킷 모니터링 방법
 - WebKit 브라우저의 경우 프록시 설정하는 방법도 가능
 - 이클립스에서 편하게 모니터링하는 방법 찾는 중...(TCP/IP Monitor 플러그인 + adb forward 조합으로 가능할지?)

* 현재 실행중인 화면이 어떤 액티비티 클래스인지 알아내는 방법
 - Hierachy Viewer 이용
 - Activity Stack 조사 : ActivityManager 이용
 - Activity Stack 내용을 뿌려주는 adb 명령어가 있으면 좋을 듯...

* 프로세스에서 현재 살아있는 View/Activity 객체 개수 알아내기
 - dumpsys meminfo 명령어 이용

* Android.mk 구조 및 사용법 

* 코드를 통한 Activity 콘트롤
 - Activity 생명주기 제어 및 강제 이벤트 발생 가능


* 아직 찾지 못한 팁
- 안드로이드 class loader 구조 : 프레임워크 클래스, 애플리케이션 클래스. 우선 순위, 변경 방법
- Dalvik GC log 파일 만드는 방법
- 반복 테스트(동일 작업을 반복)를 쉽게 하는 방법

Posted by 에코지오
,
메소드 프로파일링을 통해 애플리케이션이 실행되는 동안 어떤 쓰레드의 어떤 메소드가 얼마나 오래 수행되었는지 알 수 있습니다. 
(안드로이드에서는 profiling을 tracing이라고도 부르더군요)

Trace 파일 생성

1. 코딩을 통한 생성
Debug 클래스의 startMethodTracing("trace-base-name") 메소드와 stopMethodTracing() 메소드를 통해 프로파일링을 시작/종료할 수 있습니다. 보통 activity의 onCreate()에 Debug.startMethodTracing("trace-base-name"); 를 넣고, onDestroy()에 Debug.stopMethodTracing();를 넣어 액티비티가 생성되고 소멸되는 동안 프로파일링을 수행하게 합니다.

프로파일링이 끝나면 /sdcard/<trace-base-name>.trace 파일이 생성됩니다. (반드시 sdcard가 있어야하며 애플리케이션이WRITE_EXTERNAL_STORAGE 퍼미션을 가져야합니다)

2. DDMS에서 생성
DDMS에서 프로세스 선택후 "Start Method Profiling" 메뉴를 선택하면 프로파일링이 시작됩니다. 다시 메뉴를 선택하면 프로파일링이 종료됩니다.

프로파일링이 끝나면 /sdcard/<패키지명>.trace 파일이 생성되면서 자동으로 TraceView가 실행됩니다.

3. shell에서 생성
adb shell로 진입해서 am profile 명령어로 특정 프로세스에 대해 프로파일링할 수 있습니다.

$ adb shell
# am profile <PPRCESS ID> start <TRACE FILE> (프로파일링 시작)
# am profile <PPRCESS ID> stop (프로파이링 종료)

예를 들면,
# am profile 16434 start /sdcard/myprofile.trace

(이제 애플리케이션에서 문제가 되는 기능을 사용해본다)

# am profile 16434 stop

4. 에뮬레이터 시작옵션
에뮬레이터를 사용한다면 에뮬레이터 시작시 emulator.exe -trace <name> 옵션을 준 후, 에뮬에서 F9 누르면 트레이싱이 시작되고 다시 F9 누르면 트레이싱이 종료됩니다. 이 경우 패키지 관계없이 전체가 프로파일링 됩니다(native tracing).
트레이싱이 끝나면 C:\Documents and Settings\Administrator\.android\avd\AVD이름.avd\traces\<name> 폴더에 qtrace.* 형태의 여러 파일들이 생성됩니다. 그러나 이들 파일은 테스트해 본 바로는 현재 TraceView로 분석 불가합니다.


Trace 파일 분석

1. TraceView
trace 파일은 SDK에 포함된 traceview 툴을 이용하여 그래픽컬하게 분석할 수 있습니다.

$traceview <trace파일의 절대경로> (단, .trace 확장자 제외)

예를들어 trace파일이 C:/myapp.trace 이면 $ traceview C:/myapp

traceview 분석화면은 timeline 패널과 profile 패널로 나뉩니다.

2. Timeline 패널 
timeline 패널은 각 쓰레드별로 메소드가 언제 시작/종료됐는지 색상으로 표시합니다. 주로 main 쓰레드에서 대부분 일이 처리되고 있음을 알 수 있습니다.(마우스를 드래그하여 구간을 선택하면 확대됩니다)



3. Profile 패널 
profile 패널은 메소드 내부 상세 수행시간을 표시합니다.



profile 패널의 각 컬럼의 대략적인 의미는 다음과 같습니다.
  • Name : 클래스의 메소드명, 맨 앞의 숫자는 메소드 호출 순서(call reference)
  • Inclusive : 해당 메소드 및 그것이 호출하는 메소드(자식메소드)를 포함한 수행시간.
  • Exclusive : 해당 메소드 자체 순수 수행시간(메소드는 여러번 수행될 수 있으며 이들을 모두 합친것임)
  • Calls : 전체 tracing 기록 중에서 몇번 호출되었는지.
  • Recur Calls : recursive call이 몇 번인지(?)
  • Time/Call : 해당 메소드 1번 호출 당 수행시간.
각 컬럼을 클릭하면 해당 컬럼 기준으로 정렬하여 볼 수 있습니다. 

또한 각 메소드를 선택하면 가지가 펼쳐지면서 Parents와 Children이 나타납니다.
  • Parents : 해당 메소드를 호출한 놈. caller
  • Children : 해당 메소드가 호출한 놈. callee
Children에서 self는 다른 메소드를 호출하지 않는 순수 자체 코드가 점유한 시간을 의미합니다.



분석 요령
profile 패널을 통해서 어떤 메소드가 애플리케이션 수행시간을 다 까먹고 있는지, 어떤 메소드가 많이 호출되었는지 등의 중요한 정보를 구할 수 있습니다. 

먼저 "Incl% 컬럼"을 기준으로 수행시간 점유비율이 뚝 떨어지는 곳들을 찾습니다. 아래 그림에서는 1번과 2번 메소드의 차이가 대략 30% 포인트 차이가 납니다. 1번 메소드를 펼쳐 Children을 봅니다. Children 중에서 2번과 10번이 대부분의 시간을 차지하는 것을 알 수 있습니다.(Chilren에서 Incl% 값은 현재 펼쳐진 메소드에 국한된 상대 비율입니다). 2번(또는 10번)을 선택하여 그 2번 메소드는 또 어떤 Child가 대부분의 수행시간을 차지하고 있는지 들어갑니다. 이런 식으로 퍼센티지가 높은 Child 위주로 계속해서 파고들다보면 근본적으로 어떤 메소드에서 쓸데없이 많은 시간을 까먹고 있는지 찾아낼 수 있습니다.


나머지 Excl% 컬럼, Calls 컬럼, Time/Call 컬럼을 기준으로 정렬하여 특별히 Exclusive 비율이 높은 메소드. Call 횟수가 너무 많은 메소드, 한번 호출시 수행시간이 오래 걸리는 메소드 위주로 분석하면 됩니다.

* 내용추가 (2010-11-15)
timeline 패널을 통해서는 쓰레드별로 작업을 분담해서 처리하는 모습을 한눈에 파악할 수 있습니다. 어느 쓰레드가 대부분의 작업시간을 점유하는지를 금방 알 수 있고, 한 쓰레드에서 다른 쓰레드로 작업이 전이되는 모습을 볼 수도 있으며, 반복적인 패턴 등 우리에게 영감을 주는 패턴을 발견할 수도 있습니다.
아래의 예제는 "AsyncTask : 어떤 작업수행 -> AsyncQueryWorker : 목록조회 쿼리 실행 -> main : 화면다시그리기" 작업이 반복되는 패턴을 보여줍니다. 실제로 AsyncTask에서 핵심적인 작업을 처리하는 시간보다, 그 결과를 UI에 곧바로 반영하기 위해 쿼리를 다시 날리고 main 쓰레드에서 화면 갱신하는데 대부분의 시간을 까먹고 있는 것을 알 수 있습니다. 이 경우 AsyncTask에서의 작업이 모두 완료될 때까지 쿼리실행을 막는 것으로 성능을 개선할 수 있었습니다.




* 안드로이드 프로파일링에 대한 좀더 심층적인 자료는 아래 링크를 참조하세요.

Posted by 에코지오
,
아래는 어떤 문자열에 GSM7 문자셋에 포함되어 있지 않은 문자가 존재하는지 체크하는 메소드입니다. 프레임워크 라이브러리에 포함된 GsmAlphabet 클래스를 이용하고 있습니다.

    /**
     * GSM 이외의 문자가 포함되어 있는가?
     */
    public static boolean isNonGsmAlphabetExists(CharSequence  v) {
        try {
            com.android.internal.telephony.GsmAlphabet.countGsmSeptets(v, true);
        } catch (Exception e) {
            return true;
        }

        return false;
    }

Exception이 던져지는지 여부로 판단을 하기 때문에 성능상 좋은 소스는 아닙니다만, 저는 단순함을 좋아하기 때문에 별로 신경안씁니다.^^
더 효과적인 방법이 있는지는 모르겠습니다. 참고만 하세요.

Posted by 에코지오
,
안드로이드용 JVM인 Dalvik VM에는 여러가지 실행 옵션이 있습니다. dalvikvm -help 명령으로 가능한 옵션들을 볼 수 있습니다.

$ adb shell
# dalvikvm -help
dalvikvm -help

dalvikvm: [options] class [argument ...]
dalvikvm: [options] -jar file.jar [argument ...]

The following standard options are recognized:
  -classpath classpath
  -Dproperty=value
  -verbose:tag  ('gc', 'jni', or 'class')
  -ea[:<package name>... |:<class name>]
  -da[:<package name>... |:<class name>]
   (-enableassertions, -disableassertions)
  -esa
  -dsa
   (-enablesystemassertions, -disablesystemassertions)
  -showversion
  -help

The following extended options are recognized:
  -Xrunjdwp:<options>
  -Xbootclasspath:bootclasspath
  -Xcheck:tag  (e.g. 'jni')
  -XmsN  (min heap, must be multiple of 1K, >= 1MB)
  -XmxN  (max heap, must be multiple of 1K, >= 2MB)
  -XssN  (stack size, >= 1KB, <= 256KB)
  -Xverify:{none,remote,all}
  -Xrs
  -Xint  (extended to accept ':portable' and ':fast')

These are unique to Dalvik:
  -Xzygote
  -Xdexopt:{none,verified,all}
  -Xnoquithandler
  -Xjnigreflimit:N  (must be multiple of 100, >= 200)
  -Xjniopts:{warnonly,forcecopy}
  -Xdeadlockpredict:{off,warn,err,abort}
  -Xstacktracefile:<filename>
  -Xgc:[no]precise
  -Xgenregmap
  -Xcheckdexsum

Configured with: debugger profiler hprof show_exception=1

Dalvik VM init failed (check log file)

안드로이드 환경에서 우리는 dalvikvm 명령어를 실행하여 직접 애플리케이션 프로세스를 띄우지는 않습니다. 때문에 달빅 VM에 직접적으로 옵션을 전달할 수가 없습니다. 
그대신 안드로이드는 시스템 프로퍼티(system properties)를 설정하여 달빅 VM 실행옵션을 바꾸는 방법을 제공합니다. 간단하게는 setprop 명령어로 실행옵션을 바꿀 수 있습니다.
adb shell setprop dalvik.vm.[옵션명] [옵션값]

위와 같은 형식으로 달빅VM 관련 프로퍼티를 설정한 후, zygote 프로세스를 재시작하면(adb shell stop;adb shell start) 새로 런치되는 앱 프로세스에 변경사항이 적용됩니다.

VM 옵션에 대응하는 dalvik.vm.* 형식의 프로퍼티명이 정확히 무엇인지는 framework/base/core/jin/AndroidRuntime.cpp 소스를 참조하면 알 수 있습니다. 일단 제가 찾아낸 프로퍼티명과 프로퍼티값 샘플은 다음과 같습니다.

# -Xmx
dalvik.vm.heapsize

# -Xstacktracefile (기본값 /data/anr/traces.txt)
dalvik.vm.stack-trace-file 

# -Xcheck:jni (값은 true or false)
dalvik.vm.checkjni 

# -Xjniopts
dalvik.vm.jniopts

# -ea (값은 all, ?, ...)
dalvik.vm.enableassertions

# v=a,o=v와 같은 형식으로 값 설정
# v=a는 -Xverify:all, v=n은 -Xverify:none, v=r은 -Xverify:remote에 해당
# o=a는 -Xdexopt:all, o=v는 -Xdexopt:verified, o=n은 -Xdexopt:none에 해당
# m=y는 -Xgenregmap -Xgc:precise에 해당
dalvik.vm.dexopt-flags

# -Xint (값은 int:portable or int:fast or int:jit)
dalvik.vm.execution-mode 

# -Xdeadlockpredict
dalvik.vm.deadlock-predict

# -Xcheckdexsum (값은 true or false)
dalvik.vm.check-dex-sum 

# -Xthreshold
dalvik.vm.jit.threshold

# -Xjitop
dalvik.vm.jit.op

# -Xincludeselectedop
dalvik.vm.jit.includeop

# -Xjitmethod
dalvik.vm.jit.method 

# -Xincludeselectedmethod
dalvik.vm.jit.includemethod 

# -Xjitprofile
dalvik.vm.jit.profile  

* 자세한 내용은 다음 문서를 참조하시기 바랍니다.
Posted by 에코지오
,
가끔씩 마주치게 되는 "OutOfMemoryError : bitmap size exceeds VM budget" 에러는 메모리 누수가 주요 원인입니다. 이와 관련된 링크를 모아봤습니다.

* 액티비티가 멈출 때 비트맵을 재활용(즉 GC)되게 하라

- bitmap 이미지인 경우 recycle() 호출
- onPause에서 수행하는게 좋음
- ((BitmapDrawable)imageView.getDrawable()).getBitmap().recycle();

* 이미지를 미리 줄여서 읽어들여라

- BitmapFactory.Options.inSampleSize 활용

* Activity Context에 대한 참조(reference)를 오랫동안 유지하지 말아라

- Drawable.setCallback(null) 사용
- WeakReference를 가진 static 내부 클래스
- 이미지를 static 변수로 처리하지 마라

* 외부(outer) 클래스의 상태에 의존하지 않는 내부(inner) 클래스는 static으로 선언하라
- 내부클래스는 외부 클래스 인스턴스를 크게 만들며 또한 외부클래스 객체가 필요이상으로 오래 살아있게 되어 메모리를 더 차지할 수 있음
- 외부클래스의 상태 필드에 접근하지 않는(즉 외부 객체의 상태에 의존하지 않는) 내부클래스는 static으로 선언

* Attacking memory problems on Android

Posted by 에코지오
,
어떤 어플리케이션 프로세스가 복잡한 작업으로 인해 과도하게 CPU를 점유하게 되면, 사용자가 다른 어플을 사용하다가 ANR을 만나게 될 가능성이 큽니다. 구글링해본바 CPU 점유율을 낮추기 위한 일반적인 방법은 아래 2가지로 요약됩니다.

1. 쓰레드 우선순위 낮추기
다음 코드를 통해서 쓰레드의 우선순위를 낮춰줍니다.
 
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);

그러나 이미 백그라운드로 실행되도록 작성됐다면 효과는 미지수입니다.


2. 중간중간 쓰레드를 쉬게함
반복적인 계산의 경우 반복작업 사이사이 짧은간격으로 Thread.sleep(milisenconds) 메소드를 이용하여 쓰레드를 쉬게합니다. 
200ms 일하고 100ms 쉬고, 이런식으로 처리 시간은 다소 길어지더라도 사용자가 감내 가능한 최적의 sleep 텀을 찾아냅니다.


* 내용추가(2010/08/23)
3. 구글 동기화 어플에서 사용하는 전략
구글 동기화 어플에서는 현재 CPU 점유율에 따라서 동적으로 작업을 조정해서 처리한다고 합니다. 예들들어, 현재 CPU의 IDLE 상태 비율(즉 놀고있는 비율)을 계산해서 IDLE이 높으면 작업을 많이 처리하고, 낮으면 적게 처리합니다. 그리고 IDLE 비율이 너무 낮으면(다른 프로세스들이 CPU를 많이 쓰고 있으면) 작업을 중단하고 잠시 쉬었다가 다시 시도합니다. 

Posted by 에코지오
,
소스를 디버깅하다 보면 분명 SQLiteCursor의 finalize 메소드 코드에 중단점(break point)를 걸지 않았음에도 불구하고 자꾸 브레이크가 걸리는 경우가 있습니다.


저는 처음에 이 현상의 원인이 프레임워크 라이브러리 빌드시 어떤 식으로든 중단점 정보가 잘못 포함되어 걸리는 줄로 짐작하고 대수롭지 않게 생각했습니다. 근데 그게 아니더군요. 구글신에게 물어보니 아래와 같은 답변이 나왔습니다.

The implication is that you haven't used cursor.close() and left an 
"open" cursor around when your activity/service was destroyed.   You 
either need to do this manually, or make sure you're using a managed
cursor. 

If you're not using a managed cursor, you should wrap every cursor 
creation with a try/finally expression. 

그러니까 코드 어딘가에 close 되지 않은 커서가 있는 것이니 명시적으로 finally절에서 close해줘야 한다는 겁니다. 불행히도 close 되지 않은 커서가 어느 클래스의 어느 메소드에서 open 됐는지는 디버깅 창의 스택트레이스에 나와있지 않습니다. 

하지만 한가지 힌트는 있습니다. SQLiteCursor 객체의 mQuery 멤버변수에는 해당 커서가 실행했던 쿼리 정보가 담겨 있습니다. 이 쿼리를 Variables 창에서 확인할 수 있습니다. 이제 close되지 않는 커서가 실행했던 쿼리가 무엇인지 알았으니 대충 소스의 어느 부분에서 open된 건지 찾아낼 실마리를 얻은 겁니다.


* 내용추가(2010/08/12)
cursor가 어느 클래스의 어느 메소드에서 open 된 것인지 로그캣에 출력할 수 있습니다. SQLiteCursor.finalize()의 아래 코드에 방법이 나옵니다.

if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
    Log.d(TAG, message + "\nThis cursor was created in:");
    for (StackTraceElement ste : mStackTraceElements) {
        Log.d(TAG, "      " + ste);
    }
}

즉 SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION 이 true이면 스택트레이스를 DEBUG 레벨로 로깅해줍니다. 
DEBUG_ACTIVE_CURSOR_FINALIZATION 을 true로 설정하는 방법은 Log.isLoggable() 메소드에 나오는 아래 설명을 참조하여,
Checks to see whether or not a log for the specified tag is loggable at the specified level. The default level of any tag is set to INFO. This means that any level above and including INFO will be logged. Before you make any calls to a logging method you should check to see if your tag should be logged. You can change the default level by setting a system property: 'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>' Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPRESS will turn off all logging for your tag. You can also create a local.prop file that with the following in it: 'log.tag.<YOUR_LOG_TAG>=<LEVEL>' and place that in /data/local.prop.
다음처럼 명령을 날립니다.

$ adb shell
# setprop log.tag.SQLiteCursorClosing VERBOSE

이제 로그캣에 닫히지 않은 커서의 스택트레이스가 나올 것입니다. (그러나 저는 안나오더군요. 왜 안나오는지 모르겠습니다.ㅠㅠ)

이 방법을 응용하여 SQLite가 실행하는 모든 쿼리를 로그캣에 찍을 수 있습니다.
android.database,sqlite.SQLiteDebug 클래스의 DEBUG_SQL_STATEMENTS 옵션을 한번 볼까요.

    /**
     * Controls the printing of SQL statements as they are executed.
     */
    public static final boolean DEBUG_SQL_STATEMENTS =
            Log.isLoggable("SQLiteStatements", Log.VERBOSE);

아하 "SQLiteStatements" 군요. 아래의 명령어를 실행해주면 모든 쿼리가 로그캣에 디버그 레벨로 로깅됩니다.

$ adb shell
# setprop log.tag.SQLiteStatements VERBOSE

Posted by 에코지오
,
쓰레드 덤프는 프로세스에서 실행중인 쓰레드들의 스냅샷입니다. 쓰레드 덤프를 통해서 덤프시점에 현재 어떤 쓰레드들이 있는지, 쓰레드들의 상태는 어떤지, 쓰레드가 어떤 메소드를 실행하며 지나왔고 어떤 메소드를 실행중인지 파악할 수 있습니다.

1. DDMS 쓰레드 뷰
안드로이드는 DDMS를 통해서 쓰레드 스택을 쉽게 분석할 수 있게 지원합니다.

(1) DDMS에서 프로세스를 선택후 Update Threads 아이콘을 클릭합니다. 
(2) 오른편 Threads 탭에 선택된 프로세스의 쓰레드 목록이 나타납니다.  
(3) 쓰레드를 한개 선택하고 Refresh 버튼을 누르면 그 순간의 쓰레드스택(쓰레드가 실행한 객체 메소드) 정보가 나타납니다.


Threads 목록의 각 컬럼에 대한 설명은 다음과 같습니다.
  • ID : VM 내의 쓰레드 ID. DVM의 경우 3부터 시작하는 홀수. 앞의 별표는 데몬(daemon) 쓰레드.
  • Tid : 리눅스 쓰레드 ID. main 쓰레드의 경우 프로세스 ID와 일치.
  • Status : 쓰레드 상태 
running - executing application code(실행중)
sleeping - called Thread.sleep() 
monitor - waiting to acquire a monitor lock(다른 쓰레드가 작업을 마치기를 기다리는 중)
wait - in Object.wait() 
native - executing native code 
vmwait - waiting on a VM resource 
zombie - thread is in the process of dying 
init - thread is initializing (you shouldn't see this) 
starting - thread is about to start (you shouldn't see this either) 

  • utime : 사용자 코드 누적 실행시간. cumulative time spent executing user code, in "jiffies" (usually 10ms). 
  • stime : 시스템 코드 누적 실행시간. cumulative time spent executing system code, in "jiffies" (usually 10ms). 
  • Name : 쓰레드 이름

의미있는 스택 트레이스 정보를 보기 위해서는, 단말의 기능을 사용하면서 순간을 잘 포착하여 Refresh 버튼을 눌러야 트레이스를 제대로 볼 수 있습니다. 때를 놓치면 이미 실이 지나가고 난 뒤의 고요한 트레이스만 보이게 됩니다. ^^
애플리케이션이 응답 없음 상태(ANR)일 때 Refresh 버튼을 눌러, 도대체 쓰레드가 현재 어떤 메소드를 실행 중이길래 반응이 없는 건지 알 수 있을 것으로 보입니다. 또한 락(lock)이 걸렸을 때 이것이 쓰레드 경합(monitor 상태의 쓰레드들)에 의한 문제인지 파악할 수 있을 것으로 보입니다. (그러나 2경우 모두 아직 이런 식으로는 확인 못해봤습니다.-.-; )

2. /data/anr/traces.txt
애플리케이션에 에러가 발생해서 프로세스가 죽게 되면 달빅 VM은 보통의 JVM처럼 자동으로 쓰레드덤프(javacore) 파일을 떨궈줍니다.  "/data/anr/traces.txt" 파일이 그것입니다. 
아래는 애플리케이션 에러로 애플리케이션이 중지되는 시점의 로그입니다. 맨 아랫줄을 보시면 스택트레이스가 "/data/anr/traces.txt" 파일에 쓰여진다는 정보를 볼 수 있습니다.


경우에 따라서는 애플리케이션 프로세스가 "/data/anr/traces.txt" 파일에 쓰기 권한이 없어서(permission denied) traces.txt에 기록을 못할 수도 있습니다. 하지만 꼭 traces.txt가 아니더라도 위에 나오듯이 로그캣에 친절하게 Exception 스택 트레이스 정보가 나오기 때문에 에러 분석에는 큰 문제가 없을 것입니다.

쓰레드 덤프는 사용자가 직접 kill -3 <PROCESS ID> 명령을 날려서 만들 수도 있습니다. ANR이 발생하거나 lock이 걸리면 이렇게 쓰레드덤프를 떠야겠죠. 참고로, /data/anr/traces.txt 파일은 매번 새로 생성되지 않고 기존 파일에 쓰레드 덤프가 누적되어 기록됩니다. 아래는 쓰레드 덤프 파일에 기록된 내용의 예입니다.

----- pid 224 at 2010-02-12 05:41:20 -----
Cmd line: com.example.android.notepad

DALVIK THREADS:
"main" prio=5 tid=3 WAIT
  | group="main" sCount=1 dsCount=0 s=N obj=0x4001b268 self=0xbd00
  | sysTid=224 nice=0 sched=0/0 cgrp=default handle=-1344001384
  at java.lang.Object.wait(Native Method)
  - waiting on <0x14c328> (a android.os.MessageQueue)
  at java.lang.Object.wait(Object.java:288)
  at android.os.MessageQueue.next(MessageQueue.java:148)
... 중략 ...
  at java.lang.reflect.Method.invoke(Method.java:521)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
  at dalvik.system.NativeStart.main(Native Method)

"Binder Thread #1" prio=5 tid=11 NATIVE
  | group="main" sCount=1 dsCount=0 s=N obj=0x43cfb248 self=0x134170
  | sysTid=228 nice=0 sched=0/0 cgrp=default handle=1259984
  at dalvik.system.NativeStart.run(Native Method)

"JDWP" daemon prio=5 tid=9 VMWAIT
... 중략 ...
"Signal Catcher" daemon prio=5 tid=7 RUNNABLE
... 중략 ...
"HeapWorker" daemon prio=5 tid=5 VMWAIT
... 중략 ...
----- end 224 -----

* 일부 내용에 오류가 있을 수 있습니다.
Posted by 에코지오
,