웹이나 안드로이드를 개발하면서 리액티브란 이제 빠질수가 없는 존재가 되어버렸다.

리액티브 프로그래밍이란 데이터의 흐름과 어떠한 변화에 관련된 프로그래밍 패러다임으로  "프로그램이 주도하는것이 아니라 환경이 변하면 그에 대한 이벤트를 받아 동작" 하는것이라고 생각하면 된다.

대표적인 리액티브 프로그래밍 라이브러리로 RxJava 가 있다.

RxJava 는2013년 2월 넷플릭스의 기술블로그에서 처음으로 소개되었다.

그당시 넷플릭스는 전반적인 성능을 개선하는 프로젝트를 진행하면서 .NET 환경의 리액티브 확장 라이브러리를 JVM 에 포팅해서 RxJava 를 만들었다고 한다.

- RxJava 의 장점

1. 비동기 이벤트를 매우쉽게 처리할 수 있다.

2. 콜백방식의 문제점을 개선했다.

 

 

 

 

 

 

C 프로그래밍을 할때에는 보통 아래와같이 지역변수 (i, strike, ball, out) 들을 블록 맨 앞부분에 선언한다.

사용은 코드 마지막에 사용할지라도 맨 앞에 선언했다.

 

int main(void) { 
	int i = 0; 
	int strike = 0; 
	int ball=0, 
	int out = 1; 

	printf("Game Start!"); 
	intputNumber(); 
	strike = checkStrike(); 
	return 0; 
} 

 

이런 관습에 익숙한 프로그래머는 자바에서도 습관대로한다... 바로 나 ㅠㅠ

그러나 자바에서는 "처음으로 해당 변수를 사용하는곳에 선언" 하는것을 권장하고있다.

 

package com.jhkook.remote;
import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        print("Game Start!");
        intputNumber();

        int strike = 0;
        strike = checkStrike();
    }
}

 

 

지역변수를 너무 먼저 선언하게되면 실제로 코드가 짧을땐 상관이 없지만,

코드가 길어지게되면 해당 변수가 사용될때쯤 변수의 자료형과 초기값을 잊어버릴 위험이 크다.

 

그리고 지역변수를 너무 앞쪽에 선언하게되면 유효범위가 확장되기때문에 원래 사용하려고 했던곳 이외의 장소에서 실수로 사용되어 끔찍한 버그를 만들어낼 가능성도 있다.

 

또한 "모든 지역변수 선언에는 초기값이 포함되어야 한다"

변수를 초기화하기에 충분한 정보가 없다면 그때까지는 선언을 미뤄야한다.

 

├── bionic                          : bionic libc(BSD의 libc 수정)를 포함
├── bootable                         : bootloader, recovery mode 관련 코드
├── build                           : 빌드 관련 스크립트를 저장
│   ├── envsetup.sh              : 환경 정보 설정 파일, 제조사의 Flag들에 대한 설정을 함
│   ├── core
│   │   └── prelink-linux-arm.map : shared library들이 로딩되는 번지를 미리 지정한 파일
│   └── target
│     └── board
│           └── generic
│                 ├── AndroidBoard.mk     : 최상위 directive 파일, Android 최종 결과물
│                 │                                 구성서 자동 포함 bin에 대한 install을 결정
│                 │                                  init.rc, init%hw%.rc 들을 install
│                 └── BoardConfig.mk    : HAL의 en/disable 등을 결정
├── cts                      : Compatibility Test Suite, GMS를 탑재 위해 통과해야 하는 Test
├── dalvik                  : dalvik VM 관련 소스 코드
├── development           : 개발용 application을 저장
├── device                  : 칩 관련 설정 파일 및 디버깅을 위한 cmm 파일 등을 포함
├── external                  : 기존 작성된 library, binary등의 소스 코드를 포함
├── frameworks
│   ├── base                 : Android Framework Source Code
│   ├── cmds             : binder 관련된 service manager 소스 코드 및 여러 가지 command
│   ├── libs                  : Android base library (AudioFlinger, SurfaceFlinger, util, binder 등)
│   ├── media            : media 관련 Client & Service 라이브러리
│   └── policies          : Android 시스템을 background에서 제어하는 최상위 application
│                                    KeyGuard 관련, 화면 제어(rotation..), event 관리 등의 작업 처리
├── hardware                : Android에서 사용하는 h/w 관련 소스 코드 포함(HAL, include..)
│   ├── libhardware      : Board H/W 의존성이 높은 하드웨어 모듈 포함
│   └── libhardware_legacy: 일반적으로 Android에 반드시 있어야 하는 H/W 모듈(wifi..)
├── packages                : Android 기본 Application 소스 코드를 포함
├── prebuilt                  : 툴체인과 미리 빌드되어 들어가는 바이너리 파일 포함
├── system                : Android의 기본 바이너리 소스 코드(init…)
│   ├── core
│   │    ├── init           : Android init 소스 코드
│   │    └── vold          : external storage 제어 모듈 (Éclair)
│   └── vold                 : external storage 제어 모듈 (Froyo/Gingerbread)
└── vendor                 : vendor specific code(HAL, product list 관리…)



안드로이드 엑티비티에 대해 이해하기 위해 필요한 지식은 3가지 정도가 아닐까 생각합니다. 바로 Activity Lifecycle, Task, Intent 입니다. 모두 어느정도 이해를 하시고 계신 상태라고 생각하고 글을 적어보겠습니다.

인텐트를 이용하여 새로운 엑티비티를 띄우기 위해서는 일반적으로 다음과 같은 방법으로 새로운 엑티비티를 실행하게 됩니다.

Intent intent = new Intent(this, MyActivity.class); 
startActivity(intent);


위의 코드는 다음과 같은 순서로 실행이 됩니다.

1. 새로운 MyActivity 인스턴스가 생성됩니다.
2. 이 인스턴스가 현재 태스크 스택의 최상단에 푸시가 됩니다.
3. 엑티비티가 시작되며(onStart) 포그라운드로 가져옵니다.

