월별 글 목록: 2013년 7월월

안드로이드(Android) 앱에서 메모리 캐시 사이즈를 디바이스별로 동적으로 조절하기

안드로이드(Android)에서 많은 썸네일(Thumbnail)을 처리하기 위해서 많은 앱들이 1차 메모리, 2차 디스크 캐시를 사용하고 있다. 여기에서 2차 디스크는 용량이 충분하기 때문에 별 이슈가 없다. 하지만, 메모리 캐시의 경우에는 디바이스별로 다르기 때문에 보통은 작은 사이즈로 지정해서 사용하게 된다. 지금 출시되는 안드로이드 디바이스의 앱 메모리 설정을 보면, 이전보다 매우 크게 사용할 수 있는 것을 알수가 있다.

그래서, 메모리 캐시를 더 크게 사용해서, UX의 반응을 더 높이는 방법을 살펴보자. 기본적으로, http://www.sjava.net/332 를 보면,  안드로이드 OS의 설정정보를 읽어서, 디바이스의 정보를 알수가 있겠다.

앱마다 사용하는 썸네일 사이즈에 따라, 메모리를 점유하는 크기가 달라지기 때문에 이 내용은 앱에서 처리하는 썸네일의 크기에 따라 달라진다.

우선, 썸네일이 점유하는 메모리의 크기를 살펴보면..

100 x 100의 썸네일인 경우, 100x100x4의 크기이니 40,000 byte의 메모리를 점유한다고 보면 되겠다.  그래서, 대략적으로 캐시 사이즈에 따른 메모리 사용량을 가늠해 보면..

100×100 썸네일을 메모리에 100개 올리게 되면, 4M 정도의 메모리를 점유하게 될 것이고.. 이 정보를 기준으로 캐시 사이즈를 정하면 되겠다..

위의, 정보를 기준으로 해서 디바이스가 허용하는 앱의 힙 정보를 살펴보면..

* 갤럭시 넥서스

07-24 11:51:04.377: E/CacheUtil(27521): dalvik.vm.heapstartsize : 8m
07-24 11:51:04.377: E/CacheUtil(27521): dalvik.vm.heapgrowthlimit : 96m <– 최대 힙 사이즈를 기준으로 캐시의 사이즈를 조절한다.
07-24 11:51:04.377: E/CacheUtil(27521): dalvik.vm.heapsize : 256m <– 전체 힙 사이즈

* 넥서스 10

07-24 12:02:46.003: E/CacheUtil(12619): dalvik.vm.heapstartsize : 16m
07-24 12:02:46.008: E/CacheUtil(12619): dalvik.vm.heapgrowthlimit : 192m <– 최대 힙 사이즈를 기준으로 캐시의 사이즈를 조절한다.
07-24 12:02:46.008: E/CacheUtil(12619): dalvik.vm.heapsize : 512m <– 전체 힙 사이즈

위의 정보를 바탕으로 캐시클래스를 만들어 보면..

import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.graphics.Bitmap;

public class CacheUtil {
	static final String TAG = CacheUtil.class.getSimpleName();
	static int THUMBNAIL_CACHE_COUNT = 100;
	static boolean DEBUG = true;

	static{
		if(DEBUG) {
			Logger.e(TAG, "dalvik.vm.heapstartsize : "  + get("dalvik.vm.heapstartsize"));
			Logger.e(TAG, "dalvik.vm.heapgrowthlimit : "  + get("dalvik.vm.heapgrowthlimit"));
			Logger.e(TAG, "dalvik.vm.heapsize : "  + get("dalvik.vm.heapsize"));
			Logger.e(TAG, "cache_size : "  + getDynamicCacheSize());
		}
		
		THUMBNAIL_CACHE_COUNT = getDynamicCacheSize();
	}
		
	static LruCache<String, Bitmap> listCache = new LruCache<String, Bitmap>(THUMBNAIL_CACHE_COUNT);
	public static LruCache<String, Bitmap> listCache() {
		if(listCache == null)
			listCache = new LruCache<String, Bitmap>(THUMBNAIL_CACHE_COUNT);
		
		return listCache;
	}
	
