Java8 의 등장으로 드디어 Java를 사용하는 어플리케이션에서, 함수형 프로그래밍을 할 수 있게 되었습니다. Lambda 를 사용할 수 있으면서, Collection 처리를 선언형으로 처리할 수 있다면 매우 매력적일 것이라 생각합니다.


그러나 아쉽게도 안드로이드에서는 안좋은 소식이 있습니다. 강력한 Stream API 를 24 버전 이 후부터 사용할 수 있습니다. 하위호환성이란 문제를 생각해야하는 입장에서 24버전을 minimum 으로 맞춰두고 개발하기란 쉽지 않을 것 같습니다.


Call requires API level 24 (current min is 15): java.util.Collection#stream


하지만 이를 커버할 수 있는 방법 중 하나는 안드로이드에서 반응형 프로그래밍을 할 수 있는 RxAndroid2(RxJava2 포함) 를 사용하는 방법이라 할 수 있을 것 같습니다. 


글 작성 기준 (2017.02.27) RxJava2 가 새로 등장하였으며, 이를 기준으로 Stream 과 호환할 수 있는 메소드 사용법에 대해 포스팅해보려 합니다. (간단한 예제는 생략합니다. ㅡㅡ^)


일단 gradle 로 다음 RxJava 라이브러리들을 다운 받습니다. 


RxAndroid2 : https://github.com/ReactiveX/RxAndroid


Rx-java-extension : https://github.com/jacek-marchwicki/rx-java-extensions



1. RxJava2 에서의 매칭


Collection 클래스 내에서 매칭을 하여 결과를 알 수 있다는 것은 꽤 유용한 방법이었습니다. 


해당 결과를 가진 데이터가 존재하는가? 혹은 모든 데이터가 해당 결과를 만족하는가? 등 우리는 Java8 에서 단순히 선언만으로 데이터를 처리할 수 있었습니다.



RxJava2 에서도 크게 다르지는 않습니다.


다음과 같이 any 와 all 을 사용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
List<Integer[]> testT = Arrays.asList(new Integer[]{25},new Integer[]{38}, new Integer[]{3,5});
            
// 일부 매칭.
Observable.fromIterable(testT).
        map(arrays -> Arrays.asList(arrays)).
        any(list -> !list.contains(8)).blockingGet();
 
// 전체 매칭
Observable.fromIterable(testT).
        map(arrays -> Arrays.asList(arrays)).
        all(list -> !list.contains(8)).blockingGet();
cs



2. flatMap 을 통한 컬렉션 join


컬렉션 내부의 제네릭객체를 join 할 수 있는 flapMap 에 대해서 소개한 적이 있습니다. 



RxJava2 에서도 방법은 크게 다르지 않습니다. 호환성있게 지원이 됩니다. 아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<MemberVo> memberVoList = Arrays.asList(
        new MemberVo(1"Doohyun Nam",28)
        , new MemberVo(2"Duckhyung Yuu"27));
 
List<PersonalInfoVo> personalInfoVoList = Arrays.asList(
        new PersonalInfoVo(1"My Way Functional Programming")
        , new PersonalInfoVo(2"My Way Spring FrameWork"));
 
Observable.fromIterable(memberVoList).
        flatMap(memberVo -> Observable.fromIterable(personalInfoVoList).
                                filter(personalInfoVo -> personalInfoVo.getMemberSubjectSn().equals(memberVo.getMemberSubjectSn())).map(personalInfoVo -> {
                            MemberPersonalVo memberPersonalVo = new MemberPersonalVo();
                            memberPersonalVo.setGoal(personalInfoVo.getGoal());
                            memberPersonalVo.setName(memberVo.getName());
                            return memberPersonalVo;
                        })).
        forEach(System.out::println); 
 
// Result
// Doohyun Nam : My Way Functional Programming
// Duckhyung Yuu : My Way Spring FrameWork
cs



3. Grouping 과 Partitioning 


컬렉션 데이터를 특정 조건으로 분류하고, 그 결과를 Map 으로 만들어주던 이 기능은 많은 비지니스 로직개선에 도움이 되었습니다. Collector 의 팩토리 메소드 몇개만 사용하면 쉽게 이용할 수 있었습니다.