하지만 위와 같은 인텐트 생성에 관련된 기본적인 실행 방법을 인텐트 플래그를 사용하여 임의로 조정할 수 있습니다.

intent.addFlag(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);


위와 같은 방법을 통해 특정한 플래그 옵션값을 startActivity(intent)가 수행될때 같이 넘겨줄 수 있습니다. 지금부터 이러한 플래그 옵션값들을 좀더 상세기 적어보도록 하겠습니다.

FLAG_ACTIVITY_BROUGHT_TO_FRONT

이 플래그는 사용자가 설정하는것이 아닌 시스템에 의해 설정되는 값입니다. 엑티비티의 실행모드가 singleTask이고 이미 엑티비티스택에 존재하고 있는 상태라고 가정을 할 때 다시 그 엑티비티가 호출되고 재활용 되었을 경우 자동으로 설정이 됩니다.


FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
사용자 삽입 이미지
이 플래그를 사용하면 태스크가 리셋될때 플래그가 사용된 엑티비티부터 최상단의 엑티비티까지 모두를 삭제합니다. 리셋은 FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 플래그에 의해 실행이 되는데 시스템에 의해 홈스크린에서 사용자에 의해 백그라운드에 있던 태스크가 포그라운드로 전환될때에 항상 붙게 됩니다.

위의 그림에서 볼 수 있듯이 백그라운드와 포그라운드 전환관계에서 CLEAR_WHEN_TASK_RESET 플래그가 설정된 엑티비티와 이후의 엑티비티 모두가 삭제되는것을 알 수 있습니다. 백그라운드로 넘어갔을때 유지를 안해도 될 일회성 엑티비티들은 해당 플래그를 사용하면 도움이 될것이라 봅니다.


FLAG_ACTIVITY_RESET_TASK_IF_NEEDED

이 플래그는 인텐트를 이용하여 새로운 태스크를 생성하거나 존재하고 있는 태스크를 포그라운드로 가져오는 경우가 아닌경우에는 사용하여도 아무런 효과가 없습니다. 적절한 경우라면 태스크를 리셋 합니다. 이때에 태스크의 affinity 설정에 맞추어 리셋이 일어나게 되며 FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET과 같은 플래그 설정에 맞추어진 특정 처리가 일어나게 됩니다.


FLAG_ACTIVITY_CLEAR_TOP
사용자 삽입 이미지
만약에 엑티비티스택에 호출하려는 엑티비티의 인스턴스가 이미 존재하고 있을 경우에 새로운 인스턴스를 생성하는 것 대신에 존재하고 있는 엑티비티를 포그라운드로 가져옵니다. 그리고 엑티비티스택의 최상단 엑티비티부터 포그라운드로 가져올 엑티비티까지의 모든 엑티비티를 삭제합니다.

예를 들면 현재 ABCDE순서로 엑티비티가 스택에 들어있다고 할때 엑티비티E에서 C를 호출하게 되면 D와 E는 스택에서 삭제되고 ABC만이 남아있게 됩니다. 여기서 AB 역시 남는다는 것을 이해하셔야 합니다.


FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

엑티비티가 새로운 태스크안에서 실행될때에 일반적으로 타겟 엑티비티는 '최근 실행된 엑티비티' 목록에 표시가 됩니다. (이 목록은 홈버튼을 꾹 누르고 있으면 뜹니다) 이 플래그를 사용하여 실행된 엑티비티는 최근실행된엑티비티 목록에서 나타나지 않습니다.



FLAG_ACTIVITY_FORWARD_RESULT


기본적으로 엑티비티A가 엑티비티B를 호출하였다고 할 경우 startActivity(intent) 대신에 startActivityForResult(intent) 메서드를 이용하여 호출을 함으로써 엑티비티B의 결과값을 엑티비티A로 전달할 수 있습니다.

엑티비티B에서는 setResult(int resultCode)를 정의한 뒤에 종료를 하게 되며 엑티비티B를 호출하였던 엑티비티A는 콜백메서드인 onActivityResult()를 통해 결과값을 전달받게 됩니다.
사용자 삽입 이미지
이제 엑티비티B가 또다른 엑티비티C를 호출하였다고 가정해 봅시다. 그리고 이렇게 호출된 엑티비티C에서 엑티비티A까지 전달할 결과값을 정의하였습니다. 이 결과값을 B에서 A로 또다른 코드를 통해서 프로그래머의 코드를 통해서 값을 전달하는 번거로움을 피하기 위해 안드로이드에서는 이 인텐트 플래그값을 제공합니다.

위에 나와있는 그림의 예를 통해 보면 엑티비티B가 엑티비티C를 호출하기위해 단순히 startActivity()를 이용하는 것을 알 수 있습니다. 그리고 지금 설명중인 플래그를 붙이도록 합니다. 이후에 엑티비티C에서는 setResult()를 통해 결과값을 정의를 한후에 finish()를 통해 엑티비티를 종료하도록 합니다.

엑티비티B에서는 단순히 마찬가지로 finish()를 통해 엑티비티를 종료하시기만 하면 됩니다. 이후에 startActivityForResult()를 통해 엑티비티B를 호출했던 엑티비티A는 onActivityResult() 콜백 메서드로 결과값을 받아보시면 엑티비티C에서 정의한 값을 받을 수 있다는것을 알 수 있습니다.


FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY

이 엑티비티 플래그는 시스템에 의하여 자동으로 설정되는 플래그입니다. 홈스크린화면에서 홈버튼을 롱클릭함으로써 뜨게 되는 "최근실행목록"을 통해 실행되었을 경우 자동으로 설정됩니다.


FLAG_ACTIVITY_MULTIPLE_TASK

이 엑티비티 플래그는 FLAG_ACTIVITY_NEW_TASK와 함께 사용하지 않으면 아무런 효과가 없는 플래그입니다. 두개의 플래그를 동시에 사용할 경우 새로운 태스크는 재활용되지 않고 무조건 새로 생성되며 피호출되는 엑티비티는 이 새로운 태스트의 최상위 엑티비티가 됩니다. (당연히 하나밖에 없을테니-_-a)


