최신 안드로이드 버전 (P OS이후?) 부터는 List view보다는 Recycler View를 사용하는게

구글에서도 권고되는 사항인데요.

 

Recycler view를 다루는게 매번 헷갈리고 어렵고 힘든것같아서 이참에 뼈대코드도 작성해보고

포스팅하기로 마음먹었습니다.

 

 

바쁘신분들은 아래 링크를 눌러서 소스코드를 바로확인해보시기 바랍니다.

https://github.com/control-man/android-simple-sample/tree/master/recycler_view_simple_exam

 

 

Recycler view 구현 예제 (Cow를 눌러서 Toast 가 출력되고 있음)

 

1. res/layout/activitiy_main.xml 작성

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
        android:id="@+id/rvAnimals"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter

Recycler view를 생성해줍니다.

 

2. res/layout/recycler_row.xml 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="10dp"
    android:background="?android:attr/selectableItemBackground">
 
    <TextView
        android:id="@+id/tvAnimalName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20dp"/>
</LinearLayout>
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter

Recycler view에서 각각의 item에 대한 view를 만들어줍니다.

여기선 LinearLayout안에 Textview한개를 두는 방식으로 배치할 것입니다.

 

 

3. MainActivity.java

MainActivity안에서 inner class인  MyRecyclerViewAdapter 를 정의합니다.

 

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
 
        private List<String> mData;
        private LayoutInflater mInflater;
        private ItemClickListener mClickListener;
 
        // data is passed into the constructor
        MyRecyclerViewAdapter(Context context, List<String> data) { //// todo check
            this.mInflater = LayoutInflater.from(context);
            this.mData = data;
        }
 
        // inflates the row layout from xml when needed /////todo check
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = mInflater.inflate(R.layout.recycler_row, parent, false);
            return new ViewHolder(view);
        }
 
        // binds the data to the TextView in each row
        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            String animal = mData.get(position);
            holder.myTextView.setText(animal);
        }
 
        // total number of rows
        @Override
        public int getItemCount() {
            return mData.size();
        }
 
 
        // stores and recycles views as they are scrolled off screen
        public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
            TextView myTextView;
 
            ViewHolder(View itemView) {
                //todo check
                super(itemView);
                myTextView = itemView.findViewById(R.id.tvAnimalName);
                //todo very important ---- if don't comment it's not ripple effect..........!!!!!!!!!!!!!!!!!!!
                 itemView.setOnClickListener(this);
            }
 
            @Override
            public void onClick(View view) {
                if (mClickListener != null) mClickListener.onItemClick(view, getAdapterPosition());
            }
        }
 
        // convenience method for getting data at click position
        String getItem(int id) {
            return mData.get(id);
        }
 
        // allows clicks events to be caught
        void setClickListener(ItemClickListener itemClickListener) {
            this.mClickListener = itemClickListener;
        }
 
        // parent activity will implement this method to respond to click events
        public interface ItemClickListener {
            void onItemClick(View view, int position);
        }
    }
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter

어댑터의 멤버필드로는 recycler view의 item을 표시하기 위해 필요한 mData가 있으며,

또한 recycler view의 해당하는 position을 클릭하였을때 응답하기위한 리스너 interface 를 들고있습니다.

 

Adapter내부로 ViewHolder 클래스와 ItemClickListener를 가지고 있습니다.

ViewHolder클래스는 recycler view의 item들을 구성하는 resource를 갖고오기 위해 사용되는 클래스이고

 

 

실제적으로 사용자눈에 보여지기위해 그려지는 부분은 Adpater 클래스의 onBindViewHolder 에서 완성되게 됩니다.

 

 

4.MainActivity.java

아래는 MainActiviity onCreate에서 recycler view를 그려주기위한 생성과정입니다.

Recylcer view를 구성하기위한 item 객체를 준비해야하며,

recycler view의 resource를 찾아와서 layout 설정을 해주고,

adpater를 생성하여 item 객체를 주입시키고, click listener 역시 연결해줍니다.