그러나 RxJava2 에서는 이를 활용하기가 조금 까다롭습니다. Java8 의 Stream 처럼 collect 메소드를 지원하지만, 같이 사용가능한 Collectors 군의 팩토리 메소드들과 같은 유틸성 기능을 제공하지 않습니다.


대신에 toMap, toMultiMap, groupby 등의 메소드를 사용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
List<SubjectRelation> subjectRelationList = Arrays.asList(
                    new SubjectRelation(11001"Doohyun Nam"1)
                    , new SubjectRelation(11002"Dolkin"2)
                    , new SubjectRelation(11003"hshawng"1)
                    , new SubjectRelation(11004"spKwon"1)
                    , new SubjectRelation(21005"Other Person1"3)
                    , new SubjectRelation(21006"Other Person2"4)
            );
 
// create Map
Map<Integer, Collection<String>> mapTest = Observable.fromIterable(subjectRelationList).
           toMultimap(SubjectRelation::getCompanySubjectSn, SubjectRelation::getMemberName).
           blockingGet();
 
 
// only subscribe
Observable.fromIterable(subjectRelationList)
            .groupBy(SubjectRelation::getCompanySubjectSn).subscribe(group -> {
                  System.out.println(group.getKey());
                group.map(SubjectRelation::getMemberName).forEach(System.out::println);
            });
 
// create multi group
 Map<Integer, Map<Integer, Collection<String>>> doubleKeyMap = new HashMap<>();
            Observable.fromIterable(subjectRelationList).
                    groupBy(SubjectRelation::getCompanySubjectSn).
                    blockingSubscribe(group ->
                        doubleKeyMap.put(group.getKey(),
                                group.toMultimap(
                                        SubjectRelation::getOrganizationSubjectSn
                                        , SubjectRelation::getMemberName).blockingGet())
                    );
 
// partitioning
Map<Boolean, Collection<String>> partitioningMap = Observable.fromIterable(subjectRelationList).
             toMultimap(subjectRelation -> subjectRelation.getCompanySubjectSn().intValue() == 1
                 , SubjectRelation::getMemberName).
             blockingGet();
cs


세 번째 라인의 key 두 개를 이용하여 만든 맵의 경우 딱 맵까지 만들어주는 방법을 찾지는 못했습니다. 뭔가 우회해서 해보려고는 했으나, deadlock 현상을 보았습니다. ㅜㅡㅜ


Single 객체는 RxJava2 부터 더이상 null 을 허용하지 않겠다는 것으로 만든 객체로써, 사실 Single로 감싸게 된 맵이라도 실제 사용에는 큰 문제가 없어보입니다.


4. Math Library.


Java8 에서는 collect 나 reduce 를 이용하여 합계, 최소값, 최대값 등 통계 데이터를 구할 수 있었습니다.


RxJava2 에서는 extension 라이브러리에서 이 기능을 수행할 수 있도록 도와줍니다. 


1
2
3
4
5
// 합계
Observable.range(0100).to(MathObservable::sumInt).blockingSingle();
            
// 평균
Observable.range(0100).to(MathObservable::averageFloat).blockingSingle();
cs


5. Optional 대신 Maybe


Java8 에서는 더이상 null 을 허용하지 않겠다는 의도로 값의 존재여부를 표시할 수 있는 Optional 이란 개념이 생겼습니다. RxJava2 에서는 Maybe 를 통해서 이를 수행할 수 있습니다.


1
2
3
4
int number = 5;
 
Maybe<Integer> a = number% 5 == 0 ? Maybe.just(number) : Maybe.just(number);
a.subscribe(System.out::println);
cs


라이브러리나 플랫폼은 달라도 위에 있는 프로그래밍 패러다임의 계승은 비슷합니다.


아직 모든 기능을 알게 된 것은 아니지만, 안드로이드에서도 본격적으로 함수형 프로그래밍을 할 수 있을 것으로 보입니다.

반응형

'개발이야기 > 안드로이드' 카테고리의 다른 글