	static LruCache<String, Bitmap> gridCache = new LruCache<String, Bitmap>(THUMBNAIL_CACHE_COUNT);
	public static LruCache<String, Bitmap> gridCache() {
		if(gridCache == null)
			gridCache = new LruCache<String, Bitmap>(THUMBNAIL_CACHE_COUNT);
		
		return gridCache;
	}

	
	static int getDynamicCacheSize() {
		String value = get("dalvik.vm.heapgrowthlimit");
		if(StringUtil.isEmpty(value))
			return THUMBNAIL_CACHE_COUNT;
		
		// M가 단위로 가져온다.. 
		int size = extractInt(value);
		if(size <= 64)
			return 100;
		
		if(size <= 128)
			return 200;
		
		return 300;
	}
	
	private static String get(String key) {
		try {
			Class clazz = Class.forName("android.os.SystemProperties");
			if (clazz == null)
				return "";

			Method method = clazz.getDeclaredMethod("get", String.class);
			if (method == null)
				return "";

			return (String) method.invoke(null, key);
		} catch (Exception e) {
			if (DEBUG)
				Logger.e(TAG, ExceptionUtil.exception(e));
		}

		return "";
	}
	
	private static int extractInt(String str) {
        Matcher matcher = Pattern.compile("\\d+").matcher(str);
        if (!matcher.find())
            throw new NumberFormatException("For input string [" + str + "]");

        return Integer.parseInt(matcher.group());
    }
}

위 클래스를 이용해서, 디바이스에 설정되어 있는 메모리 크기의 정보를 기준으로 메모리 캐시의 크기를 동적으로 설정할 수가 있겠다.

안드로이드(Android) 3.2 이상에서 앱의 설정부분을 폰/태블릿에 적합하게 보여주자..

이제는 많은 안드로이드 앱들이 한 바이너리에서 폰과 태블릿 UI를 화면에 맞게 지원하기 시작했고, 이와 더불어 설정 화면도 폰과 태블릿을 한꺼번에 지원할 수 있도록 안드로이드 개발 사이트에서 가이드를 하고 있다. 안드로이드의 설정 부분에 대한 가이드는 http://developer.android.com/guide/topics/ui/settings.html 에서 볼 수 있고, 폰 전용, 폰과 태블릿 그리고 태블릿 전용으로 적합한 설정화면을 어떻게 나타내야 하는지를 쉽게 배울 수 있다.

우선 대표적인 안드로이드 앱인 GMail 앱이 어떻게 폰과 태블릿을 지원하는지 살펴보자. 아래의 왼쪽 화면은 폰 버전의 설정화면이고, 우측이 태블릿의 설정화면이다. GMail 앱은 구글의 서비스인 만큼 안드로이드 가이드를 매우 충실히 따르고 있고, 가장 좋은 레퍼런스 중의 하나이다.

하지만 위 GMail 앱의 폰 UI는 한번 더 들어가야 실제 설정화면을 볼 수 있어서, 개인적으로 폰에서는 좋은 UI가 아니라고 생각한다. 그리고, 많은 안드로이드 앱이 설정(Settings) 메뉴를 클릭하면 바로 설정에 관한 내용을 보여 주고 있는데, 이게 태블릿에서 보면 좋아 보이지 않는다.

그래서, 폰과 태블릿의 UI 가이드를 따르면서, 기존의 GMail 앱과는 다른 UI를 사용해 보는게 어떨까 한다. 아래는 예제 프로그램에서 보여지는 설정 부분의 메뉴화면이다.

그래서, 폰과 태블릿에서 확인하면..
아래처럼, Settings 화면에서 폰과 태블릿의 다른 UI를 확인할 수 있다.

 

위의 옵션 메뉴는 동일하게 보여지기 때문에, 아래의 menu>main.xml에서 동일한 코드로 구성을 한다.

그리고 각 설정화면은, PreferenceFragment를 상속하고 있는 클래스(AboutPreferenceFragment, SettingPreferenceFragment)에서는 xml> 디렉토리에 있는 about_preference.xml과 settings_preference.xml을 사용해서 각 화면을 처리한다.