FLAG_ACTIVITY_NEW_TASK

이 엑티비티 플래그를 사용하여 엑티비티를 호출하게 되면 새로운 태스크를 생성하여 그 태스크안에 엑티비티를 추가하게 됩니다. 단, 기존에 존재하는 태스크들중에 생성하려는 엑티비티와 동일한 affinity를 가지고 있는 태스크가 있다면 그곳으로 새 엑티비티가 들어가게됩니다.

하나의 어플리케이션안에서는 모든 엑티비티가 기본 affinity를 가지고 같은 태스크안에서 동작하는것이 기본적(물론 변경이 가능합니다)이지만 FLAG_ACTIVITY_MULTIPLE_TASK 플래그와 함께 사용하지 않을경우 무조건적으로 태스크가 새로 생성되는것은 아님을 주의하셔야 합니다.


FLAG_ACTIVITY_NO_ANIMATION

안드로이드 OS가 2.0으로 올라오면서 새로 추가된 엑티비티 플래그입니다. 이 플래그를 사용할 경우 엑티비티가 스크린에 등장할시에 사용될 수 있는 다양한 애니메이션 효과를 사용하지 않습니다.


FLAG_ACTIVITY_NO_HISTORY

이 플래그를 사용할 경우 새로 생성되는 엑티비티는 어떤 태스크에도 보존되지 않게 됩니다. 예를 들면 로딩화면과 같이 다시 돌아오는것이 의미가 없는 화면이라면 이 플래그를 사용하여 태스크에 남기지 않고 자동적으로 화면이 넘어감과 동시에 제거할 수 있습니다.


FLAG_ACTIVITY_NO_USER_ACTION

이 플래그가 설정되면 자동적으로 엑티비티가 호출될 경우에 자동 호출되는 onUserLeaveHint()가 실행되는것을 차단합니다. onUserLeaveHint() 콜백 메서드는 어플리케이션 사용중에 전화가 온다거나 하는등의 사용자의 액션없이 엑티비티가 실행/전환되는 경우에 호출되는 메서드입니다.


FLAG_ACTIVITY_REORDER_TO_FRONT
사용자 삽입 이미지
호출하려던 엑티비티가 이미 엑티비티 스택에 존재하고 있다면 이 플래그를 사용하여 스택에 존재하는 엑티비티를 최상위로 끌어올려줍니다. 결과적으로 엑티비티 스택의 순서가 재정렬되는 효과를 가집니다. 위의 예를 볼 경우에 엑티비티E가 C를 호출하게 됨으로써 엑티비티C가 최상단으로 이동하는 결과를 확인하실 수 있습니다.


FLAG_ACTIVITY_SINGLE_TOP

이 플래그는 말그대로 하나의 탑(?)을 의미하는 설정입니다. 엑티비티를 호출할 경우 호출된 엑티비티가 현재 태스크의 최상단에 존재하고 있었다면 새로운 인스턴스를 생성하지 않습니다. 예를 들어 ABC가 엑티비티 스택에 존재하는 상태에서 C를 호출하였다면 여전히 ABC가 존재하게 됩니다.





복수의 data를 insert할 때 속도 향상을 위해서는 다음 method를 이용하면 됩니다.

 

DB.beginTransaction();

DB.setTransactionSuccessful();

DB.endTransaction();

 

제 경우, 특정 파일을 읽어 각 라인을 파싱한 item을 sqlite db에 insert 하는 프로그램을 구현하는 중이었습니다.

 

위의 method를 사용하지 않고 약 500개의 item을 insert할 때 제 갤럭시S에서 로그를 출력해보니 10개당 약 5~6초의 시간이 걸려 전체 500개를 insert하는데 약 5분이상 걸렸었습니다. 만약 thread등의 처리를 하지 않는다면 그 시간동안 먹통 상태가 될 것입니다.

  

(갤럭시S의 경우 memory access 속도 등에 문제가 있어 타 제품보다 db 입출력 속도가 느리다고 하며, 같은 갤럭시S 내에서도 뽑기에 따라서 그 속도가 천차만별이라고 합니다)

 

이 문제를 해결하기 위해서 개발 포럼등을 검색한 결과 위의 method를 사용하면 빨라질 수 있다고 하여 다음과 같이 구현해 보았습니다.

 

DB.beginTransaction();

try {

  while(readline())    // 500번 반복

  {

      parsing();

      insert();

  }

  DB.setTransactionSuccessful();

} finally {

    DB.endTransaction();

}

 

로그를 확인하니 500개가 입력되는데 약 2초!! 가 걸리게 되었습니다.

 

주의할 점은 setTransactionSuccessful()을 호출하기 전에 프로그램이 종료되면, insert()를 호출하였다고 하더라도 data가 저장되지 않습니다.

 


안드로이드 어플을 개발할 때 허용되는 힙 사이즈는 디바이스마다 다르긴 하지만 대략 15메가 내외로 제한적입니다.
이 상황에서 카메라 어플이나 포토에딧 어플과 같은
큰 이미지를 제어한다던지, 겔러리나 리스트 뷰에서 다량의 이미지를 제어하는 경우,
거의 대부분 out of memory 문제를 접하게 됩니다.

이런 경우에 대해서 잘 정리된 사이트가 있어서 먼저 소개 합니다.

대부분의 메모리 문제의 경우, Bitmap recycle 과 Drawable callback 을 null 로 맞추어서 해결할 수 있습니다만,
겔러리나 리스트뷰의 경우 ListAdapter 에서 생성하는 View 를 제대로 unbind 를 해주지 않는다면 메모리 문제가 발생할 수 있습니다.