MVP 패턴을 통한 메모리 누수 관리  (0) 2016.11.24
WeakReference 테스트  (2) 2016.11.15
액티비티 개수 구하기  (0) 2016.11.15
안드로이드 DOZE 모드 만들기  (0) 2016.11.15
Posted by N'

현재 개발하고 있는 앱의 메모리 누수가 감지되고 있습니다. ㅡㅡ^ 


메모리 누수 감지는 


https://github.com/square/leakcanary


를 통해서 프로파일링을 하고 있고, 대부분 문제가 스레드가 활동하는 동안 Context 가 살아남아서 생기는 문제가 있습니다.


뭐 자바의 패러다임으로 볼 때, 메모리는 VM 이 관리해주기 때문에 딱히 개발자가 건들 필요는 없어보이지만, 안드로이드 메모리가 제한적이고 각 객체가 더이상 쓰이지 않을 때 바로바로 해제를 해주어야 한다는 측면에서 적절한 작업이 이뤄줘야하는 것으로 보입니다. 특히 액티비티 인스턴스는 용량이 크기 때문에 잘 해제가 되야 합니다. ㅡㅡ^, 


해당 문제를 해결하기 위해, 아래 포스트처럼 WeakReference 를 통해 해결을 하려 하고 있으며, 이를 적용하기 위한 일괄된 패턴이 필요했습니다.



그래서 이번에 시도해보고 있는 것은 

잔디 블로그 (http://tosslab.github.io/android/2015/03/01/01.Android-mvc-mvvm-mvp.html

에서 소개한 MVP 패턴 사례를 참고하여, 메모리 누수를 막아보려고 하고 있습니다.


제가 하려고 했던 아래 코드입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SplashActivity extends Activity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
 
        Handler hd = new Handler();
        hd.postDelayed(() -> {
            // 심플하지만, 이 곳 핸들러의 역할 때문에 메모리 누수가 존재
            setResult(RESULT_OK);
            finish();
 
            overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
        }, 1000);
    }
}
cs



핸들러 내부에서 Context 가 살아남아 있으며, 실제 프로파일러에서는 Handler 에서 메모리 릭이 감지되었다고 나오고 있습니다.


이러한 이유로, 해야할 일을 나누어 봅시다.


WeakReference 관리는 Presenter 가 담당합니다. 아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SplashPresenter {
    private Handler hd;
    private WeakReference<SplashActivity> splashActivityWeakReference;
 
    public SplashPresenter(SplashActivity splashActivity) {
        splashActivityWeakReference = new WeakReference<>(splashActivity);
        hd = new Handler();
    }
 
    /**
     * 1초 후에 해당 액티비트를 종료한다.
     */
    public void delayFinished() {
        hd.postDelayed(() -> {
            SplashActivity splashActivity = splashActivityWeakReference.get();
 
            if (splashActivity != null) {
                splashActivity.smoothFinishActivity();
            }
 
        }, 1000);
    }
}
cs


잔디 블로그의 경우에는 Presenter 를 인터페이스화 시킨 후 구현화하였지만,


- 저의 경우는 Activity 와 Presenter 가 1:1 관계이고, 

- Activity 에서 View 관련 업데이트를 해야할 종류가 많을 경우 interface 에 그만큼 메소드를 정의해야하기 때문에 불편하다는 생각이 있었으므로, 


Activity 와 Presenter 를 단순 has-a 관계로 처리하고자 하였습니다.


그리하여, Activity 구현 부는 다음과 같이 View 만 업데이트하는 구조를 취하게 되었습니다. Activity 자체가 WeakReference 관리를 할 필요는 없는것이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SplashActivity extends Activity {
    private SplashPresenter splashPresenter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
 
        splashPresenter = new SplashPresenter(this);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        splashPresenter.delayFinished();
    }
 
    /**
     * Activity 종료
     * <p>
     * <pre>
     *     Activity 를 페이드아웃 애니메이션을 사용하며 종료.
     * </pre>
     */
    public void smoothFinishActivity() {
        setResult(RESULT_OK);
        finish();
        overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
    }
}
cs


다음과 같은 구조를 통해, View 를 담당하는 Activity 클래스는 WeakReference 를 고려할 필요가 없게 되었으며, Activity instance 는 GC Time에 메모리 회수가 가능하게 되었습니다.



