안드로이드에서 쓰레드 사용기법에 대해서 잘 정리된 자료를 발견했습니다.


안드로이드에는 비동기 작업을 위한 여러 API 들이 제공되는데요, 각각의 차이점과 어떤 경우에 사용하면 좋은지에 대해서 깔끔하게 정리가 되어있네요.



Loader는 이 자료를 보면서 처음 알게되네요.


자료 출처 : http://www.slideshare.net/andersgoransson/efficient-android-threading  


Posted by 에코지오
,

지난 번에는 AndroidAnnotations(AA) 오픈소스 라이브러리의 @Click 어노테이션을 흉내내봤습니다. 이번에는 @Background@UiThread 어노테이션을 흉내내보죠.


먼저 @Background와 @UiThread 어노테이션 클래스를 작성합니다.


(1) Background.java

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;


/**

 * <pre>

 * 메소드를 백그라운드 쓰레드에서 실행해주는 어노테이션.

 *

 * ※ 메소드 제약사항

 * - 메소드 리턴타입은 void만 가능

 * - Exception을 던지는 것이 가능하지만 에러정책에 따라 메소드 caller한테는 throw되지 않을 수 있음

 *

 * ※ NOTE

 * - 이 어노테이션은 작업진행상태 스핀다이얼로그 표시, 작업취소 처리, 작업 콜백메소드 등의 기능을 지원하지 않는다.

 * </pre>

 */

@Documented

@Retention(RetentionPolicy.CLASS)

@Target(ElementType.METHOD)

public @interface Background {

}


(2) UiThread.java

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;


/**

 * <pre>

 * 메소드를 UI 쓰레드에서 실행해주는 어노테이션.

 *

 * ※ delay

 * - delay 파라미터 설정시 해당 밀리초 이후에 실행된다. 기본값은 0이다.

 *

 * ※ 메소드 제약사항

 * - 메소드 리턴타입은 void만 가능

 * - Exception을 던지는 것이 가능하지만 에러정책에 따라 메소드 caller한테는 throw되지 않을 수 있음

 * </pre>

 */

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface UiThread {

    long delay() default 0;

}


자 이제 위 어노테이션이 붙은 메소드를 백그라운드 쓰레드나 UI쓰레드에서 실행시키기 위해서 AOP(AsepctJ)의 힘을 빌려봅니다.


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;


/**

 * <pre>

 * {@link Background}, {@link UiThread} 어노테이션이 붙은 메소드가

 * 백그라운드 및 UI쓰레드에서 실행되도록 애스펙트로 처리한다.

 * </pre>

 */

@Aspect

public class BackgroundAnnotationAspect {


    private Handler uiHandler = new Handler(Looper.getMainLooper());


    private Executor threadExecutor = Executors.newCachedThreadPool();


    @Around("execution(@com.mycompany.async.Background * *.*(..))")

    public void proceedInBackground(final ProceedingJoinPoint pjp) {

        threadExecutor.execute(new Runnable() {

            @Override

            public void run() {

                try {

                    pjp.proceed();

                } catch (final Throwable e) {

                    // 에러정책에 따라 적절히 에러 처리

                }

            }

        });

    }


    @Around("@annotation(uithread) && execution(@com.mycompany.async.UiThread * *.*(..))")

    public void proceedInUiThread(final ProceedingJoinPoint pjp, UiThread uithread) {

        runInUiThread(new Runnable() {

            @Override

            public void run() {

                try {

                    pjp.proceed();

                } catch (Throwable e) {

                    // 에러정책에 따라 적절히 에러 처리

                }

            }

        }, uithread.delay());

    }



    /**

     * {@link Runnable}을 UI쓰레드에서 실행한다.

     *

     * @param run

     * @param delayMillis 지연시간. 밀리초

     */

