Thumbnail을 가져와야 하니, 보통 위 메소드 안에서 Thumbnail을 가져오기 위해, AsyncTask를 상속받은 쓰레드로 처리를 하는 것이 일반적입니다. 이 상황은 쓰레드를 많이 생성하게 됩니다..
위의 경우에 RejectedExecutionException 이 발생할 수 있습니다.. 안드로이드는 내부적으로 java.util.concurrent 패키지의 ExecutorService를 Thread 디스패칭에 사용하고 있습니다. 따라서, 요 문제는 아래의 ExecutorService가 처리해야 하는 AsyncTask의 POOL_SIZE가 순간적으로 넘어서고, 이 경우에 AsyncTask가 풀에 들어갈 수 없어서 발생하는 익셉션입니다. 위 문제는 빈번하게 발생하지 않을 것 같지만, 게임이라던지 쓰레드가 많이 필요한 작업을 하다 보면 발생할 수 있습니다..
아래는 Android 플랫폼에서 기본적으로 가지고 있는 AsyncTask의 정책값입니다.
private static final int CORE_POOL_SIZE = 1;
private static final int MAXIMUM_POOL_SIZE = 10;
private static final int KEEP_ALIVE = 10;
따라서, 위 문제는 하나의 쓰레드로 30개의 이미지를 가져오는 형태로 살짝 바꾸면 해결이 됩니다. 당근 성능과 안전성의 트레이드 오프라고 할 수 있겠습니다..
위에서 간단하게 RejectedExecutionException을 피해갈 수 있는 방안에 대해서 살펴보았습니다..
위에서 고려한 사진의 Thumbnail에 대한 예로, 한번 더 고민을 해 보면, 만약에 30개를 가져오는 쓰레드를 getView()외에서 처리를 한다면, 지금 보고 있는 화면에서 사진이 안 보일 겁니다.. 왜냐하면, getView()에서 처리를 하지 않았기 때문에 빈 사진만 보이겠죠.
그래서, 위 가정대로, 사진이 많아서 페이징을 해야하고 Thumbnail을 보여준다면, 사진을 가져오는 첫 페이지에서는 getView()에서 AsyncTask를 불러서 사진을 가져오고, 두 번째 페이지 부터는 데이터를 가져와서 Adapter에 바인딩하는 쓰레드가 30개의 사진을 처리하는 쓰레드를 다시 부르는 형태로 구현을 하는 것이 좋을 것 같습니다..
안드로이드에서는 메세지를 보여주고 사라지는 용도로 Toast를 빈번하게 사용하고 있습니다.
하지만, 아쉬운 점이 하나 있다면, duration을 설정할 수 없습니다.
그래서, 간단하게 duration을 변경하는 방법으로, Toast의 show()를 쓰레드로 원하는 시간 만큼 계속 보여주면 쉽게 해결할 수 있습니다.
간단하게, 안드로이드 Toast 클래스에서 제공하는 SHORT과 LONG의 기본 시간값입니다..
private static final int LONG_DELAY = 3500; // 3.5 seconds
private static final int SHORT_DELAY = 2000; // 2 seconds
아래는 CountDownTimer를 이용해서 총 4초동안, 1초씩 줄어들면서 Toast의 show() 메소드를 호출해서, 총 6초(Toast.LENGTH_SHORT이 duration 이니)동안 메세지를 보여주는 코드입니다..
final Toast toast = Toast.makeText(context(), getString(R.string.login_error),Toast.LENGTH_SHORT);
toast.show();
new CountDownTimer(4000, 1000) {
public void onTick(long millisUntilFinished) {toast.show();}
public void onFinish() {toast.show();}
}.start();
Facebook은 정말 플랫폼 회사라고 할 수 있는 다양한 SDK를 지원하고 있고, 그 중에 안드로이드에서 Facebook을 쉽게 연동할 수 있는 SDK가 있습니다.. 보통 SDK라고 하면, 전통적인 관념에서 보자면, 소트프웨어를 개발하가 위해서 지원하는 툴킷을 포함한 라이브러리(?)라고 정의할 수 있겠습니다..
그런데, Facebook SDK를 받아서 사용해 보면, 라이브러리는 아니지만, 그렇다고 해서 SDK라고 봐야 하는지도 약간은 의문인데.. 형태는 프레임웍의 모습을 띄고 있습니다.. 흠, 모습이 좀 애매해서, 그냥 SDK라고 봐야 하나 봅니다..
서두가 너무 길었네요.. ^^;;
제가 안드로이드에서 Facebook을 연동하면서 이슈가 될 만한 것중에 2가지를 골라봤습니다..
1. 인증창이 안뜨는 경우가 종종 발생한다..
제가 2.2와 2.3버전에서 테스트를 해 봤는데, 에뮬레이터는 잘 동작합니다..
하지만, 폰에 설치를 해보니, 되는 폰도 있고 아닌 것도 있습니다..
그래서, 인증창을 띄울때 기존의 예제에서 조금 더 추가해줘야 할 필요가 있습니다.
facebook.authorize(this, new String[] { "email", "publish_checkins" },
new DialogListener() {
@Override
public void onComplete(Bundle values) {}
@Override
public void onFacebookError(FacebookError error) {}
@Override
public void onError(DialogError e) {}
@Override
public void onCancel() {}
}
);
- 위의 코드로 인증창이 안뜨는 문제가 해결된 코드..
facebook.authorize(this, Facebook.FORCE_DIALOG_AUTH, new String[] { "email", "publish_checkins" },
new DialogListener() {
@Override
public void onComplete(Bundle values) {}
@Override
public void onFacebookError(FacebookError error) {}
@Override
public void onError(DialogError e) {}
@Override
public void onCancel() {}
}
);
2. Facebook accessToken의 캐싱타임..
- 요 문제는 기본적으로 accessToken의 캐싱 타임이 하루로 설정되어 있기 때문에 발생할 수 있습니다. https://github.com/facebook/facebook-android-sdk/blob/master/facebook/src/com/facebook/android/Facebook.java 소스의 대략 91번째 줄이 accessToken의 캐싱타임입니다..
코드를 살짝 보니, 두둥 final private long REFRESH_TOKEN_BARRIER = 24L * 60L * 60L * 1000L; <-- 수정도 보지도 마라라는 코드.. ^^;;
흠, 그래서 입맛에 맞게 accessToken의 캐싱 타임을 줄이려면, Facebook SDK의 REFRESH_TOKEN_BARRIER 값을 변경해야 합니다..
Android WebView가 제공하는 getUrl() 메쏘드는 WebView를 통해서 가져온 웹 페이지의 URL을 넘겨줍니다.
대체로 getUrl() 메쏘드는 redirect된 사이트의 url을 가져옵니다.. 하지만, 간혹 redirect 하는 페이지에서 현재 사이트이 url이 아닌, redirect 전의 url을 가져오는 경우가 있습니다..
그래서, 위와 같은 문제를 해결하기 위한 방안으로, android webkit의 WebViewClient를 상속받아서 WebView의 페이지 로더로 바인딩을 해서 처리하면 해결할 수 있습니다..
* WebClient를 상속받은 클래스
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class OAuthWebClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
@Override
public void onPageFinished(WebView view, String url) { // WebView의 페이지 로드가 완료되면 콜백의 형태로 이 메쏘드가 호출됩니다.. 좀 더 정확하게는 WebView가 이벤트 발생하는 경우 WebViewClient의 선언된 메쏘드들을 호출하고, 요 형태는 전형적인 옵저버 패턴의 모습입니다.
}
}
* WebView에서 위의 클래스를 사용하는 형태
- 아래 코드는 WebView에서 url이 바뀌는 것을 주기적으로 2초마다 모니터링하는 스켈레톤 코드의 형태이다.
webView.setWebViewClient(new OAuthWebClient());
Timer timer = new Timer();
TimerTask monitorThread = new TimerTask() {
@Override
public void run() {
if(webView == null || webView.getUrl() == null)
return;
// url이 현재 webView의 url이 아닐수 있기 때문에, 위에서 선언한 클래스의 onPageFinished() 메쏘드를 활용할 필요가 있다.
String url = webView.getUrl();
}
}
timer.schedule(monitorThread, 10* 1000, 2000); // 10초 후에, 매 2초마다 monitorThread를 실행
결론으로, WebView의 getUrl()메쏘드가 잘 동작하지만, 실제로 잘 못 가져오는 문제가 있기 때문에, 정확한 URL을 가져오기 위해서는 위의 WebViewClient를 상속한 OAuthWebClient같은 클래스를 만들어서, 좀 더 정확하게 의도한 바를 오버라이딩을 통해서 구현할 수 있을 것이다.
이클립스에서 문법이 전혀 틀리지 않았는데..
selector.xml이 layout에서 전혀 보이지 않는 문제가 종종 발생을 합니다..
몬가 살펴보니, Error Log를 보라고 해서, 살펴보니 아래와 같네요..
org.xmlpull.v1.XmlPullParserException: Binary XML file line #3: <item> tag requires a 'drawable' attribute or child tag defining a drawable
................
....
..
간단한 selector인데 흠.. drawable attribute도 있구요..
흠... 아무리 해도 안되네요.. ^^;;
그래서, 이클립스를 다시 재시작을 했습니다.. 잘됩니다..
젠장, 그래서 결론은 문제가 없어 보이는데, 에러가 발생하면, 재시작.. 재시작이 중요한 해결책이 될 수 있겠네요.. ^^
Dialog는 안드로이드에서 자주 사용되는 UI 컴퍼넌트이지요..
그런데, Dialog를 show()하고 종료하기 위해서, cancel(), dismiss()를 사용할 수 있는데요..
API 문서에서는 아래와 같이 설명을 하고 있습니다.. 제가 UI쪽은 거의 안해봐서 그런가? 동작하는게 똑 같아 보입니다..
아래 dismiss()메소드는 스크린에서 dialog를 제거하는데 쓰레드 세이프 하다네요..
Dismiss this dialog, removing it from the screen. This method can be invoked safely from any thread. Note that you should not override this method to do cleanup when the dialog is dismissed, instead implement that inonStop().
대충 예제 코드만 보면, 버튼의 클릭은 trackEvent, Activity를 로딩해서 데이터를 가져오는 형태는 trackPageView 메소드를 호출해서 통계자료로 사용하는 것으로 보인다.. 그리고, 코드는 일정시간이 지나면 Cron 잡처럼 디스패칭해서 데이터를 서버(Google Analytics)로 전송하는 형태라는 것을 알려주고 있다.
흠,, 그래도 도대체 어떻게 구현했는지가 궁금했다.. 그래서, 살짝 까본 결과는 아래와 같다.
* GoogleAnalyticsTracker는 static 인스턴스로 전형적인 싱클톤 패턴의 형태를 띠고 있다.
* 예제의 startSession 메소드를 호출하게 되면
** 이벤트를 저장할 데이터베이스를 생성한다
** 네트웍으로 처리할 디스패처를 생성한다..
** 디스패처의 콜백을 받을넘을 생성한다..
** 네트웍 연결관리하는 매니저를 생성하고..
** 안드로이드의 Handler를 하나 생성한다.. 이넘의 용도는 Timer의 schedule메소드와 비슷하게 일정시간 뒤에 등록한 Handler의 메소드를 실행한다..
* 예제의 trackEvent를 호출하면..
** 이벤트 객체를 만들어서, 위의 Handler객체의 postDelayed메소드를 사용해서 디스패처를 구동한다.
흠, 위 과정이 Google Analytics for Android가 서버에 통계데이터를 리포팅하는 러프한 과정이다.
그 중에서도 디스패처로 NetworkDispater 클래스(내부적으로 아파치 HTTP 라이브러리 사용)를 사용하고 있는데, 틀만 살펴보면 아래와 같다..
class NetworkDispatcher implements Dispatcher
{
private static final String GOOGLE_ANALYTICS_HOST_NAME = "www.google-analytics.com";
private static final int GOOGLE_ANALYTICS_HOST_PORT = 80;
private static final int MAX_GET_LENGTH = 2036; // 2048도 아닌 2036의 사이즈는 왜?? 흠.. 궁금하다..
private static final int MAX_POST_LENGTH = 8192;
private static final String USER_AGENT_TEMPLATE = "%s/%s (Linux; U; Android %s; %s-%s; %s Build/%s)";
private final String userAgent;
private static final int MAX_EVENTS_PER_PIPELINE = 30;
private static final int MAX_SEQUENTIAL_REQUESTS = 5;
private static final long MIN_RETRY_INTERVAL = 2L;
private final HttpHost googleAnalyticsHost;
private DispatcherThread dispatcherThread;
private boolean dryRun = false;
public NetworkDispatcher()
{
this("GoogleAnalytics", "1.4.2");
}
Android 기반의 앱을 개발하다 보면, 해상도 문제가 골치 아프다..
특히, 태블릿을 지원하기 시작하는 허니콤이나, 모바일과 태블릿을 같이 지원하는 아이스크림까지 나온 상황에서는 말이다..
물론, 해상도에 적합하게 xml을 선언해서 개발하라고 하는 구글의 가이드(Supporting Multiple Screens)도 있지만, 다양한 해상도를 지원하기 위해서 /res/blahblah/layout.xml을 만들라고 하는것은.. 해상도에 맞춰 앱의 퀄리티를 유지하는 방향으로는 좋겠지만, 그 만큼 개발비용(iOS에 비해)을 증가시킬게 분명하다.. 물론, 이렇게 해서 태블릿도 하나의 앱에서 지원하면 좋겠지만, 개발해 보니, 이렇게 분리해서 태블릿의 장점을 녹여낼 수 있을까?라는 질문에는 회의적이다..
그래서, 개인적으로 도달한 답은 모바일 앱과 태블릿 앱을 분리해서 개발하는 게 좋을 듯 하다..
그럼, 각각의 앱을 개발하면서 각자(모바일, 태블릿)의 해상도는 어떻게 지원할 것인가? 에 대한 대답은 layout_weight으로 UI를 구성하는 것이다..
아래의 예제에서 빨간색으로 구성한 것들이 <LinearLayout>에서 각 UI가 차지하는 %를 숫자로 표현한 것이고, weightSum은 없으면 알아서 배열을 하지만, 아래의 예에서는 입력을 해 놓았다..
그리고, horizontal이든지 vertical이든지 비율로 크기를 맞추기 위해서는, 가로를 맞추기 위해서는 layout_width, 세로로 맞추기 위해서는 layout_height을 '0dp'로 입력을 해 두면 된다..
3. 해시키 만들고 등록하기..
3.1 해시키 만들기 : 아래의 명령을 통해서 해시키를 만든다..
> keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64
3.2 만들어진 해시키를 Facebook에 등록한다.. Facebook Developer 페이지의 Step 5의 이미지를 보면 쉽게 확인할 수 있다.
안드로이드 2.3 부터인가? AsyncTask 기반의 쓰레드 프로그래밍을 해야 합니다.. 그러다가 자주는 아니지만 가끔 android.view.WindowLeaked 에러를 토하면서 앱이 죽을 때가 있는데, 이런 경우에는 별도 쓰레드(AsyncTask 같은)를 실행한 Activity가 onPuase나 onClose 상태가 되면서 finish되는 상태가 되고, 별도 쓰레드(UI 작업을 하는)가 아직 실행중인 상태에서 발생을 하게 됩니다.. 결국, 현재 Context를 AsyncTask에서 사용하고 있는데, Context가 사라저 버려서 발생을 합니다..
그래서, 간단한 해결책을 살펴보자..
Dialog류의 작업을 보통 AsyncTask로 많이 구동시키는데..
모바일 애플리케이션들은 대체로 Http보다는 보안 이슈로 Https를 사용합니다..
안드로이드의 Http/Https 래퍼는 Apache HTTP Client와 Java의 HttpUrlConnection(HttpsURLConnection) 2 가지가 있습니다. 공식 블로그에서는 진저브레드(Gingerbread) 이상에서는 HttpURLConnection을 사용하기를 권장하고 있습니다. 하지만, 상당히 많은 앱에서 Apache HTTP Client를 사용하고 있을 것으로 생각이 듭니다.. 저 같은 경우에는, 양 쪽을 같이 혼용해서 사용하고 있습니다..
SSLSessionCache 클래스는 API 문서를 보시면 알 수 있습니다..
2개의 옵션을 제공하는데, 세션캐시를 특정위치에 저장할 수 있거나, 기본 위치에 저장하는 정도.. ^^;;
아래의 코드는 HttpClient를 생성해주는 메쏘드입니다.
빨간색 부분이 SSL에 SSLSessionCache를 사용하게 해 주는 코드입니다.. 흠..
public static synchronized HttpClient createHttpClient(Context c, int cTimeout, int sTimeout) {
// Use a session cache for SSL sockets
SSLSessionCache sessionCache = c == null ? null : new SSLSessionCache(c);
Android에서 앱의 Activity Stack을 일정 숫자만큼 유지를 하는 클래스입니다..
이 클래스를 통해서 앱의 종료도 detect할 수 있습니다..
* ActivityManager클래스..
코드가 너무 간단해서 설명 생략..
import java.util.concurrent.LinkedBlockingQueue;
import android.app.Activity;
/**
* Activity Manager Class
* @author mcsong@gmail.com
*
*/
public class ActivityManager {
private static final int max = 13;
private static final int size =12;
private static LinkedBlockingQueue<Activity> queue = new LinkedBlockingQueue<Activity>(max);
private static final String CNAME = ActivityManager.class.getSimpleName();
public static void createActivity(Activity activity) {
if(activity == null)
return;
* 사용방법
- Activity 생성(onCreate 메소드에서)시에 createActivity()를 호출한다. 자연스럽게 size만큼 Activity Stack이 관리가 된다.
- 앱 종료 detect는 isRootActivity()로 확인을 하면 된다.. ture인 경우 각 Activity 클래스에서.. onBackPressed()를 Override해서 확인하면 된다.. 참고로, AbstractActivity 클래스를 만들어서 상속을 사용하면 코드가 깔끔해 지겠다..
- 앱을 종료하면서 clear()를 호출해서 Activity stack에 있는 Activity들을 다 날려주셔야 Activity 관리가 명확해 집니다.. ^^
* 안드로이드 activity의 동작형태는 android:launchMode="standard" 에서 잘 동작합니다.. ^^