فی مصائب نشت حافظه در اندروید یا فرار از تله مموری لیک(بخش دوم)

همونطور که تو پست قبلی قولش رو داده بودم تو اولین پست درمورد مموری لیک صحبت کردم و گفتم چطوری بوجود میاد و چرا باید مراقبش باشیم و چطوری قابل تشخیص هست این رو هم گفتم که معمولا براساس خطاهای برنامه نویس اشیائی بوجود میاد که نمیشه با garbage collector از بینش برد و.... معمولا یه سری اشتباهات رایج هست که باعث میشه نشت حافظه بوجود بیاد و تو این بخش این دسته از اشتباهات رایج و سناریو های معمول رو بررسی می کنیم اگر قسمت قبل رو نخوندین بهتون پیشنهاد میکنم حتما نگاهی بهش بندازید:

فی مصائب نشت حافظه در اندروید یا مموری لیک چیست؟(بخش اول)

broadcast receivers (دریافت کننده های سراسری):

خب broadcast receiver جایی استفاده میشن که لازمه توی برنامه چک کنیم مثلا باطری خالیه یا صد در صد پره یا گوشی تازه بوت شده یا خیر و...

سناریو:

سناریو این هست که ما توی یک اکتیویتی یک broadcast receiver ایجاد کرده و رجیستر کردیم ولی یادمون رفته تو stop این broadcast receiver رو آن رجیستر یا غیرفعال کنیم این یک نمونه واضح از مموری لیک هست چون حتی زمانی که شما اکتیویتی رو ببندید باز هم اون رفرنس و اون فضای اشغال شده باقی خواهد ماند

راه حل

راه حل بسیار ساده است شما باید حتما توی اکتیویتی broadcast receiver رو آن رجیستر کنید و کل این کار تو کمتر از یک خط انجام میشه

بطور کلی پیشنهاد میشه که broadcast receiver رو در on resume و On start رجیستر کنید و در on destroy ان رجیستر

Static Activity or View Reference

سناریو:

فرض کنید نیاز هست که با یک edittext در اکتیویتی کار کنید اما بیاید و edittext رو بنا به هر دلیل و منطقی که ممکن داشته باشید به صورت static تعریف کنید

در این صورت GC توانایی برگردوندن حافظه رو از اون ویو نخواهد داشت و مشکل نشت حافظه بوجود میاد

راه حل

هرگز ویو اکتیویتی و کانتکست رو به صورت استاتیک تعریف نکنید هرگز چنین کاری نکنید

Context

اگر بخوام به سادگی توضیح بدم دو نوع context در اندروید وجود داره یکی UI با حجم بیشتر وسربار بیشتر که برای کارهایی ازجمله inflate کردن استفاده میشه و دیگری non-Ui که سبک تر هست و برای کارهای دیگری که فرايند طولانی تر دارند یا ارتباطی به UI ندارن استفاده میشه بحث Context پیچیدگی های خاص خودشو داره و ترجیح میدم برای خارج نشدن از مسیر اصلی خیلی درگیرش نشیم

سناریو :

در نظر بگیرید که کلاسی داریم که کارش خوندن دیتا از حافظه هست یا کارش به گونه ای هست که باید از singleton Class استفاده کنید و دقیقا برای این کار به Context احتیاج داره ماهم از اونجایی که توی یک اکتیویتی داریم این فراخوانی سینگلتون کلاس رو انجام میدیم با خیالت راحت getactivitycontext رو پاس میدیم نتیجه؟ نشت حافظه

راه حل:

هرجا که نیاز به استفاده از Context وجود داشت جز در مواردی که context مستقیما درگیر المان های UI هست باید از getapplicationcontext استفاده کنید دقت کنید که getcontext و getactivitycontext هردو از نوع UI هستند و در صورتی که به کلاس های سینگلتون یا کلاسهای درگیر هرنوع فرایند طولانی مدت ارسال بشن نشت حافطه ایجاد میکنن میشه برای جاهایی که حتما باید از UI context استفاده کرد context رو ارسال کرد و بلافاصله بعد از استفاده ازش مقدارش رو null کرد هرچند روش دوم توصیه نمیشه