촤측의 이미지에서, 태블릿(sw720dp는 10.1인치, sw600dp는 7인치를 표현)을 처리하기 위한 코드는 SettingsActivity.java에서 구현을 하고 있다. 이 클래스의 내용은 간단하게, xml 헤더가 없으면(폰 UI), SettingPreferenceFragment를 android.R.id.content에 바인딩해서, 폰 UI를 표현한다.

 

 

 

 

package net.sjava.ex.preference;

import java.util.List;

import android.os.Bundle;
import android.preference.PreferenceActivity;
import net.sjava.ex.preference.R;
import net.sjava.ex.preference.fr.SettingPreferenceFragment;

public class SettingActivity extends PreferenceActivity {
	static final String TAG = SettingActivity.class.getSimpleName();

	@Override
	public void onBuildHeaders(List<Header> target) {		
		try {
			if (getResources().getIdentifier("settings_header", "xml", getPackageName()) <= 0)
				return;

			loadHeadersFromResource(R.xml.settings_header, target);
		} catch(Exception e) { }
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);	

		if(!hasHeaders())
			createTransaction().replace(android.R.id.content, new SettingPreferenceFragment()).commit();
	}

	protected android.app.FragmentTransaction createTransaction() {
		return getFragmentManager().beginTransaction();
	}
}

소스코드는..
cfile21.uf@27489E4151E6D981039453.zip이 UI/UX는 안드로이드 공식 UI 가이드를 정확하게 따르지는 않지만, 폰과 태블릿을 한꺼번에 지원하는 세팅메뉴에서 한 번은 생각해 볼 만한 이슈라고 생각한다. 개인적으로, 이 UI가 더 좋다고 생각한다.

자바 CountDownLatch를 이용한 동시성 높이기..

각종 환경에서 성능을 높이기 위해서 취하는 방법이 멀티 프로세스와 멀티 쓰레드이다. 자바에서는 성능을 높이기 위해서 기본적으로 멀티 쓰레드의 형태를 취하고 있다. 물론, JNI를 사용해서 Native의 프로세스를 띄울 수 있기도 하다. 자바의 기본 패키지중에서 쓰레드를 사용해서 동시성을 극대화 시키는 각종 자료구조와 클래스들은 java.uti.concurrent 패키지를 확인해 보면 알 수 있다.

수학적인 계산을 제외한, 거의 모든 컴퓨터 성능이슈는 느린 I/O 처리를 어떻게 빠르게 처리하냐가 주된 관심사가 아닐까 한다. 

그래서, 많이 쓰이는 쓰레드의 형태를 살펴보면..
1. 잡을 개별 쓰레드로 위임한다.
2. 잡을 개별 쓰레드가 처리하고 결과를 취합해서, 취합된 결과를 기준으로 처리를 한다.

보통은, 위의 1.번으로 처리를 하는 경우가 많지만, 간혹, 2.번의 형태가 필요한 경우가 발생한다. 이 경우에 보통은 메인 쓰레드에서 잡 쓰레드를 생성하고, 자신은 wait하고 있다가 notify를 받는 구조를 많이 취하게 된다. 바로 이런 형태로 2.번의 상황을 처리할 수 있게 해 주는 클래스가 CountDownLatch이다.

자, 간단하게 예제를 살펴보자.

package net.sjava.example.countdownlatch;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
	static final int max = 10;
	/**
	 * 단일 쓰레드 테스트
	 */
	public static void testSingle() throws Exception {
		long start = System.currentTimeMillis();
		for (long i = 0; i < max; i++) {
			Thread.sleep(1000);
		}

		long elapsedTime = System.currentTimeMillis() - start;
		System.out.println("testSingle elapsed time -> " + elapsedTime);
	}

	/**
	 * CountDownLatch 테스트
	 */
	public static void testCountDownLatch() throws Exception {
		final CountDownLatch latch = new CountDownLatch(max);

		long start = System.currentTimeMillis();
		for (long i = 0; i < max; i++) {
			new Thread(new Worker(latch)).start();
		}

		latch.await();
		long elapsedTime = System.currentTimeMillis() - start;
		System.out.println("testCountDownLatch elapsed time -> " + elapsedTime);
	}

	/**
	 * Job 쓰레드
	 */
	static class Worker implements Runnable {
		private CountDownLatch latch;

		public Worker(CountDownLatch latch) {
			this.latch = latch;
		}

		@Override
		public void run() {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException ex) {
				ex.printStackTrace();
			} finally {
				if (this.latch == null)
					return;

				latch.countDown();
			}
		}
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		testSingle();
		testCountDownLatch();
	}
}