마지막으로 reycler view에 adpater를 연결 시켜주게되면 recycler view가 출력됨을 확인하실 수 있습니다.

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

        // data to populate the RecyclerView with
        ArrayList<String> animalNames = new ArrayList<>();
        animalNames.add("Horse");
        animalNames.add("Cow");
        animalNames.add("Camel");
        animalNames.add("Sheep");
        animalNames.add("Goat");

        // set up the RecyclerView
        RecyclerView recyclerView = findViewById(R.id.rvAnimals);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        //todo common context가 아닌 this?
        adapter = new MyRecyclerViewAdapter(this, animalNames);
        adapter.setClickListener(new MyRecyclerViewAdapter.ItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Toast.makeText(getApplicationContext(), "You clicked " + adapter.getItem(position) + " on row number " + position, Toast.LENGTH_SHORT).show();
            }
        });
        recyclerView.setAdapter(adapter);

    }

 

 

안드로이드 앱을 개발하시다보면 앱이 업데이트되어서,

최신버전을 받을수있도록 앱스토어로 사용자가 가게끔 해야하는 경우가 있습니다.

 

어떤 아키텍쳐로 가져갈까 고민을 해보면 결국 서버가 필요하다는것을 모두 느끼실겁니다.

하지만 저같은 가난한 개발자의 경우는 서버 운영비용도 없기에 어떻게 해야할까

고민을 했는데, 그해답을 Google이 제공하는 Firebase를 이용하면 가능할 수 있음을 

알게 되었습니다.

 

구글 Firebase

Remote config를 사용하시려면 사전에 Firebase세팅이 끝나셔야 합니다.

 

1. Remote Config 파라메터 설정

https://console.firebase.google.com/project/lottomagic-b574d/config

위의 주소로 들어가신다음 앱에서 요청하여 받을 remote config를 추가하거나 수정 및 삭제 하실 수 있습니다.

로또매직 앱의 latest version을 앱에 알려주기 위해 아래와 같이 셋팅했습니다.

 

Remote config

2. app/src/main/res/xml/remote_config_defaults.xml

위의 경로의 xml을 추가해주고 아래와 같은방식으로 작성합니다.

작성하는 이유는, remote config를 읽어오는것을 실패하게되면 기본값으로 받아올 수 있는값을

설정하기 위함입니다.

<?xml version="1.0" encoding="utf-8"?>
<!-- START xml_defaults -->
<defaultsMap>
  <entry>
    <key>latest_version_code</key> 
    <value>0</value>
  </entry>
</defaultsMap>

 

 

3. app/build.gradle

gradle설정을 아래와 같이합니다.

...

implementation 'com.google.firebase:firebase-config:18.0.0'

....

 

4. app/src/main/java/com/jinlab/android/lottomagic/remote/config/RemoteConfigManager.java

    public void init() {
     mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
     if (BuildConfig.DEBUG) {
      L.d(TAG, "debug config");
      mFirebaseRemoteConfigSettings = new FirebaseRemoteConfigSettings.Builder()
       .setMinimumFetchIntervalInSeconds(60 * 10) // 10 mins
       .build();
     } else {
      // Release
      mFirebaseRemoteConfigSettings = new FirebaseRemoteConfigSettings.Builder()
       .setMinimumFetchIntervalInSeconds(ONE_DAY_SECONDS)
       .build();
     }
     mFirebaseRemoteConfig.setConfigSettingsAsync(mFirebaseRemoteConfigSettings);
     mFirebaseRemoteConfig.setDefaultsAsync(R.xml.remote_config_defaults);
    }

위의 매니져 클래스에 init부분을 정의하고 MainActivity가 열릴때 init메서드를 호출하게끔 구현하였습니다.

init메서드에서는, remote config를 셋팅하는데, 디버그버전일때는 10분에 한번씩 firebase 서버에서 fetch할 수 있도록하였고, 실제 릴리즈버전에서는 하루가 지나야 fetch하도록 하였습니다.

 