반응형
Posted by N'

Rx의 도입과 액티비티를 옵저버로 관리하게 되니, 메모리 누수라는 골치아픈게 발생하고 있습니다.


WeakReference 클래스를 사용하면 된다고 나오지만, 정확히 이해가 가지 않아, 테스트 코드를 작성해보았습니다.


일단, WeakReference 를 사용하지 않을 경우.

- 확인해보면 GC 가 강제 실행되었지만, A 내부 B 객체가 살아있음을 알 수 있습니다. (컬렉션이 물고 있기 때문이죠.)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test {
 
    public static class A {
        private B b = new B();
 
        public B getB() {
            return b;
        }
 
        public void setB(B b) {
            this.b = b;
        }
 
        public static final class B {
            public String text = "아직 살아있다.";
        }
    }
 
    public static void main(String[] args) {
        // TODO Auto-generated method stu
        A a = new A();
        HashSet<A.B> abc = new HashSet<>();
 
        abc.add(a.getB());
        a = null;
        System.gc(); // GC 강제 발생
 
        for (A.B b : abc) {
            System.out.println(b.text);
        }
    }
}
 
// 아직 살아있다.
 
cs


WeakReference 로 참조를 유지할 경우.

- GC 가 돌며, A 의 인스턴스가 삭제되었고, 참조하고 있던 B의 인스턴스도 삭제가 되었음을 알 수 있습니다. (안드로이드에서도 이 힌트를 가지고, 메모리 릭을 방지할 수 있을 것이라 생각합니다. ㅡㅡ^)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test {
 
    public static class A {
        private B b = new B();
 
        public B getB() {
            return b;
        }
 
        public void setB(B b) {
            this.b = b;
        }
 
        public static final class B {
            public String text = "아직 살아있다.";
        }
    }
 
    public static void main(String[] args) {
        // TODO Auto-generated method stu
        A a = new A();
        HashSet<WeakReference<A.B>> abc = new HashSet<>();
 
        abc.add(new WeakReference<>(a.getB()));
 
        a = null;
        System.gc();
 
        for (WeakReference<A.B> b : abc) {
            System.out.println(b.get().text);
        }
    }
}
 
// Exception in thread "main" java.lang.NullPointerException
    at Test.main(Test.java:31)
 

cs


반응형
Posted by N'

안드로이드에서 메모리 릭이 발생하고 있는지 아닌지를 다음 코드를 통해 확인합시다.


기종 따라 다르지만, instanceCount 가 무한대로 증가한다면, 문제가 있는 겁니다. ㅡㅡ^


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestInstanceActivity extends Activity {  
           public static int instanceCount = 0;
           public TestInstanceActivity () {
                     super();
                     instanceCount++;
                     Log.d("test""TestInstanceActivity() instanceCount: " + instanceCount);
           }
 
 
           @Override
           protected void finalize() throws Throwable {
                     super.finalize();
                     instanceCount--;
                     Log.d("test""finalize() instanceCount: " + instanceCount);
           }
}
cs


반응형
Posted by N'

안드로이드 N이 등장하면서, 클라이언트의 배터리를 아껴주기 위하여, DOZE 모드가 등장하였습니다.


이 때, 모든 네트워크 작업이 중단 되기 때문에 반드시 이에 대한 처리를 해주어야합니다.

특히 푸시 서비스를 제공하는 앱을 개발하기 위해서는 FCM(이전 GCM 등)을 사용하여, 앱을 깨우는 작업이 필요합니다. (FCM은 유일하게 DOZE 모드 중, 네트워크 수신이 가능합니다.)


그러하기 때문에 테스트를 위해서 DOZE 모드 상태를 강제적으로 만들필요가 있는데, 아래와 같은 명령어를 ADB에서 사용하면 됩니다. (STEP 이 IDLE 상태로 변할 때까지 명령어를 난타합시다. ㅡㅡ^)



- adb shell dumpsys battery unplug (배터리 플러그 빼기)

- adb shell dumpsys deviceidle step (idle 모드 만들기)




반응형
Posted by N'