특히 BitmapDrawable 과 NinePatchDrawable 을 명확하게 구분해서 사용해야 하는데,
메모리를 많이 사용하는 어플의 경우 힙 사이즈 제한으로 인해 bitmap 을 개발자가 create / recycle 하면서 메모리를 관리하게 되는데,
NinePatchDrawable 을 사용한 경우 해당 bitmap 을 개발자가 recycle 할 수 있는 방법이 없기 때문에
bitmap create / recycle 이 필요한 경우 BitmapDrawable 을 사용해야 하는 경우가 있습니다.

또한 ListAdapter 의 경우, 항상 view 를 새로 생성하는 방식은 피해야 하며,
일반적으로 convertView 를 사용하지만, 때에 따라서 개발자가 직접 view instance 를 관리하는게
메모리 관리에 있어서는 더 명확해 보일 수도 있습니다.

또한 activity 의 onPause 에서 메모리를 해제할 것들과 onDestroy 에서 메모리를 해제할 것들을 구분해야 하며,
activity 를 start 할 때 flag 를 어떻게 줄 것인지도 같이 고민을 해야
background 에 있는 activity 의 메모리로 인해, 현재 activity 에서 out of memory 가 발생하는 문제도 해결 할 수 있을 것입니다.

비트맵 관련 앱에서 많이 발생하는 힙메모리 관련 오류 해결하기
개발 환경 - 안드로이드 2.1

1. 액티비티
    - onCreate에서 dalvik 버추얼 머신에게 힙메모리 임계값 지정하기 (70%)
       dalvik.system.VMRuntime.getRuntime().setTargetHeapUtilization(0.7f); --> 메인 액티비티에서 한번만 지정

    - onDestroy 이벤트에서 모든 지역변수 null 처리히기, 모든 비트맵 인스턴스 변수 recycle 시키기,
      메인앱에서 System.gc() 한번 호출하기
      
    - dalvik 버추얼 머신에게 dalvik.system.VMRuntime.getRuntime().runFinalizationSync()를
      호출하여 강제로 클래스들의 finalization 호출하기

2. 뷰를 상속한 차일드뷰
    - destroyDrawingCache 메소드를 상속 받아서 이부분에서 비트맵 인스턴스들 recycle 및 null 처리하기

제가 그래픽 관련 앱을짜면서 가장 애먹은 부분이 힙메모리 부분이었습니다.

힙모메리가 한계값까지 계속 늘어나기만 하고 줄어들지 않아서 결국에는 앱이 강제 종료 되는 현상이
계속 발생하더군요.. 구글링으로 여기저기 뒤지다가 안드로이드 플랫폼에서는 앱을 사용자가 종료를
시켜도 실제로는 메모리에 상주해 있어서 메모리가 계속 늘어나는 현상이 있다고 합니다.

디벨로퍼 블로그에 있는 메모리 릭을 발생시키는 잘못된 코드의 예제다.

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
  
TextView label = new TextView(this);
label.setText("Leaks are bad");
  
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);

setContentView(label);
}

Drawable 은 TextView 를 거쳐 Activity 의 Context 를 참조하게 되므로 액티비티의 첫 생성시 메모리 공간은 릭이 발생한다고 하는것 같은데...
비트맵도 마찬가지 아닌가... 어이쿠!! 구글아저씨들~~ 우리 저런 코드 엄청 많이 쓰거든요!! 잘못된 예제를 바로 잡는 예제도 주셔야죠!! 약올리는것도 아니고... 권장사항에 있는데로라면 new TextView(this) 대신 (getApplicationContext()) 를 하면 해결될거라고 하는것 같은데...
글쎄요.. 내경우는 문제가 좀 달라요.

난 Activity도 View 도 죽지 않는다. View 에 올리는 Bitmap 만 죽어라고 로테이션 할 뿐이다. 이경우 어떻게 해야 하는가...??
Context 를 참조하는 View 가 Bitmap 의 Callback 으로 등록되기 때문에 View 가 죽기전엔 Bitmap 이 죽지 않을까 그럼...??
이런것 때문에 Bitmap.recycle() 메서드가 있는것 아닌가? 왜 그럼 Callback 을 해제할수 없는가?? 왜 가비지 컬럭터는 이딴식으로 작동하나..??
이럴 바에야 malloc/free 하는게 더 좋지 않은가..??

제공되는 메소드의 특성상 Bitmap 은 static 이 될 수 밖에 없고.. 그런고로 recycle() 메소드가 제공되는것이겠지만...
작동이 되지 않는것은 분명 버그다.

실험1.
자 요렇게 자료형태를 구성했다.

1. Main 에서 View1, View2 로 참조시켜도 둘다 표현이 된다.
2. 셋중 어느 한군데서 recycle() 을 호출하면 모두 표현불가능해진다. ( invalidtate() 를 호출하기 전에는 표시되어있던건 그냥 있다 )
3. View 로 참조후 Main 에서 bitmap = null 을 줘도 View에서 사용하는데 아무 지장이 없다.
4. View 에서 참조후 한군데서 null 을 줘도 다른 View 나 메인에서는 아무 이상이 없다. 다른 View 로 다시 참조도 가능하다.
5. 두개의 이미지를 번갈아 수십 수백번 하나의 bitmap 참조에 할당해도 문제가 없다.

결론.
Bitmap 클래스는 비트맵의 메모리상의 위치와 크기, 밀도, 등의 각종 정모만 가질뿐.. 실제 Bitmap 의 내용을 변수로 갖지 않는다. 메모리상에서 참조해야할 위치만 가지고 있다고 생각된다. recycle 을 하면 메모리는 해제되지만 getWidth(), getDensity() 같은걸 해보면 정보는 다 그대로 가지고 있다. 메모리를 해제해도 invalidate()를 호출하기 전에 화면이 그대로 있으니 비디오 장치는 따로있다는 말이렸다.
메인에서는 View 로 생성한 Bitmap 을 넘긴후 즉시 null 처리해 버려도 전혀 문제가 되지 않는다.