구글에서는 실시간으로 앱에서 정보를 받아오는 목적이라면 remote config가 아닌 실시간 데이터베이스를 사용하라고 권장하고있습니다.

 

로또매직의 앱업데이트같은경우는 매번 쿼리를 날리는게 아니라 하루에 한번정도면 족하다라고 생각하여 remote config로 구현하였습니다.

 

 

 

5. app/src/main/java/com/jinlab/android/lottomagic/remote/config/RemoteConfigManager.java

 

MainActivity에서 remote config를 fetch하고 사용할 수 있도록 다음과 같은 wrapper 메서드를 생성하였습니다.

    public void fetchAndActivate(Activity activity, OnCompleteListener < Boolean > onCompleteListener) {
        mFirebaseRemoteConfig.fetchAndActivate().addOnCompleteListener(activity, onCompleteListener);
    }

 

 

 

6. app/src/main/java/com/jinlab/android/lottomagic/main/ui/MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        // Remote config
        RemoteConfigManager.getInstance().init();
        RemoteConfigManager.getInstance().fetchAndActivate(thisnew OnCompleteListener<Boolean>(){
            @Override
            public void onComplete(@NonNull Task<Boolean> task) {
                if (task.isSuccessful()) {
                    boolean updated = task.getResult();
                    L.d(TAG, "Config params updated: " + updated);
                    try {
                        mLatestVersionCode = Integer.parseInt(RemoteConfigManager.getInstance().getString(LATEST_VERSION_KEY));
                        if (mLatestVersionCode > BuildConfig.VERSION_CODE) {
                            showUpdatePopup();
                        }
                    } catch (NumberFormatException e) {
                        L.e(TAG, "NumberFormatException for mLatestVersionCode");
                        mLatestVersionCode = 0;
                    }
                } else {
                    L.e(TAG, "Fail to load remote config");
                }
            }
        });
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

MainActivity에서는 remote config fetch가 성공하고 active가 되는시점의 리스너를 등록하였습니다.

현재버전과 firebase로부터 받은 latest version을 비교하여, Firebase에 올린 latest version의 정보가 더높은경우 

update popup이 뜨도록 구현하였습니다.

 

 

7.  app/src/main/java/com/jinlab/android/lottomagic/remote/config/RemoteConfigManager.java

    public String getString(String key) {
        return mFirebaseRemoteConfig.getString(key);
    }

fetch가 성공하면 Firebase에 설정해놓은 파라메터의 value를 받아올 수 있습니다.

 

 

 

Fetch의 시간이 10분이라면, 10분뒤에 remote config를 원격의 firebase에서 받아오게 됩니다.

만약 10분전에 운영자가 Firebase remote config를 추가하거나 update하고나서,

곧바로 앱에서 fetch를 요청하게되면 반영된값을 받아오지못하고 이전에 firebase에 반영되어있는 값을 리스너에서 받을수있습니다.

즉 10분이 지나서야 요청하게되면 새로 생신된 값을 받아오게 됩니다.

 

 

성공적인 구현 기원합니다.

 

 

Remote config로 App update logic을 추가한 로또매직 구경하러 가기

 

 

*참고

RemoteConfig 구글 가이드
https://firebase.google.com/docs/remote-config/use-config-android

 

Android에서 Firebase 원격 구성 시작하기

Firebase 원격 구성으로 클라우드에서 앱의 매개변수를 정의하고 값을 업데이트하면 앱 업데이트를 배포하지 않고도 앱의 모양과 동작을 수정할 수 있습니다. 이 가이드에서는 시작하는 단계를 안내하고 샘플 코드를 제공합니다. 샘플 코드는 firebase/quickstart-android GitHub 저장소에서 클론하거나 다운로드할 수 있습니다. 앱에 Firebase 추가 Android 프로젝트에 Firebase를 추가하지 않았다면 먼저 추가합니다. Andr

firebase.google.com

 