    private void runInUiThread(Runnable run, long delayMillis) {

        if (delayMillis <= 0) {

            // 현재 쓰레드가 UI 쓰레드인 경우 그냥 run 실행.

            if (Looper.getMainLooper().getThread() == Thread.currentThread()) {

                run.run();

                return;

            }


            uiHandler.post(run);

            return;

        }


        uiHandler.postDelayed(run, delayMillis);

    }

}


다 됐습니다. 아래처럼 쓰면 됩니다. (꼭 액티비티 내의 메소드에 국한되지 않고 아무 클래스에서든 사용가능합니다)


public class TestAsyncActivity extends Activity {

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

        Button test = ....;
        test.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                doBackground();
            }
        });
    }

    @Background

    private void doBackground() {

        URL url = new URL("http://mycompany.com/xxx");

        URLConnection conn = url.openConnection();

        byte[] contents = FileCopyUtils.copyToByteArray(conn.getInputStream());

        doUiThread(new String(contents));

    }


    @UiThread

    private void doUiThread(String contents) {

        Toast.makeText(getApplicationContext(), contents, Toast.LENGTH_LONG).show();

    }


유용하게 쓰시길 바랍니다.



Posted by 에코지오
,

RoboGuice와 더불어 안드로이드 코딩을 굉장히 심플하게 만들어주는 AndroidAnnotations(AA) 오픈소스 라이브러리는 @Click 이라는 아주 유용한 어노테이션을 제공합니다. 

뷰의 ID가 설정된 어노테이션을 메소드에 선언해주면 설정된 뷰에 click 이벤트가 발생할 때 해당 메소드가 실행됩니다. 

기존에는  view.setOnClickListener(new OnClickListener() { ...}) 와 같은 OnClickListener 익명 클래스로 도배된 지저분한 코딩이 필요했었죠.


@Click(R.id.myButton)  

void myButtonClicked() { 

 ... ... // R.id.myButton을 누르면 이 메소드가 호출됩니다.

}  


@Click

void anotherButton(View clickedView) {

 ... ... // R.id.anotherButton 버튼을 누르면 이 메소드가 호출됩니다.

}


@Click({R.id.myButton1, R.id.myButton2})  

void myButtonsClicked() { 

 ... ... // R.id.myButton1 또는 R.id.myButton2를 누르면 이 메소드가 호출됩니다.

}  


AA를 사용하지 않는(사용하기 싫은?) 환경을 위해 비슷한 기능을 제공하는 우리만의 @Click을 만들어 보겠습니다. 

먼저 Click 어노테이션을 작성합니다. value는 클릭 이벤트가 발생하는 뷰의 id 값입니다.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Click {

    int[] value() default -1;

}


@Click 어노테이션을 선언하는 메소드는 아래의 제약사항을 준수해야 합니다(AA의 @Click도 마찬가지입니다).

- 리턴 타입은 void만 가능

- 파라미터는 없거나 View 타입의 1개만 가능

- Exception을 던지는 것이 가능하지만 메소드 caller한테는 throw되지 않음 (AA에서는 아예 checked Exception도 못던집니다).


그리고 공통 액티비티 클래스에서 android.app.Activity의 onContentChanged() 메소드를 오버라이딩합니다.


    /**

     * 어노테이션에 설정된 뷰에 대해 클릭 이벤트 발생시 해당 메소드가 실행되도록 리스너 등록.

     *

     */

    @Override

    protected void onContentChanged() {

        super.onContentChanged();

        processClick(this);

    }


    public static void processClick(final Activity activity) {

        Class clazz = activity.getClass();


        for (final Method m : clazz.getDeclaredMethods()) {

            Click click = m.getAnnotation(Click.class);//m.isAnnotationPresent(Click.class)

            if (click == null)

                continue;


            Class<?>[] params = m.getParameterTypes();

            final boolean hasViewParameter = (params.length > 0);


            int[] ids = click.value();

            for (int id : ids) {

                View view = activity.findViewById(id);

                if (view != null) {

                    view.setOnClickListener(new OnClickListener() {

                        public void onClick(View view) {

                            try {

                                if (hasViewParameter) {

                                    m.invoke(activity, view);

                                } else {

                                    m.invoke(activity);

                                }

                            } catch (IllegalAccessException e) {

                                throw new RuntimeException(e);

                            } catch (InvocationTargetException e) {

                                throw new RuntimeException(e);

                            }

                        }

                    });

                }

            }


        }

    }