실험2.
이번에는 조금 무식한 실험을 했다.
인터넷에서 배경화면류 (1024 * 768 ~ 1900 * 1200 다양함..) 정도의 이미지를 300개 받아서.. sd 카드에 넣고 하나의 이미지뷰에 로테이션으로 전부 보여주려고 시도했다.

힙 메모리는 기기마다 할당크기가 다 다르다고 한다. 누군가 나와 똑같은 실험을 진행한다면 다른 결과를 얻을 수 있다. 내폰은 넥서스다.

추가.
최초로 이 삽질을 했던것은 실패다. 잘못된 실험이다. 같은크기의 이미지로 했어야 했다. 그거 변경하는게 귀찮아서 그냥 했던 나의 불찰이다. 23번에서 자꾸 죽은 이유는 23번이미지가(24개째) 너무 큰 사이즈(4256 x 2848) 짜리여서 그랬다. 실험 내용은 수정했다.
실험 순서로는 네번째 실험이 된 셈이다. 3번째 실험에서 이미지 사이즈가 3200 x 3200 = 10240000  이상은 무사히 통과된다는 사실을 알았다.

1. 16개 표시 후 다이. ( 노말 )
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

2. 91개 표시 후 다이. 92번째 이미지가 살짝 크긴 한데(3330x2929) 동일사이즈 이미지를 테스트 했을때는 더 큰 크기도 통과했었다. ( gc() 호출 - 호출한다고 gc가 바로 수행되는건 아니다. gc 에 작업을 예약할 뿐.. )
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);
System.gc();

3. 91개 표시후 다이. 92번째 이미지가 의심시러워 진다. ( 지역변수로 사용 )
Bitmap tmp;
tmp = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(tmp);
tmp = null;
System.gc();

4. 1200개 무사히 표시. - 이 코드는 92 번째 이미지를 무사히 통과했다. ( recycle() 메소드만 호출 )
if(bitmap != null) { bitmap.recycle(); }
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

5. 1200개 무사히 표시.  ( recycle() 과 gc() 호출 )
if(bitmap != null) { bitmap.recycle(); bitmap = null; System.gc(); }
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

6. 그럼 지역변수를 선언한다해도 static 이니까 해제해 볼까..
Bitmap tmp;
tmp = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(tmp);
tmp = null;
System.gc();

5. 23개 표시 후 다이. ( ImageView 자체를 지역변수로 동적 할당 )
if(stage.getChildCount() > 0){
for(int i=0; i<stage.getChildCount(); i++){
ImageView rtmp = (ImageView)stage.getChildAt(i);
rtmp = null;
}
stage.removeAllViews();
System.gc();
}
ImageView tmpImg = new ImageView(getApplicationContext());
tmpImg.setAdjustViewBounds(true);
tmpImg.setScaleType(ScaleType.CENTER_INSIDE);
bitmap = BitmapFactory.decodeFile(filepath);
tmpImg.setImageBitmap(bitmap);
stage.addView(tmpImg);

6. 23개 후 다이. ( bitmap 할당만 )
bitmap = BitmapFactory.decodeFile(filepath);
//img.setImageBitmap(bitmap);

6. 23개 후 다이. ( bitmap 을 static 으로 선언 )
private static Bitmap bitmap;
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

결론.
로그를 보면 거의 매번 gc가 호출되어 수행되는걸 볼수있다.(항상은 아니다. 이넘은 확실히 지 하고싶은데로 한다) 
if(bitmap != null) { bitmap.recycle(); bitmap = null; }  이정도면 메모리 릭은 충분히 잡아지는듯하다. gc도 일일이 예약할 필요는 없다. 수행속도만 느리게 할 뿐이다. 어차피 지 오고싶으면 오는 녀석이다.


힙 메모리 총량의 문제

하지만 문제는 다른곳에서 발생한다. 현제 개발중인 앱은 bitmap 을 화면에 계속 올린다. 결론부터 이야기하면 메모리 총량의 한계에 의한 문제가 발생한다. 화면에 올라간 bitmap 을 ArrayList 로 가지고 있으면서 View 에서 참조하고 사용자의 입력에 따라 작업을 하는데 이 과정에서 힙메모리 총량 부족으로 앱이 죽는현상이 자꾸 발생한다. 내가 겪었던 문제는 메모리 누수에 의한 문제가 아니었다. 총량의 문제였다.

기본적으로 메모리 총량에 의한 문제는..이런류의 루틴이 필요하다고 생각한다.

if (프리메모리 > 필요메모리){
할당/작업
}else{
경고/안쓰는것 해제..
}

하지만 프리메모리와 필요한 메모리의 양을 알아낼 방법이 없다. 내가 자바초보라서 모르는건지도 모르겠지만.. bitmap 의 구조를 분석해서 필요한 메모리를 계산해 낼수는 있을것이다. bitmap 구성방식이야 어차피 다 나와있는거니까...그런데 프리메모리를 알 수가 없다. 몇가지 코드를 찾아냈지만 다르다... 프리메모리가 한참 남았다는데 앱은 자꾸 죽는다. 실제로 올라가는것도 계산이 조금 이상하다.

문제는 힙 메모리 한계때문에 앱이 죽는현상을 막을 방법이 없다. 최소한 앱이 죽지는 말아야 할것아닌가.. 그런데 막을 방법이 없다.
try - catch 문으로도 잡히질 않는다. 이부분은 Exception 이 아니라 런타임 에러에 해당하기 때문이라나...
이런 사소한 부분에 대해 확실히 안드로이드의 부족함을 느낀다. 지금 머릿속에는 비트맵을 sd 카드에서 필요할때마다 한번씩만 로딩하는 루틴을 생각하고 있다. 물론 속도는 굉장히 느려지겠지... 정 안되면 ndk 로 가던지...

(혹시 해결법을 아시는분 계시면 알려주시기 바랍니다.)