개인마다 한주한주를 버티는 나름의 희망이 있을거라 생각합니다.

개개인 마다 추구하는 가치는 다를바라 생각하지만 

우리나라에서 로또는 직장인들은 한번은 당첨을 꿈꾸는 아이템이라 생각합니다.

 

 

로또를 하시면서 물론 자동으로 하시는분들도 대부분이겠지만,

나름의 규칙을 가지고 하시는분들도 꽤 있으신데요.

로또매직 받으러 가기

 

 

심플하면서도 쉽게 로또번호를 저장하고 분석할 수 있는 로또매직이라는 앱을 소개합니다.

 

로또매직에 저장된 내번호들

로또매직 다운받으러 가기

 

jinlab에서 제작하였으며, 저장된 번호의 홀:짝, 고:저, 연번, 총합등을 분석해주고 있습니다.

 

로또매직의 랜덤뽑기 기능

로또매직 다운받으러 가기

 

로또매직은 랜덤뽑기기능도있으며, 사용자가 직접 커스텀한 번호를 저장할 수 있는 기능도있습니다.

 

또한, QR스캐너도 있어서 로또용지만있다면 바로 당첨확인도 할 수 있습니다.

 

추후에는 로또데이터를 분석해서 사용자들에게 제공할 비전을 갖고 있습니다.

 

이번주에 로또매직으로 로또한번 해보시는거 어떤가요?

'개인벙보처리방침 > 로또매직' 카테고리의 다른 글

로또매직 개인정보처리방침  (0) 2020.02.16

1. 개인정보의 처리 목적 (‘com.jinlab.android.lottomagic’이하 ‘로또매직’) 은(는) 다음의 목적을 위하여 개인정보를 처리하고 있으며, 다음의 목적 이외의 용도로는 이용하지 않습니다.
- 고객 가입의사 확인, 고객에 대한 서비스 제공에 따른 본인 식별.인증, 회원자격 유지.관리, 물품 또는 서비스 공급에 따른 금액 결제, 물품 또는 서비스의 공급.배송 등


2. 개인정보의 처리 및 보유 기간

 (‘com.jinlab.android.lottomagic’이하 ‘로또매직’) 은(는) 정보주체로부터 개인정보를 수집할 때 동의 받은 개인정보 보유․이용기간 또는 법령에 따른 개인정보 보유․이용기간 내에서 개인정보를 처리․보유합니다.

② 구체적인 개인정보 처리 및 보유 기간은 다음과 같습니다.
☞ 아래 예시를 참고하여 개인정보 처리업무와 개인정보 처리업무에 대한 보유기간 및 관련 법령, 근거 등을 기재합니다.
(예시)- 고객 가입 및 관리 : 서비스 이용계약 또는 회원가입 해지시까지, 다만 채권․채무관계 잔존시에는 해당 채권․채무관계 정산시까지
- 전자상거래에서의 계약․청약철회, 대금결제, 재화 등 공급기록 : 5년

3. 정보주체와 법정대리인의 권리·의무 및 그 행사방법 이용자는 개인정보주체로써 다음과 같은 권리를 행사할 수 있습니다.

① 정보주체는 jinlab(‘com.jinlab.android.lottomagic’이하 ‘로또매직) 에 대해 언제든지 다음 각 호의 개인정보 보호 관련 권리를 행사할 수 있습니다.
1. 개인정보 열람요구
2. 오류 등이 있을 경우 정정 요구
3. 삭제요구
4. 처리정지 요구



4. 처리하는 개인정보의 항목 작성

 ('com.jinlab.android.lottomagic'이하 '로또매직')은(는) 다음의 개인정보 항목을 처리하고 있습니다.



5. 개인정보의 파기('로또매직')은(는) 원칙적으로 개인정보 처리목적이 달성된 경우에는 지체없이 해당 개인정보를 파기합니다. 파기의 절차, 기한 및 방법은 다음과 같습니다.