WEAK Reference

بدون شک سخت ترین قسمت این مقاله چه برای نوشتن چه برای درک و بکارگیری همین بخش هست بهتون پیشنهاد میکنم اگر با این نوشته نتونستید درک درستی از موضوع داشته باشید حتما به مقالات مرتبط با پرفرمنس در اندروید و رفرنس ها در جاوا مراجعه کنید چون همین جا می پذیرم احتمالا مطلب یکم سخت تر از موضوعات قبلی باشه

بطور کلی 4 نوع از رفرنس در جاوا وجود داره strong -weak-soft-phantom

زمانی که دارید یک شی از یک کلاس ایجاد میکنید و یا مقداری در constructor میگیرید شما یه ارتباط و یک نوع معرفی مبدا انجام میدید ! این معرفی مبدا رو بهش میگن رفرنس دهی

که البته خیلی ها برای ساده تر شدن فقط نوع strong و weak رو در نظر میگیرن و به دوتای دیگه کار زیادی ندارن اینجا هم کل بحث ما پیرامون همین دو نوع رفرنس هست بهتره برای واضح شدن ادامه مطلب نگاهی به این دو سبک و نوع رفرنس دهی داشته باشیم

رفرنس دهی قوی یا strong :

این همون نوع رفرنس دهی و ایجاد شی ساده در جاوا هست و احتمالا همه به عنوان شکل استاندارد ایجاد شی میشناسنش میتونید به مثال پایین یه نگاهی بندازید تا متوجه بشید دقیقا از چه چیزی حرف میزنیم !

MyObject object = new MyObject();

خب این نوع ایجاد ابجکت برای ما شیئی رو بوجود میاره که GC توانایی از بین بردنش رو نداره و به اصطلاح strong reachable هست خب تا اینجا همه چیز عالیه ما هم دقیقا همینو میخوایم که شی ایجاد شده رو در بالاترین حد دسترسی نگه داریم اینکه مشکل کجا بوجود میاد رو بعد از بررسی weak reference توضیح میدم پس فعلا تا همینجا بشناسیدش :)

رفرنس دهی ضعیف یا weak:

این نوع رفرنس دهی تقریبا مشابه نمونه بالاست با این تفاوت که GC توانایی از بین بردنش رو داره و اجازه نمیده که درون حافظه باقی بمونه نمونه تعریفش هم به این شکل هست :

private WeakReference<MyObject> object= new WeakReference<>(object);

خب حالا ببینیم کجا ها باعث ایجاد مشکل میشه تقریبا تمامی موارد بعدی که درباره نشت حافظه درباره شون بحث میکنیم مستقیما به همین رفرنس دهی اشاره دارن پس اگر متوجه نشدید مفهوم چیه یا دوست دارید بیشتر درباره اش بخونید حتما یه سر به اینجا و اینجا بزنید

مشکل زمانی ایجاد میشه که شما یک اکتیویتی یا المان ویو رو به یک کلاس داخلی دیگه پاس میدین و توی کلاس دوم المان مورد نظر مثلا edittext ما به صورت strong reference تعریف شده کار شما انجام میشه اما به نشت حافظه میخورید

تاکید اول :هرگز یک اکتیویتی یا ویو و المان های درونش رو به صورت strong reference در کلاس دیگه تعریف نکنید هرگز این کار رو نکنید چون مانع پس گرفتن حافظه از اون اکتیویتی بعد از پایان کارش میشید ! هرگز این کار رو نکنید

تاکید دوم : اگر یکی از این موارد زیر رو استفاده میکند به هیچ عنوان حتی به ذهنتون خطور هم نکنه که المان های UI مربوط به ویو و اکتیویتی درونشون به صورت strong reference معرفی کنید هرگز به این چنین کاری فکر نکنید در ادامه میتونید چندتا نمونه از اشتباهات رایج مربوط به رفرنس دهی المان های UI رو مشاهده کنید

  • inner classes
  • threads
  • runnable
  • timertask
  • Handler
  • asynctasks

1 . AsyncTask

پیاده سازی غلط :