안펍에서 한분은 비슷한 문제를 겪었는데 자기는 비트맵수의 제한을 두어서 죽는걸 방지한다고 하더라. 물론 그리 나쁜 생각은 아니지만 근본적은 해결책도 아니다. 장치마다 화면 사이즈가 다 다르니 혹시 갤탭에서 이용하는 유저가 있다면 비트맵을 화면 사이즈에 맞춰 로딩하더라도 상당한 메모리를 먹을 테고 넥서스에서 20개 올려지던 비트맵이 갤탭에선 5개도 못올리는 현상이 생길 수 있다. 그럼 limit 은 얼마를 줘야 하나...??
어림 짐작으로 해야하는 이런 주먹구구식 코딩은 하고 싶지가 않다...ㅠ.ㅠ 모르겠다. 정 안되면 할지도.. 어느 기기에서 죽는다는 피드백이 오면 limit 을 조금씩 낮춰가는... 그런 조잡한 짓을 하게 될지도 모르겠다. 하지만 앤디 루빈 아저씨....ㅜ.ㅜ 이건 아니 잖아요 !!!!!!

답을 못찾으니 그저 한숨만 나올 뿐이로군요..

 

안드로이드 개발환경을 구성하고 익숙하지도 않은 이클립스를 사용하여 Hello World 프로그램을 만들 때 가장 눈에 거슬리는 것이 다음 화면 이었다. 에뮬에서 사용할 가상머신의 규격을 설정하는 것인데 스마트폰 기기에 대해서 아는바가 없어 그냥 막고 품어 왔지만 중요한 부분이라서 정리해 본다.

 

스마트폰 스팩에 대해서 잘 정리해 놓은 자료가 있어서 퍼왔다. 스마트폰 별로 화면의 크기가 몇 인치이며 가로세로 비율 지원하는 해상도와 해상도에 대한 약어를 표시하고 있다. 

V:H

픽셀

종류

크기

          

5:3

800*480

WVGA

4.3”

HTC HD2, HTC EVD 4G

4.0

갤럭시S AMOLED

3.7”

HTC Desire

SAMSUNG 옴니아2 AMOLED Omnia-II

3.5”

옵티머스 Q

3.0”

갤럭시A  AMOLED PLUS

-

SonyEricsson Xperia X1

80:49

800*490

-

3.7”

PANTECH 시리우스(Sirius) AMOLED

-

854*480

-

3.7”

Motorola MOTOROI

4.0”

SonyEricsson Xperia X10

4:3

640*480

VGA

-

HTC Touch Diamond

320*240

QVGA

-

HTC Touch Dual

1024*768

XGA

9.7”

APPLE 아이패드

16:9

640*360

-

3.2”

NOKIA 5800 Express Music

3:2

480*320

HVGA

3.0”

안드로원

3.5”

Apple iPhone 3GS (애플 아이폰 3GS)

APPLE 아이팟터치

-

LG Andro-1

-

HTC G1

960*640

 

3.5”

APPLE 아이폰4G

-

1024*600

 

 

갤럭시 탭

 

스크린사이즈 (Screen Size) : 스크린 사이즈는 스크린의 대각선 크기 값으로 물리적인 크기를 나타낸다. 안드로이드는 스크린 사이즈를 large, normal, small 로 나눈다.

 

가로세로비 (Aspect ratio) : 가로 세로비는 스크린의 물리적인 넓이와 높이 비율을 말한다. 안드로이드에서는 리소스 제한자인 long, notlong 을 이용하여 화면 비율에 대한 layout 리소스를 제공한다 .

 

해상도 (Resolution) : 스크린이 가지고 있는 전체 픽셀수를 나타낸다 . 해상도가 보통 "넓이 * 높이" 로 표현되기는 하지만 해상도가 특정 가로세로비 (Aspect ratio) 를 의미하지는 않습니다 . 안드로이드에서는 해상도를 직접 처리하지는 않습니다 .

 

밀도 (Density) : 스크린 해상도를 기반으로 물리적 넓이와 높이안에 얼마나 많은 픽셀이 들어 있는가를 나타낸다. 저밀도(Lower density) 스크린에서는 같은 넓이와 높이안에 더 적은 수의 픽셀이 있고 고밀도(Higher Density) 스크린에서는 같은 넓이와 높이안에 더 많은 수의 픽셀이 있다. 안드로이드에서 Density는 아주 중요한 개념이다. 만일 UI 요소들을 pixel 단위로 크기를 지정하면 낮은 density 화면에서는 더 크게 보이고 높은 density 화면에서는 더 작게 보인다. Android는 density를 high, medium, low로 나누며 , 플랫폼에서는 실제 스크린밀도에 맞게 리소스들의 사이즈를 조정한다. 밀도는 (dpi : dot per inch, density per inch,  ppi : pixel per inch) 단위를 사용한다.

 

길이단위 : View나 위젯들에게 부여된는 _width, _height 속성값에 부여되는 단위로 px(픽셀), in(인치), mm(밀리미터), pt(포인트) 처럼 기존의 웹개발에서 많이 봐왔던 단위도 있지만 dp(또는 dip), sp(또는 sip, 밀도에 독립적니 폰트단위) 같은 생소한 단위도 있다. dip(Density Independent Pixel)는 밀도(density)와 상관없이 레이아웃의 위치나 크기를 표현할 때 사용하는 가상의 pixel 단위다. 1dip는 밀도가 160 일때 1픽셀에 해당한다. 즉 Mediaum Density(160), mdpi 화면에서는 1pixel = 1dip 이며 , 다른 dip에서 픽셀변환공식은 pixels  = dips * (density / 160) 로 처리된다.

 

표준 디스플레이 해상도 : VGA, QVGA 같은 약어들을 알아보자.  640*480 해상도를 VGA(Video Graphic Array) 라고 부르며 여기에 Quarter, Half, Wide, Full, Super, Ultra 같은 걸 붙여서 크기를 나타낸다.

 