-파기절차
이용자가 입력한 정보는 목적 달성 후 별도의 DB에 옮겨져(종이의 경우 별도의 서류) 내부 방침 및 기타 관련 법령에 따라 일정기간 저장된 후 혹은 즉시 파기됩니다. 이 때, DB로 옮겨진 개인정보는 법률에 의한 경우가 아니고서는 다른 목적으로 이용되지 않습니다.

-파기기한
이용자의 개인정보는 개인정보의 보유기간이 경과된 경우에는 보유기간의 종료일로부터 5일 이내에, 개인정보의 처리 목적 달성, 해당 서비스의 폐지, 사업의 종료 등 그 개인정보가 불필요하게 되었을 때에는 개인정보의 처리가 불필요한 것으로 인정되는 날로부터 5일 이내에 그 개인정보를 파기합니다.



6. 개인정보 자동 수집 장치의 설치•운영 및 거부에 관한 사항

jinlab 은 정보주체의 이용정보를 저장하고 수시로 불러오는 ‘쿠키’를 사용하지 않습니다.

7. 개인정보 보호책임자 작성


① jinlab(‘com.jinlab.android.lottomagic’이하 ‘로또매직) 은(는) 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.


▶ 개인정보 보호책임자
성명 :jinlab
직책 :개인
직급 :개인
연락처 :jinss2174@gmail.com,
※ 개인정보 보호 담당부서로 연결됩니다.

▶ 개인정보 보호 담당부서
② 정보주체께서는 jinlab(‘com.jinlab.android.lottomagic’이하 ‘로또매직) 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. jinlab(‘com.jinlab.android.lottomagic’이하 ‘로또매직) 은(는) 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.



8. 개인정보 처리방침 변경

①이 개인정보처리방침은 시행일로부터 적용되며, 법령 및 방침에 따른 변경내용의 추가, 삭제 및 정정이 있는 경우에는 변경사항의 시행 7일 전부터 공지사항을 통하여 고지할 것입니다.



9. 개인정보의 안전성 확보 조치 ('로또매직')은(는) 개인정보보호법 제29조에 따라 다음과 같이 안전성 확보에 필요한 기술적/관리적 및 물리적 조치를 하고 있습니다.

1. 내부관리계획의 수립 및 시행
개인정보의 안전한 처리를 위하여 내부관리계획을 수립하고 시행하고 있습니다.

2. 문서보안을 위한 잠금장치 사용
개인정보가 포함된 서류, 보조저장매체 등을 잠금장치가 있는 안전한 장소에 보관하고 있습니다.

3. 비인가자에 대한 출입 통제
개인정보를 보관하고 있는 물리적 보관 장소를 별도로 두고 이에 대해 출입통제 절차를 수립, 운영하고 있습니다.

'개인벙보처리방침 > 로또매직' 카테고리의 다른 글

로또앱 소개 Feat.로또매직  (0) 2020.02.23

역시나 아래 참고 싸이트를 번역한수준밖에는 안됩니다 ^^;;

 

보통 타이머를 사용하면

MainActivity에서, onCreate 부분에서 startTimer()를 호출하게 됩니다.

그러면 타이머 클래스는, 주기를 가지고 일정주기동안 계속해서 실행하게 됩니다.

 

Activity가 destroy가 호출되었지만 Activity내에서 타이머가 살아있게되면, 결국 Activity는 소멸되지못하고

Leak 이발생하게 됩니다.

 

어떻게 Memory Leak 을 막을 수 있을까요?

 

 

1. 액티비티가 종료되는 onDestory() 에서 cancerTimer() 와 같은, 타이머 종료호출 메서드를 call하여, 타이머를 종료시키고 그로인해, Activity의 reference가 잡히는것을 해지할 수 있습니다.

 

 

 

 

 

참고

https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e

 

9 ways to avoid memory leaks in Android

I have been an android developer for quite some time now. And I realised that most of that time, I tend to spend on adding new features to…

android.jlelse.eu

 