public class AsyncTaskReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;
    private BackgroundTask backgroundTask;

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

        /*
         * Executing AsyncTask here!
         * */
        backgroundTask = new BackgroundTask(textView);
        backgroundTask.execute();
    }

    /*
     * Couple of things we should NEVER do here:
     * Mistake number 1. NEVER reference a class inside the activity. If we definitely need to, we should set the class as static as static inner classes don’t hold
     *    any implicit reference to its parent activity class
     * Mistake number 2. We should always cancel the asyncTask when activity is destroyed. This is because the asyncTask will still be executing even if the activity
     *    is destroyed.
     * Mistake number 3. Never use a direct reference of a view from acitivty inside an asynctask.
     * */
 private class BackgroundTask extends AsyncTask<Void, Void, String> {    
        @Override
        protected String doInBackground(Void... voids) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return &quotThe task is completed!&quot
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            textView.setText(s);
        }
    }
}

پیاده سازی صحیح :

public class AsyncTaskReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;
    private BackgroundTask backgroundTask;

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

        /*
         * Executing AsyncTask here!
         * */
        backgroundTask = new BackgroundTask(textView);
        backgroundTask.execute();
    }


    /*
     * Fix number 1
     * */
    private static class BackgroundTask extends AsyncTask<Void, Void, String> {

        private final WeakReference<TextView> messageViewReference;
        private BackgroundTask(TextView textView) {
            this.messageViewReference = new WeakReference<>(textView);
        }


        @Override
        protected String doInBackground(Void... voids) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return &quotThe task is completed!&quot
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
          /*
           * Fix number 3
           * */          
            TextView textView = messageViewReference.get();
            if(textView != null) {
                textView.setText(s);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        /*
         * Fix number 2
         * */
        if(backgroundTask != null) {
            backgroundTask.cancel(true);
        }
    }
}

2 . Threads

پیاده سازی غلط

public class ThreadReferenceLeakActivity extends AppCompatActivity {

    /*  
     * Mistake Number 1: Do not use static variables
     * */    
    private static LeakyThread thread;

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

        createThread();
        redirectToNewScreen();
    }


    private void createThread() {
        thread = new LeakyThread();
        thread.start();
    }

    private void redirectToNewScreen() {
        startActivity(new Intent(this, SecondActivity.class));
    }


    /*
     * Mistake Number 2: Non-static anonymous classes hold an 
     * implicit reference to their enclosing class.
     * */
    private class LeakyThread extends Thread {
        @Override
        public void run() {
            while (true) {
            }
        }
    }

پیاده سازی صحیح :

public class ThreadReferenceLeakActivity extends AppCompatActivity {

    /*
     * FIX I: make variable non static
     * */
    private LeakyThread leakyThread = new LeakyThread();

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

        createThread();
        redirectToNewScreen();
    }


    private void createThread() {
        leakyThread.start();
    }

    private void redirectToNewScreen() {
        startActivity(new Intent(this, SecondActivity.class));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // FIX II: kill the thread
        leakyThread.interrupt();
    }


    /*
     * Fix III: Make thread static
     * */
    private static class LeakyThread extends Thread {
        @Override
        public void run() {
            while (!isInterrupted()) {
            }
        }
    }
}

و البته میتونید مثال های بیشتر رو در این مقاله ببینید

سعی من زمان نوشتن این دو پست باز کردن موضوع و بررسی بیشترش بود بخش های زیادی از این دو پست صرفا ترجمه مقالاتی بود که خوندم و تو خود متن و منابع لیک مقالات هست میتونید بهشون مراجعه کنید تلاشم هم اول ایجاد محتوای قابل درک و ساده به زبان فارسی بود و دوم بررسی دقیق تر و کنار هم چیدن پازل هایی که از جاهای مختلف تو اینترنت قابل دسترسی هستن در صورتی که سوالی مطرح بود یا احیانا ایرادی در نوشته دیدین ممنون میشم که تو کامنت ها یادآوری کنید

مرسی از وقتی که گذاشتید

منابع :

https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e
https://android.jlelse.eu/memory-leak-patterns-in-android-4741a7fcb570
https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859
https://instabug.com/blog/how-to-fix-android-memory-leaks/