다 됐습니다. 공통 액티비티를 상속받은 액티비티에서 AA의 그것과 동일한 기능을 수행하는 @Click 어노테이션을 사용할 수 있습니다. 간단하죠?


※ 단, AndroidAnnotations의 @Click과 차이점이 몇가지 있습니다.

- AA에서는 setContentView()에 @Click 처리코드가 작성되지만, 위에서는 onContentChanged()에서 @Click을 처리하도록 했습니다. 

- AA의 @Click은 뷰의 id가 주어지지 않은 경우 메소드 이름으로 뷰 id를 자동으로 찾지만 위 코드는 그러한 기능은 제공하지 않습니다.

- AA에서는 컴파일타임에 Java Annotation Processing에 의해 @Click 처리 코드가 생성되지만 위 코드는 런타임에 @Click이 붙은 메소드를 이벤트리스너를 등록합니다.



Posted by 에코지오
,

AndroidAnnotations는 어떤 객체에 대한 주입(injection)이 완료된 이후에 코드를 실행할 수 있는 @AfterInject 어노테이션을 제공합니다. 

그리고  GuiceyFruitMycilaGuice 같은 라이브러리도 인젝션에 대한 콜백을 받을 수 있는 JSR250의 @PreDestroy, @PostConstruct 어노테이션을 구현하여 제공합니다.

그러나 대표적인 안드로이드용 Injection 프레임워크인 RoboGuice는 객체가 생성되고 필드에 대한 주입이 모두 끝난 이후 지정된 메소드(ex. 초기화 메소드)를 실행하는 깔끔한 방법을 제공하지 않습니다. 


다행히 방법이 전혀 없는 것은 아닙니다. 2가지 방법이 존재합니다.


1. InjectionListener를 이용

유연한 방법은 아니지만 Guice의 InjectionListener를 구현하여 주입이후에 지정된 메소드를 실행하게 할 수 있습니다.

(아래 코드에서는 MyInitClass 객체에 대한 모든 주입이 끝나고 나서 init 메소드를 실행합니다)


bindListener(Matchers.subclassesOf(MyInitClass.class), new TypeListener() {

    @Override

    public <I> void hear(final TypeLiteral<I> typeLiteral, TypeEncounter<I> typeEncounter) {

        typeEncounter.register(new InjectionListener<I>() {

            @Override

            public void afterInjection(Object i) {

                MyInitClass m = (MyInitClass) i;

                m.init();

            }

        });

    }

});



2. @Inject를 이용

Guice는 필드에 대한 모든 주입을 끝내고 나면 @Inject가 붙은 메소드를 실행한다고 합니다. 


    @Inject

    protected void init() {

       ... ...

    }


이 방법은 약간의 트릭에 해당하지만 매우 간단하기 때문에 저는 이 방법을 사용합니다. 자세한 내용은 다음 링크를 참고하세요.


http://stackoverflow.com/questions/2093344/guice-call-init-method-after-instantinating-an-object



Posted by 에코지오
,

RoboGuice 사용시에 클래스 타입만으로는 주입받을 모듈을 구분하기 어려운 경우가 있습니다. 

예들들어 시간이 오래걸리는 작업을 처리하는 동안 사용자에게 진행중임을 알려주는 다이얼로그가 필요해서 아래처럼 Dialog를 주입받는 코드를 작성했습니다.


@Inject

protected Dialog progressDialog;


위 코드에서 우리가 원하는 것은 작업이 진행중임을 알려주는 Dialog를 주입시키는 것입니다. 그러나 다른 모듈에서는 다른 모양의 Dialog가 주입되어야 하는데 저 Dialog 타입만으로는 그것을 구분할 수 없습니다.