아래 참고싸이트를 번역한 수준밖에안됩니다 ^^;;;

 

 

Thread 인스턴스를 생성해서 운영할때도 문제가 생길 수 있습니다.

Thread 가 실행되는 시점에서는 GC가 Thread를 수거해가지 않습니다. 또한, Thread가 참조하고있는

Outer class등의 reference도 잡고있어서, Activity내의 Thread를 운영할때는 주의를 기울여야 합니다.

 

 

흔히들 하는 실수

 

1. 액비비티 내에서 Thread의 참조변수를 static 변수로 두는 행위

 

2. custom Inner Thread class를 정의할때 non-static 으로 정의하는경우

 

 

 

해결책

 

1. Thread 인스턴스를 static변수로 저장하지 말아라.

 

 

2. custom static innter thread class를 정의하고 운영해라. 

단 Activity가 종료될때 (onDestroy() ), Thread의 종료를 끝낼 수 있는 로직을 추가해서 깔끔하게 종료되도록 해라.

 

 

 

 

 

 

참고

https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e

 

9 ways to avoid memory leaks in Android

I have been an android developer for quite some time now. And I realised that most of that time, I tend to spend on adding new features to…

android.jlelse.eu

 

 

아래의 참고싸이트를 번역한 수준밖에 안됩니다 ^^;;;

 

보통 Handler를 사용할때 생길 수 있는 문제가...

 

1. 멤버변수로 new Handler() 의 인스턴스를 가지고 있는경우

핸들러의 인스턴스를 클래스가 가지고있으면, Message Queue가 Outer class인 Outer class의 reference를 잡고있게 되어버린다.

 

2. 핸들러가 Outer class의 reference를 가지고 있어야 할 경우

 

 

 

해결책

 

1. Handler 인스턴스가 필요한경우 새롭게 Handler class를 상속받는 static inner class를 내부에 정의하고 구현.

-> 이렇게 되면 Handler 인스턴스를 사용해도 Message Queue는 Outer class Reference를 안잡게되어, Activity가 종료되는순간 해제되게됨.

 

2. WeakReference 사용

Handler인스턴스도 결국 메시지를 받아서 Outer class인 Acitivtiy의 view를 변경해야할 작업이 있을 수 있음

이럴때 WeakReference로 받게되어 처리하게되면, Activity종료시점에 안정적으로 해제할 수 있음. 또한, Acitivtiy가 날라간 경우도 생길 수 있는데 이때 null check를 통해 안정적으로 가저가면 문제는 해결됨.

 

 

 

 

 

 

 

참고

https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e

 

9 ways to avoid memory leaks in Android

I have been an android developer for quite some time now. And I realised that most of that time, I tend to spend on adding new features to…

android.jlelse.eu

 

아래 싸이트 에서 발췌하였습니다.

https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e

불러오는 중입니다...

들어가서 확인해보시면 알겠지만(6. AsyncTask Reference) 흔히들 Activity에서 inner class로 AsyncTask를 상속받은 클래스를 만든다음 거기서 시간이 많이 지연되는일 혹은, Outer class인 Activity의 view들을 직접 참조하여 사용하는 경우가 많을 것입니다.

 

 

1. 일반 inner class로 생성하여 outer class의 reference를 쥐지 않도록 해야합니다. static inner class를 사용하는게 바람직합니다.

2.outer class의 activity가 종료될때 AsyncTask가 진행중인일이있다면 취소를해주고 종료를 시켜줘야 합니다. 안그렇게 되면 AsyncTask는 activity가 종료되었음에도 불구하고 계속해서 동작하기 때문입니다.

3.inner asynctask에서 직접 outer class의 activity의 view를 참고하지말아야합니다. (결국 context를 쥐고있는것과 같게 됩니다.)

 

 

위의 3가지 실수를 막기위한 옵션으로는

1. inner AsyncTask class -> static inner AsyncTask class로 변경

 

2.Activity onDestory시점에서 AsyncTask의 task를 끝낼수있도록 cancel 요청을 날려야함

 

