안드로이드 시스템은 date format과 time을 제공하고있는데요.

 

문제는 국가별로 date format이 조금씩 달라서 글로벌 서비스를 하는경우에는 조금은 고민을 해봐야합니다.

 

어떻게 적절하게 국가별로 date와 time을 보여줄지요.

 

예로들면 우리나라는 2월24일을 2.24 이런format으로 표현한다면

 

독일은 24.02. 프랑스는 24/02 등 조금씩 선호하는 방식이 다른편입니다.

 

국가별로 분기해서 작성하자니 만만지않을것같아서 찾아보니 웬걸? 안드로이드에서는 편이상 이런 기능들까지 다 제공해주고있습니다.

 

 

위의 그림은 epoch time으로 한국, 미국, 독일, 프랑스의 date format을 나타내본것이고 

time format도 출력해보았습니다. time format의 경우 현재 사용자가 12시간 format을 사용하고있는지 24시간 format을 사용하고있는지에 따라 출력되게끔 구현하였습니다.

 

 

 

MainActivity.java

main에서만 다 구성하였습니다.

 

getBestDateTimePattern - 현지 Locale에 맞는 date를 출력해줍니다. 기호에 따라 시간과 분도 format에 출력할 수 있습니다. 여기서 좋고 편한것은, MM과 DD의 위치를 신경쓰실 필요가없습니다.

Locale에 맞도록 적절하게 순서를 다시 바꿔서 return해줍니다.

 

getProperTimeAsClockPreference - 현재의 context 정보에 맞게끔 time format을 return하게 됩니다.

12시간, 24시간제 인지에 따라 변환해주고 또한, 국가에 따라 조금씩 다른 format을 주기까지합니다.

    public String getBestDateTimePattern (Locale locale) {
        //return  DateFormat.getBestDateTimePattern(locale, "MM dd hh:mm");
        return  DateFormat.getBestDateTimePattern(locale, "MM dd");
    }
    /*
    12 / 24 For distinguish
    static DateFormat	getTimeFormat(Context context)
    Returns a DateFormat object that can format the time according to the context's locale and the user's 12-/24-hour clock preference.
    */
    public java.text.DateFormat getProperTimeAsClockPreference(Context context) {
        return  DateFormat.getTimeFormat(context);
    }

 

 

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

        Button button = findViewById(R.id.button);
        final TextView tv = findViewById(R.id.textView);

        button.setOnClickListener(new View.OnClickListener () {
            @Override
            public void onClick(View view) {

                SimpleDateFormat dateFormat = null;
                long time = System.currentTimeMillis();

                tv.append("\n time:::" + time);

                String bestDateFormat = getBestDateTimePattern(Locale.KOREA);
                dateFormat = new SimpleDateFormat(bestDateFormat);
                String convertedKoreaDate = dateFormat.format(new Date(time));

                tv.append("\n" + convertedKoreaDate);

                bestDateFormat = getBestDateTimePattern(Locale.US);
                dateFormat = new SimpleDateFormat(bestDateFormat);
                String convertedDate = dateFormat.format(new Date(time));

                tv.append("\n" + convertedDate);


                bestDateFormat = getBestDateTimePattern(Locale.GERMAN);
                dateFormat = new SimpleDateFormat(bestDateFormat);
                convertedDate = dateFormat.format(new Date(time));

                tv.append("\n" + convertedDate);

                bestDateFormat = getBestDateTimePattern(Locale.FRANCE);
                dateFormat = new SimpleDateFormat(bestDateFormat);
                convertedDate = dateFormat.format(new Date(time));

                tv.append("\n" + convertedDate);


                long stampAsCal;
                java.text.DateFormat formatDateTime;
                formatDateTime = getProperTimeAsClockPreference(getApplicationContext());
                String _time = formatDateTime.format(time);

                tv.append("\n converted time:" + _time);
                //kr 24 case: 1:11
                //kr 12 case: 오전 1:11

                //ge 24 case: 1:11
                //ge 12 case: 1:11 vorm

                // remove space
                String convertedConvertedKoreaDate = convertedKoreaDate.replace(" ", "");

                // remove . end of the date
                if (convertedConvertedKoreaDate.length() > 0 && convertedConvertedKoreaDate.charAt(convertedConvertedKoreaDate.length()-1) == '.') {
                    convertedConvertedKoreaDate = convertedConvertedKoreaDate.substring(0, convertedConvertedKoreaDate.length()-1);
                    Log.d("jinss", "gotta");
                }

                convertedConvertedKoreaDate = convertedConvertedKoreaDate + " " + _time;

                tv.append("\n final converted time:" + convertedConvertedKoreaDate);
            }
        });


    }

한국, 미국, 독일, 프랑스에 대해서 시험적으로 테스트 해보았습니다.

또한 최종 date와 time까지 출력도 해보았습니다.

 

앱을 실행해놓고, 12시간 24시간 변경도 해보신다음 버튼을 눌러서 텍스트가 바뀌어서 나오는지도 한번 확인해보시면 되겠습니다.

 

최신 안드로이드 버전 (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

 

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

 

보통 타이머를 사용하면

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

 

최악의 상황을 가정해보자

 

1. static변수가 Inner class의 인스턴스를 저장하고있다.

2. inner class가 outer class의 context 정보를 가지고 있다.

 

 

해결방법

 

1. static변수에 context를 담지말고 일반변수로 담아라

2. inner class 를 -> static inner class로 변경해라. => static inner class는 hidden으로 outer class instance의 reference를 가지지 않는다.

3. WeakReference를 이용해서 context를 받고 필요할때 get으로 꺼내서 지역변수 수준으로 사용해라.

 

static inner class 생성자 {

   private final WeakReference<Activity> activityRef = new WeakReference<>(activtiy);

}

...

 

public void someMethod() {

  Activtiy activity =  activityRef.get();

 

  if (activtiy != null) {

   ....

  }

 

}

 

 

 

 

 

 

참고

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