이런 상황에서는 추가적인 정보를 제공하는 어노테이션을 이용하면 됩니다. 위 예제의 경우 먼저 @TaskProgressDialog와 같은 어노테이션을 만들어 추가해줍니다.  


@Inject

@TaskProgressDialog

protected Dialog progressDialog;


그리고 Guice 모듈설정에서 AnnotatedBindingBuilder.annotatedWith() 메소드를 통해 해당 어노테이션이 붙은 놈에 적절한 모듈을 주입합니다.


protected void configure() {

  bind(Dialog.class).annotatedWith(TaskProgressDialog.class).toProvider(TaskProgrerssDialogProvider.class);

  ... ...

}


이렇게 하면 Dialog 타입이면서 @TaskProgressDialog 어노테이션이 붙어 있는 놈한테만 TaskProgressDialogProvider가 생성한 Dialog 객체가 주입될 것입니다.


* 참고로, Guice는 문자열을 통해 어노테이션을 구분할 수 있는 @Named 어노테이션을 제공합니다. 그러니까 필요시마다 TaskProgressDialog 같은 별도의 어노테이션 클래스를 만들 필요가 없다는 얘기입니다. 단, 문자열이 이용되므로 보조적인 수단으로만 사용을 권장합니다.


public class RealBillingService implements BillingService {

  @Inject

  public RealBillingService(@Named("Checkout") CreditCardProcessor processor,

      TransactionLog transactionLog) {

    ...

  }


Guice 모듈설정에서는 Names.named() 를 이용합니다.


bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class);






Posted by 에코지오
,

추상(abstract) 애스펙트 클래스를 상속받아 포인트컷 메소드나 어드바이스 메소드를 오버라이딩하는 경우에 무엇이 적용되는가에 대해서 정리합니다. (참고로 애스펙트는 abstract 애스펙트만 상속받을 수 있습니다)


포인트컷 메소드 오버라이딩


추상 애스펙트에 정의된 (non-abstract) 포인트컷 메소드를 하위 애스펙트에서 오버라이딩하는 경우


(1) 포인트컷을 재정의하지 않은 경우

=> 추상 애스펙트의 포인트컷이 적용됨


(2) 포인트컷을 재정의한 경우 

=> 하위 애스펙트의 포인트컷이 적용됨


결론 : 자식 애스펙트에서 부모 애스펙트에 정의된 포인트컷을 재정의할 수 있다.



어드바이스 메소드 오버라이딩


추상 애스펙트에 정의된 어드바이스 메소드를 하위 애스펙트에서 오버라이딩하는 경우


(1) 어드바이스를 재정의하지 않은 경우

=> 추상 애스펙트의 어드바이스가 적용되지만 동작(메소드)는 하위 애스펙트의 것이 적용됨


(2) 어드바이스도 재정의한 경우

=> 추상 애스펙트의 어드바이스도 적용되고 하위 애스펙트의 어드바이스도 적용됨. 동작(메소드)은 하위 애스펙트의 것이 적용됨


결론 : 자식 애스펙트에서는 부모 애스펙트에 정의된 어드바이스의 동작(메소드 내용)은 재정의할 수 있지만,

언제(before, after 등) 어드바이스를 적용할지는 수정할 수 없다(무조건 새로운 적용 위치가 추가됨).


Posted by 에코지오
,

포인트컷 재사용


공통 포인트컷만 모아놓은 애스펙트 작성를 작성합니다. 예를들어 MyPointcuts.

다른 애스펙트에서는 MyPointcuts.somePointcut() 처럼 static하게 액세스하여 참조합니다.


@Aspect

public class MyPointcuts {

 @Pointcut("within(my..On*Listener+) && execution(!private !static * on*(..) throws !Exception)")

 public void eventListenerMethods() {}

}


@Aspect

public class MyAspect {