QVGA(Quarter VGA) 320*240 = 0.077mpx
WQVGA(Wide QVGA) 400*240 
FWQVGA(Full WQVGA) 432*240
HVGA(Half VGA)   320*480 = 0.15mpx  2:3
VGA(Video Graphic Array) 640*480=0.3mpx  4:3
WVGA(Wide VGA) 800*480  5:3
FWVGA(Full Wide VGA) 854*480  16:9
SVGA(Super VGA) 800*600 = 0.5mpx   4:3
XGA/XVGA(Extended Graphic Array) 1024*768 = 0.8mpx
WXGA 1280*800 = 1.0mpx
SXGA 1280*1024 = 1.3mpic
WXGA+ 1440*900 = 1.3mpx
SXGA+ 1400*1050 = 1.4mpx
WSXGA+ 1680*1050 = 1.7mpx
UXGA  1600*1200 = 1.9mpx
WUXGA 1920*1200 = 2.3mpx
QXGA 2048*1536 = 3.1mpx
WQXGA 2560*1600 = 4.1mpx 
QSXGA 2560*2048 = 5.2mpx
WQSXGA 3200*2048 = 6.6mpx
QUXGA 3200*2400 = 7.7mpx
WQUXGA 3840*2400 = 9.2mpx
WUQSXGA 4200*2690 = 11.3mpx

 

이정도 정리하고 나니까 에뮬레이터의  시작할 때 올라오는 시작옵션창이 어느정도 이해가 간다.

 

 

  

이제 한 단계 넘어서 다양한 화면크기의 다양한 밀도를 가지는 안드로이드 모바일 기기를 고려하면서 개발하기에 필요한 정보들을 정리해 본다.  다음 표는 Android 에서 지원되는 Screen Size 와 Density 간의 관계를 나타낸다.