3.WeakReference를 사용해서 view혹은 context를받아야함

 

 

 

흔히들 BroadcastReceiver 를 사용할때 등록하는것은 예제코들 잘 보고 실천을 하지만

해제하는것에 대해서는 심각하게 생각들을 안하는 것 같다.

 

BroadCastReceiver를 등록한다는것 자체가 익명의 클래스를 생성해서 Android Framework에 등록을 하게된다.

아직  BroadCastReceiver 를 등록했던 액티비티가 사라지게되면 어떻게 될까?

 

해당 액티비티의 context를 Android Framework가 쥐고있기에 완벽하게 해제가 되지않고 memory leak 이 발생하게 된다.

 

onStop 혹은 onDestroy method에서 적절하게 BroadCastReceiver를 필히 해제해줘야 할 것이다.

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
 
 
public BroadCastActivitiy extends AppCompatActivity {
 
    private BroadCastReceiver br;
 
 
    ....
 
    protected void onStart() {
        super.onstart();
        br = new BroadCastReceiver() {
            @override
            public void onReceive(Context context, Intent intent) {
 
            }
          }
        registerReceiver(br, new IntentFilter("hello.myworld"));
    }
 
 
 
    protected void onStop() {
        super.onStart();
        if (br != null)
             unregisterReceiver(br);        
    }
 
    protected void onDestory() {
        super.onDestory();
        if (br != null)
             unregisterReceiver(br);
    }
 
 
}
 
 

 

 

앞선 포스팅 (3. memory leak 회피방법: Inner Class Reference를 주의하라)

과 원리는 동일하다.

 

 

Class에서 익명 class를 만들게되면 익명클래스의 instance는 outer class의 instance에 대해서 

reference를 가지게 된다.

 

 

상황 1. 참고싸이트에서 해결방법 요약

(https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e)

익명클래스로 선언하고 outer class의 context를 static inner class instance로 넘김.

static inner class는 outer class의 instance를 가지지만 WeakReference로 가지기 때문에

스레드에서 긴작업을 하는도중 outer class의 instance가 해제되면, GC에 의해 잘 수거가 이루어지게 되고

WeakReference.get()을 하게되면 null값 반환후 종료되게된다.

 

 

상황2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
public class Outer{
    public void sampleMethod () {
        SampleThread st = new SampleThread();
        st.start();
    }
    
    private static class SampleThread extends Thread {
        public void run() {
            Object sampleObject = new Object();
            // ...
        }
    }
 
}
 
 
s

위의 예제는 올바르다고 볼수있는가? => 그렇다.

Main thread (GC Root)는 Outer와 Static inner class를 따로 참조하고있으며

Outer와 static inner class간에는 서로 참조관계가 없다.

 

 

 

아래의 예제는 올바르다고 볼수있는가? => 아니다.

static inner class를 사용했지만 익명객체를 선언하게 되면서 (New Runnable...)

Runnable 객체가 Outer Class의 instance를 가지게 되고, SampleThread(static inner class)

익명의 Runnable 을 받게되어버리면 다시 Outer class의 intance를 reference하기에 문제가 생기게된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
public class Outer{
    public void sampleMethod () {
        SampleThread st = new SampleThread(new Runnable() {
            public void run () {
                Object sampleObject = new Object();
                // ...
            }
        });
        st.start();
    }
    
    private static class SampleThread extends Thread {
        public SampleThread(Runnable runnable) {
            super(runnable);
        }
    }
 
}
 
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
 

 

 

Outer가 일을 끝내서 소멸되려고 하는찰나에, 익명의 runable이 실행시간이 길게된다면 이는 outer class의 reference를 계속 잡고있기에 outer class의 memory leak이 발생하게된다.

 

 

 

참고

https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e

 

9 ways to avoid memory leaks in Android

I have been an android developer for quite some time now. And I realised that most of that time, I tend to spend on adding new features to…

android.jlelse.eu

 

+ Recent posts