    @Around("my.MyPointcuts.eventListenerMethods()")

    public Object handleListenerException(final ProceedingJoinPoint pjp) {

        try {

            return pjp.proceed();

        } catch (final Throwable e) {

            exceptionHandler.handle(e);

            return null;

        }

    }

}



어드바이스 재사용

abstract 포인트컷을 만들고 그 포인트컷을 사용하는 어드바이스를 작성합니다. 애스펙트를 abstract로 선언해야 합니다(여전히 @Aspect 어노테이션 필요함).


@Aspect

public abstract class MyAspect {

 @Pointcut("")

 protected abstract void toOverridePointcut(); 


 @After("toOverridePointcut()")

 public void myAdvice() {

    ... ...

 }

 ...

}


하위 애스펙트에서 abstract 포인트컷을 구현합니다.


@Aspect

public class YourAspect extends MyAspect {

 @Pointcut("within(android.app.Activity+) && execution(void onCreate())")

 protected void toOverridePointcut() {}


 ... ...

}


Posted by 에코지오
,

RoboGuice 사용환경에서 AspectJ의 aspect 클래스에 모듈을 injection하는 방법입니다.



방법1. static 방식으로 injection


(1) 애스펙트 클래스에서 static 멤버변수로 선언

주입할 모듈을 static 멤버변수로 선언합니다.


 @Inject protected static ContextScopedProvider<T> tProvider; // T는 @ContextSingleton이어야함

 @Inject protected static ExceptionHandler exceptionHandler;//Singleton


(2) Guice 모듈 설정에서 static injection 처리

configure() 메소드에서 requestStaticInjection 메소드를 통해 주입합니다.


requestStaticInjection(ExceptionHandlingAspect.class);


그러나 이 방법은 권장하지 않습니다. Guice에서도 static injection은 deprecated될 것이라고 합니다.

http://code.google.com/p/google-guice/wiki/AvoidStaticState



방법 2. aspect 객체를 구하여 injection


aspect는 AspectJ에 의해 인스턴스화되며 우리가 직접 생성할 수 없습니다. 대부분의 경우 aspect는 싱글턴 객체이며, AspectJ는 싱글턴 aspect 객체를 참조할 수 있는 Aspects.aspectOf() 메소드를 제공합니다.


(1) 주입할 모듈을 애스펙트 클래스에 보통의 멤버변수로 선언


 @Inject protected ExceptionHandler exceptionHandler;


(2) Guice 모듈 설정에서 requestInjection을 통해 injection 처리


requestInjection(org.aspectj.lang.Aspects.aspectOf(ExceptionHandlingAspect.class));


참고:

Posted by 에코지오
,

AspectJ 문서에는 아래와 같이 @within()과 @annotation()에 대해 설명합니다.



즉, Anno라는 어노테이션에 대해서(제가 제대로 해석했다면...)


- @within(Anno) : Anno 어노테이션을 갖는 타입(클래스) 안에 정의된 코드와 관련된 조인포인트

- @annotation(Anno) : 조인포인트 대상이 Anno 어노테이션을 갖는 조인포인트


http://whiteship.tistory.com/379 에서는 이렇게 설명합니다.


- @within(Type) : 선언된 타입에 @Type 어노테이션이 붙어있을 때 그 객체의 모든 execution Join point를 나타냅니다.

- @annotation(Type) : 실행되는 메소드에 @Type 어노테이션이 붙어있을 때 그 메소드의 execution Join point를 나타냅니다.


http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:fdl:aop:aspectj 에서는 이렇게 설명하네요.


- @within(Transactional) : 대상 객체의 선언 타입이 @Transactional 어노테이션을 갖는 모든 결합점

- @annotation(Transactional) : 실행 메소드가 @Transactional 어노테이션을 갖는 모든 결합점