안드로이드는 폰이 다양해서 여러 UI를 동시 처리하기 위해서 신경써야 한다. 그중 제일 중요한 것은 density 이며  dpi라는 단위를 사용해야 유연성이 높일 수 있다. 이 표는 가지각색의 안드로이드 기기을 일정정도 그룹화하여 이미지나 레이아웃을 구성하는 기준으로 삼자는 것이다.  Baseline은 HVGA, Normal Screen, Medium density 이며 dip와 pixel이 1:1 로 매칭되는 조건이다. 국내 폰들은 현재 WVGA(400*800) hdpi 가 지배적이다. 실제 개발 시 장치 종류마다 별도의 리소스를 따로 마련해 두고 각각의 폴더이름에  밀도를 나타내는 (ldpi, mdpi, hdpi, nodip) 같은 한정자를 사용하여 관리한다. 다양한 크기 및 density 를 지원해야 하기 때문에 안드로이드 UI 개발은 iPhone UI 개발보다 훨씬 복잡하고 귀챦다. 그렇기 때문에 안드로이드가 내부적으로 UI를 처리하는 방식을 이해하고 개발할 앱의 타겟을 잘 잡아서 만들어야 한다 .  

 

 표준 리소스를 디렉토리 샘플

 res/anim

 res/raw

 res/xml

 res/layout/*.xml                                     -> Noraml 스크린 사이즈 레이아웃

 res/layout-small/layout.xml                     -> Small 스크린 사이즈 레이아웃
 res/layout-large/layout.xml                     -> Large 스크린 사이즈 레이아웃

 res/drawable

 res/drawable-ldpi/icon.png                    -> Low density 를 위한 아이콘

 res/drawable-mdpi/icon.png                   -> Medium density 를 위한 아이콘

 res/drawable-hdpi/icon.png                   -> High density 를 위한 아이콘

 res/drawable-nodpi/res.xml                    -> density 와 무관한 리소스

 res/menu

 res/values/strings.xml, colors.xml, styles.xml, arrays.xml, dimens.xml

 res/values-ko

 

 

접미어

 

언어 (us, kr, fr, ja),  지역 (rUS, rKR, rFE), 화면방향 (port, land, square), 해상도 (92dpi, 108dpi),
가로세로비 (long, notlong), 스크린사이즈 (small, normal, large, 320*240, 480*320)

코드나 리소스에서 참조시 접미어는 붙이지 않는다. 여러개의 접미어를 사용할 때 - 를 연속해서 사용하고 일정 순서에 의거하여 작성해야 한다. 대소문자를 구분하고 중첩은 허용하지 않는다. 

 

 

 

 


How Android Supports Multiple Screens

(원문펌 http://overoid.tistory.com/9)

 

 안드로이드에서는 런타임시에 아래 3 가지 방식 중 하나로 다양한 화면을 지원합니다 . 아주 중요한 내용입니다 .

 
i) Pre -Scaling ( 보통 bitmap 이미지 처리시에)

 Pre -Scaling 은 로딩시점에 크기를 조절합니다 . CPU 에 이득이 있다고 알려져 있습니다. Pre -Scaling 은 폰의 Density 를 기준으로 동일한 dpi 디렉토리에 지정된 리소스를 로딩하며 이때는아무런 크기 변환없이 보여줍니다 . 즉 폰이 hdpi density 라면 res/drawable-hdpi 디랙토리 하위의 리소스를 먼저 찾아서 있다면 아무런 크기 변환없이 그대로 보여주게 됩니다. 만일 매칭되는 리소스가 없다면 디폴트 리소스 (basline) 를 로딩하고 로딩 시에 적합한 density로 크기 변환을 합니다 . 예를들어 res/drawable-mdpi/ 에 100*100 아이콘만 존재한다고 할 때 만일 폰의 사양이 hdpi 라면 안드로이드는 drawable-mdpi 하위의 아이콘을 읽을때 자동으로 크기를 확대해서 150 * 150 bitmap 을 만듭니다. 반대로 drawable-hdpi/ 에 150*150 아이콘만 존재할 때 mdpi 폰에서 읽으면 자동으로 100*100 아이콘으로 크기를 변환합니다 . drawable-hdpi 및 drawable- mdpi에 모두 이미지가 존재한다면 별도의 스케일 변환 작업이 필요없으니 좀 더 성능에 유리 합니다.

 

 ii) Auto-scaling (pixel dimensions and coordinates)

 Auto-scaling 은 그리는 시점에 크기를 조절합니다 . 메모리에 이득이 있다고 알려져 있습니다 . 주로 Pixel 좌표, Pixel Dimesion 값, Application 에서 사용된 Pixel 수식 등에 적용되며, 리소스가 아닌 웹이나 SD카드에서 Bitmap 데이터를 가져왔을 때도 적용됩니다. 쉽게 얘기하면 App에서 ( 10,10) 에서 (100,100) 좌표로 4각형을 그리도록 구현했다면 High -Density(240dpi) 화면을 가진 Device 에서는 그리는 시점에 자동으로 스케일을 변환해서 (15,15) (150,150) 좌표에 사각형을 그리게 된다 .

 

ii) Compatibility Mode (호환모드)

 Large 스크린을 지원 안하는 앱을 Large 스크린에서 실행하면 검은 배경에 원래 크기만큼만 표시합니다. 보기에는 별로입니다 . 앱이 싼티 납니다. 아 .. 여전히 복잡하고 어렵군요 . 원문 보시면 좀 더 상세하지만 복잡한 내용들로 가득 차 있습니다. 중요한 점은 안드로이드가 여러 화면사이즈를 지원하기 위해서 런타임시에 자동으로 크기를 바꾼다는 것을 기억하십시요 .

 

 

제가 사용하는 기본 레이아웃 디자인 룰은 다음과 같습니다 .

 

 i) 레이아웃 디자인시에는 HVGA 기본 스크린 사이즈를 중심으로 DIP 단위만을 사용해서 디자인합니다. px과 dip가 1:1 이라서 화면 크기에 대응하여 사이즈 결정하기가 좋습니다. 320 * 480 화면 기준으로 들어갈 이미지나 UI 요소들 각각의 가로 사이즈를 px로 계산한 후 코딩시에는 그 값의 단위를 dip 로만 입력하 면 됩니다 .

 

 ii) AbsoluteLayout 을 사용하지 않습니다. 즉 , 화면의 절대 좌표 보다는 상대 좌표를 사용해야 합니다. 이거 사용해서 디자인하면 ..나중에 감당 안됩니다 .

 
 iii) Bitmap 은 HDPI 기준으로 만듭니다. 그래야 자동으로 크기가 조정 되더라도 보기에 좋습니다. 즉  HVGA 를 기준으로 계산했을 때 100 * 100 이미지가 필요하다면 HDPI 기준으로 크기의 1.5 배인 150 *150 크기로 실 이미지를 제작하시면 됩니다. 만일 여력이 된다면 hdpi 및 mdpi 기준으로 각각 만드는 것도 나쁘지는 않습니다. 위에서 언급했듯이 성능에 유리합니다.

 

iv) 각 장치별로 레이아웃 및 이미지를 모두 별도로 만들어서 세밀하게 조정할 수도 있지만 그런 경우 관리가 힘듭니다 . 가급적 레이아웃과 이미지를 적게 사용해서 통일된 UI를 구성하는 것이 좋습니다.

 

 

 

 

 

 

스마트폰 해상도관련 사항

 

2.1 가로/세로 값 

    this.getWindowManager().getDefaultDisplay().getWidth();

    this.getWindowManager().getDefaultDisplay().getHeight();

    context.getResources().getDisplayMetrics().widthPixels
    context.getResources().getDisplayMetrics().heightPixels

    context.getResources().getDisplayMetrics.density

 

2.2 주의 사항

  i) 3요소가 일치해야 한다

    <uses-sdk android:minSdkVersion="8" />... .

    프로젝트 만들때 Build target설정에서 Platform 2.2버젼...

    스마트폰의 안드로이드 OS 버젼(2.2)...

  

  ii) 스마트폰이나 에뮬의 버젼이 제일 최신버젼이어야 한다

      스마트폰의 안드로이드 OS 버젼을 최대기준으로 볼 때 uses-sdk Build target이 적거나 같아야 한다.

 

   iii) 결론

일반적으로 대부분의 스마트폰 해상도는 480*800아나 위의 두 가지 조건이 맞지 않으면 320*533으로 나올 것이다예를 들어 스마트폰은 2.2버젼인데 새로나온 2.3버젼인 9API로 프로젝트를 만들면 해상도는 320*533으로 나올 것이다.

 

2.3 해상도 테스트

  i)    AndroidManifet 상에

       <supports-screens android:largeScreens = ”true” android:normalScreens = ”true”

         android:smallScreens = ”true” android:anyDensity = ”true”/>

  ii)    글자의 크기는 XML상에 px가 아닌 sp로 할 것.

  iii)    Widget의 크기 및 padding dip로 사용할 것.

  iv)    LayoutLinearLayout 이나 RelativeLayout 등을 사용할 것

  v)    안드로이드 에뮬레이트 안의 AVD manager Add해서 해상도를 여러 개 적용하여 테스트하면

       도움이 될 것임.

 

 

 

 

 

해상도 밀도 관련 : (구글크롬으로 접속해서 번역하면 쉽게 이해 볼수 있당)

참고 http://www.kandroid.org/guide/practices/screens_support.html

  

스마트폰 해상도 정리

참고 : http://blog.naver.com/handyson/120391295


 
/**
  *  URI로부터 실제 파일 경로를 가져 온다.
  *  @param uriPath URI : URI 경로
  *  @return String : 실제 파일 경로
  */
 public String getRealImagePath (Uri uriPath) {
  String[] proj = { MediaStore.Images.Media.DATA };
  Cursor cursor = managedQuery(uriPath, proj, null, null, null);
  int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
  cursor.moveToFirst();
 
  String path = cursor.getString(index);
  path = path.substring(5);
  return path;
 }

 

public String getRealPathFromURI(Uri contentUri) {
  String[] proj = { MediaStore.Images.Media.DATA };
  Cursor cursor = managedQuery(contentUri, proj, null, null, null);
  int column_index = cursor
    .getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
  cursor.moveToFirst();
  return cursor.getString(column_index);
 }

+ Recent posts