위의 코드의 결과는 아래와 같아서, 너무 극단적이긴 하지만, CountDownLatch의 사용을 위한 쉬운 예가 될 것이다.

testSingle elapsed time -> 10,000
testCountDownLatch elapsed time -> 1,004

오페라 넥스트에서 크롬 앱 스토어의 확장 프로그램(Extension) 사용하기..

오페라(Opera) 넥스트에서 크롬(Chrome) 앱 스토어의 확장 프로그램(Extension)을 사용하는 방법이다.

1. 오페라->확장기능->더 많은 확장기능 보기를 클릭
2. 오페라 add-on 웹 사이트에서 Download Chrome Extension 앱을 설치한다.
– 타이틀에서 알 수 있듯이 크롬 앱 스토어의 확장 프로그램을 설치해 주는 오페라 Extension이다.

3. 오페라에서 크롬 앱 스토어(https://chrome.google.com/webstore/category/apps)를 열고, 원하는 확장 프로그램을 설치해 주면 된다.
3.1 확장 프로그램 설치 완료..
– URL을 표시해 주는 박스 우측에 현재 설치된 확장 프로그램의 일부를 볼 수 있고, 전체 확장 프로그램은 오페라->확장기능 메뉴를 클릭해서 볼 수 있다.

3.2 확장 프로그램이 아닌 앱은 설치 불가..

* Reference
http://www.omgchrome.com/opera-extensions-can-be-installed-in-chrome/

안드로이드 기기 정보 확인하기

안드로이드 기기 정보는 디버깅을 위해서 꼭 필요한 정보이다. 그래서 기기 정보를 확인하는 방법을 살펴보자. 안드로이드 기기 정보를 확인하는 방법으로 Adb 툴을 사용할 수 있고, 리플렉션을 이용해서 코드로 정보를 확인할 수 있다.  코드에서 안드로이드 기기의 정보를 얻기 위해서 android.os.Build 클래스에서 제공하는 각종 static 변수의 값을 사용한다. 하지만 이 클래스가 제공하는 정보는 제한적이어서 더 많은 정보가 필요할 수 있다. 이런 경우, android.os.SystemProperties 클래스를 사용해서 더 많은 기기의 정보를 확인할 수 있다. 그리고 이 클래스를 사용하기 위해서는 시스템 앱으로 개발되지 않는 이상 기기의 정보를 확인할 수 없게 되어 있다. 그래서, 안드로이드가 제공하는 기기의 정보를 앱 레벨에서 확인하는 방법에 대해서 살펴보자.

1. Adb를 사용하는 방법

1.1 Adb를 사용해서 기기 설정 파일 가져오기..

mcsong@mcsong-ubuntu:~$ adb -d pull /system/build.prop
45 KB/s (3212 bytes in 0.069s)

1.2 기기 설정 파일 정보..

# begin build properties
# autogenerated by buildinfo.sh
ro.build.id=JDQ39
ro.build.display.id=JDQ39
ro.build.version.incremental=573038
ro.build.version.sdk=17
ro.build.version.codename=REL
ro.build.version.release=4.2.2
ro.build.date=Fri Feb  8 22:38:31 UTC 2013
ro.build.date.utc=1360363111
ro.build.type=user
ro.build.user=android-build
ro.build.host=wpef10.hot.corp.google.com
ro.build.tags=release-keys
ro.product.model=Galaxy Nexus
ro.product.brand=google
ro.product.name=mysid
ro.product.device=toro
ro.product.board=tuna
ro.product.cpu.abi=armeabi-v7a
ro.product.cpu.abi2=armeabi
ro.product.manufacturer=samsung
ro.product.locale.language=en
ro.product.locale.region=US
ro.wifi.channels=
ro.board.platform=omap4
# ro.build.product is obsolete; use ro.product.device
ro.build.product=toro
# Do not try to parse ro.build.description or .fingerprint
ro.build.description=mysid-user 4.2.2 JDQ39 573038 release-keys
ro.build.fingerprint=google/mysid/toro:4.2.2/JDQ39/573038:user/release-keys
ro.build.characteristics=nosdcard
# end build properties
#
# system.prop for toro
#

rild.libpath=/vendor/lib/libsec-ril_lte.so
rild.libargs=-d /dev/ttys0
telephony.lteOnCdmaDevice=1

# Ril sends only one RIL_UNSOL_CALL_RING, so set call_ring.multiple to false
ro.telephony.call_ring.multiple=0

# Turn on IMS by default
persist.radio.imsregrequired=1
persist.radio.imsallowmtsms=1

# Default ecclist
ro.ril.ecclist=112,911,#911,*911

#
# ADDITIONAL_BUILD_PROPERTIES
#
ro.com.google.clientidbase=android-verizon
ro.com.google.locationfeatures=1
ro.url.legal=http://www.google.com/intl/%s/mobile/android/basic/phone-legal.html
ro.url.legal.android_privacy=http://www.google.com/intl/%s/mobile/android/basic/privacy.html
ro.setupwizard.mode=OPTIONAL
ro.cdma.home.operator.numeric=310004
ro.cdma.home.operator.alpha=Verizon
ro.cdma.homesystem=64,65,76,77,78,79,80,81,82,83
ro.cdma.data_retry_config=default_randomization=2000,0,0,120000,180000,540000,960000
ro.gsm.data_retry_config=max_retries=infinite,default_randomization=2000,0,0,80000,125000,485000,905000
ro.gsm.2nd_data_retry_config=max_retries=infinite,default_randomization=2000,0,0,80000,125000,485000,905000
ro.config.vc_call_vol_steps=7
ro.cdma.otaspnumschema=SELC,1,80,99
wifi.interface=wlan0
media.aac_51_output_enabled=true
ro.opengles.version=131072
ro.sf.lcd_density=320
ro.hwui.disable_scissor_opt=true
# 디바이스 별 힙 정보
dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=96m
dalvik.vm.heapsize=256m
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=8m
ro.config.ringtone=Themos.ogg
ro.config.notification_sound=Proxima.ogg
ro.config.alarm_alert=Cesium.ogg
ro.com.android.dateformat=MM-dd-yyyy
ro.com.android.dataroaming=false
ro.carrier=unknown
ro.com.android.wifi-watchlist=GoogleGuest
ro.error.receiver.system.apps=com.google.android.feedback
ro.setupwizard.enterprise_mode=1
keyguard.no_require_sim=true
drm.service.enabled=true
ro.facelock.black_timeout=1250
ro.facelock.det_timeout=1500
ro.facelock.rec_timeout=2500
ro.facelock.lively_timeout=2500
ro.facelock.est_max_time=800
ro.facelock.use_intro_anim=true
camera.flash_off=0
dalvik.vm.dexopt-flags=m=y
net.bt.name=Android
dalvik.vm.stack-trace-file=/data/anr/traces.txt

2. 리플렉션을 사용하는 방법

2.1 리플렉션을 사용하는 코드..

public static String get(String key) {
	try {
		Class clazz = Class.forName("android.os.SystemProperties");
		if(clazz == null)
			return "";
		
		Method method = clazz.getDeclaredMethod("get", String.class);
		if(method == null)
			return "";
		
		return (String) method.invoke(null, key);
	} catch (Exception e) {
		ZLog.e("WHOOPS", "Exception during reflection: " + e.getMessage());
	}
	
	return "";
}

2.2 위 코드의 결과

ZLog.e(TAG, "HeapStartFree --> " + SystemProperties.get("dalvik.vm.heapstartsize"));
ZLog.e(TAG, "HeapMaxFree --> " + SystemProperties.get("dalvik.vm.heapmaxfree"));
HeapStartFree --> 8m
HeapMaxFree --> 8m

이 코드로 간단하게 로그를 확인하면, 위에서 확인한 설정파일인 build.prop의 안드로이드 기기 정보를 확인할 수 있다. 변경도 가능하겠지만, 시스템 정보라서 권한 문제가 발생할 수 있다. 만약 앱이 시스템 권한을 가진다면 이런 리플렉션을 사용하지 않아도 바로 사용할 수 있겠다.