그러니까 @within()은 어노테이션이 붙은 클래스에 적용되고, @annotation()은 어노테이션이 붙은 메소드에 적용된다고 심플하게 이해하고 사용하면 될듯합니다만, 정확히 하자면 @annotation()은 조인포인트가 클래스건 메소드건 따지지 않는 것으로 보입니다.


(1) @annotation(Anno) && within(my.aop.*)

=> my.aop 패키지의 클래스 중에서 Anno 어노테이션이 붙은 클래스


(2) @annotation(Anno) && within(my.aop.*) && execution(* test*(..))
=> my.aop 패키지의 클래스에 정의된 test* 메소드 중에서 Anno 어노테이션이 붙은 메소드

(3) @annotation(Anno) && execution(* test*(..)) 
=> test* 메소드 중에서 Anno 어노테이션이 붙은 메소드

(4) @annotation(Anno) && @within(Anno) && execution(* test*(..))
=> Anno 어노테이션이 붙은 클래스의 test* 메소드 중에서 Anno 어노테이션이 붙은 메소드


대충 감이 오네요.


참고로 @annotation(Anno) && execution(* test*(..)) 포인트컷은 이렇게도 표현할 수 있습니다.

  

execution(@Anno * test*(..))




Posted by 에코지오
,

AndroidAnnotations 라이브러리와 비교하여 RoboGuice의 단점으로 지적되고 있는 사항을 해결하기 위한 방안입니다.



런타임시에 리플렉션(reflection) 사용에 따른 성능하락

- 스마트폰 하드웨어 성능이 점점 강해지고 있으므로 앞으로 큰 문제는 안될 것임

  또한 Dalvik VM의 리플렉션 성능이 점점 좋아지고 있음

- 게임처럼 대부분의 기능이 스마트폰 내에서 처리되는 앱이라면 문제될 수 있음

  그러나 주로 네트워크를 통해 서버측에서 결과를 받아 단순히 표출하는 클라이언트 앱의 경우에는 

  응답시간의 대부분은 네트워크 소요시간이 차지하며, 폰 로컬의 리플렉션 처리가 차지하는 시간은 매우 작을 것으로 판단됨. 

  따라서 클라이언트 앱에서 RoboGuice의 런타임 리플렉션은 큰 문제가 아님.



앱 용량이 많이 커지는 문제

- RoboGuice를 사용하기 위해 필요한 guice-3.0.jar 파일과 roboguice-2.0.jar 파일의 크기를 합치면 거의 820KB임.

- 반면, androidannotations-2.5.jar 파일은 53KB에 불과함

- 그러나 실제로 apk의 용량이 820KB만큼 커지는 것은 아니며 ProGuard를 적절히 설정하면 apk 파일 크기가 생각보다 많이 늘어나지 않음



RoboActivity 상속에 따른 Activity 클래스의 유연성 부족

- AndroidAnnotations에는 Activity에 @EActivity 어노테이션을 선언만하면 되기 때문에 Activity가 다른 액티비티를 상속받는 것이 가능함

- 그러나 RoboGuice에서는 RoboActivity를 상속받아야 하므로 다른 액티비티를 상속받을 수 없음

- 따라서 AOP를 통해서 다른 액티비티를 상속받지 않고도 기능을 추가할 수 있는 방법을 적용하는 것을 고려



유틸리티성 어노테이션 부족

- AndroidAnnotations는 @ViewById 같은 injection용 어노테이션 외에 @Click, @Rest, @Background, @UiThread 등의 다양한 유틸리티성 어노테이션을 제공함

- 반면 RoboGuice는 코드를 줄여주는 이러한 유틸리티가 별로 없음

- AndroidAnnotations 소스를 참고하여 유틸리티성 어노테이션을 처리하는 AOP 애스펙트를 개발하여 적용하면 됨(별로 어렵지 않음)

- @Click : (공통) 액티비티의 setContentView() 메소드에 after 어드바이스로 처리

- @Background, @UiThread : 메소드에 around 어드바이스로 처리

- @Rest : ... 고민중...






Posted by 에코지오
,