داستانی سریع درمورد async callback، نشت حافظه، WeakReference و تصورات غلط

آفلاین
user-avatar
پوریا شریفی
13 تیر 1399, خواندن در 11 دقیقه

همکارم بهم گفت که: "شما گفتید که در اینجا می‌تواند نشت حافظه وجود داشته باشد. من هم به دنبال راه‌حلی رفتم و دیدم که می‌توانیم از weakReference <Context > برای ارجاع به Context اکتیویتی استفاده کنیم، به این ترتیب نشت نخواهیم داشت".

من هم به او گفتم، "این عالی است، جز اینکه این یک هک است، و مشکل اصلی را حل نمی‌کند، بنابراین این کار را نکن".

سپس او گفت: "اما نمی‌فهمم منظورت چیه. مقاله‌ای را به من نشان دهید که مشکلی که شما می‌گویید را توضیح دهد و اینکه چرا نباید از weakReference استفاده کنم".

هنگامی که من مسئله واقعی را توضیح دادم، او گفت: "این یک مشکل متفاوت است، اما اکنون می‌دانم که چرا نباید از weakReference استفاده کنیم".

اما این موضوع است که ما را به اینجا می‌رساند، زیرا ظاهرا مردم موفق نشده‌اند که مقاله‌ای در مورد اینکه چرا نباید از weakReference<Context> (یا weakReference<Activity> یا weakReference<MyContract.View>) استفاده کنیم تا نشت حافظه را رفع کنیم (مربوط به تغییرات پیکربندی و asynchronous callback، AsyncTask و موارد مشابه).

در حقیقت، تنها مقالاتی که به این موضع اشاره دارد که این رویکرد درست نیست را؛ می‌توانید در این لینک پیدا کنید.

بنابراین بیایید سفری را آغاز کنیم و ارزیابی کنیم که چرا استفاده از weakReference راه‌حل مناسبی نیست، و چه گزینه‌های دیگری برای حل نشت حافظه و مشکل واقعی ما وجود دارد.

به هرحال، نشت حافظه در Context اندروید چیست؟

برای افرادی که نمی‌دانند، می‌تواند اتفاق بی‌افتد که وقتی یک ارجاع از Context اکتیویتی داشته باشید، در چیزی که بیشتر از اکتیویتی زنده است. اکتیویتی شروع به تغییر پیکربندی می‌کند، بنابراین اکتیویتی از نو ساخته می‌شود، اما ارجاعی که از Context داشتیم نگه داشته می‌شود و همه view‌های inflate شده آن نیز زنده هستند، بنابراین garbage collector نمی‌تواند آن‌ها را finalize کند.

البته، این امر در صورت ثبت اکتیویتی در global bus نیز صدق می‌کند، چون اکتیویتی خاتمه یافته ولی اکتیویتی unregister نمی‌شود. همچنین در صورت وجود یک ارجاع static به اکتیویتی نیز اتفاق می‌افتد. همه این موارد می‌توانند باعث بروز نشت حافظه شوند.

یک ابزار خیلی باحال به نام LeakCanary وجود دارد که می‌توان برای شناسایی این سناریو از آن استفاده کرد.

گزینه‌هایی برای رفع نشت حافظه ناشی از نگه‌داری ارجاع به اکتیویتی

#(-۱).) WeakReference

هر راهنما، مقاله، کتابخانه و پاسخی که در stackOverflow پیدا می‌کنید، فقط در مورد نحوه استفاده از weakReference صحبت کرده است.

// from stack overflow
@Override
protected void onPostExecute(Bitmap bitmap) {
    super.onPostExecute(bitmap);

    final ImageView imageView = mImageViewReference.get();
    if (imageView != null) {
        imageView.setImageBitmap(bitmap);
    }
}

مطمئناً، اگر به نتیجه عملیات asynchronous اهمیت ندهید، کارتون تمومه. آیا نتیجه موفقیت‌ آمیز بود؟ آیا به شکست منجر شد؟ این ارجاع null است، ما مطمئناً به نتیجه آن به اندازه کافی اهمیت نمی‌دهیم تا آن را رسیدگی کنیم، یا در مورد آن به کاربر اطلاع دهیم.(و آیا null بودن آن بستگی به این دارد که garbage collector آن را finalized کرده است.)

حال اگر نمی‌خواهید فقط صاف جلوه دهید، نتیجه یک درخواست شبکه پرهزینه را نادیده بگیرید، ممکن است بخواهید کاری در این باره انجام دهید. شاید خطایی که دریافت کرده‌اید مهم باشد و نمی‌خواهید آن را کنار بگذارید. شاید حتی یک دیالوگ نشان دهید که بخواهید کاربر از آن اطلاع داشته باشد.

بنابراین ما به راه‌حلی نیاز داریم که این سناریو را رسیدگی کند: برای مثال انتشار یک رویداد و اجازه به اکتیویتی برای ثبت/لغو اکتیویتی برای انجام این رویداد، و در صف قرار دادن این رویداد وقتی که اکتیویتی در دسترس نیست.

#(۱-).) استفاده از EventBus که هنگام توقف اکتیویتی، متوقف می‌شود و هنگام توقف رویداد‌ها را در صف قرار می‌دهد

رویکرد آن به شکل زیر است که این مشکل را به روشی نسبتاً ساده حل می‌کند، این رویکرد نیاز است زیرا از صف کار با اولویت در اندروید استفاده می‌کنیم، بنابراین نیاز داریم که رویدادها را از طریق EventBus منتشر کنیم.

public class SingletonBus {
    private final Bus bus;
    SingletonBus() {
        this.bus = new Bus(ThreadEnforcer.ANY); //yes I know
    }
    private Bus bus;

    private volatile boolean paused;
    private final Vector<Object> eventQueueBuffer = new Vector<>();   
    private Handler handler = new Handler(Looper.getMainLooper());

    public <T> void postToMainThread(final T event) {
        if(paused) {
            eventQueueBuffer.add(event);
        } else {
            handler.post(() -> {
                bus.post(event);
            });
        }
    }

    public <T> void register(T subscriber) {
        bus.register(subscriber);
    }

    public <T> void unregister(T subscriber) {
        bus.unregister(subscriber);
    }

    public boolean isPaused() {
        return paused;
    }

    public void setPaused(boolean paused) {
        this.paused = paused;
        if(!paused) {
            Iterator<Object> eventIterator = eventQueueBuffer.iterator();
            while(eventIterator.hasNext()) {
                Object event = eventIterator.next();
                postToMainThread(event);
                eventIterator.remove();
            }
        }
    }
}

بنابراین وقتی اکتیویتی بچرخد، رویدادها در صف قرار می‌گیرند و هنگامی که دوباره resume شود، آن‌ها را دریافت می‌کنیم.

ما می‌خواهیم رفتار دقیقا مشابه ولی با یک مکانیسم بروزتر داشته باشیم.

#(۰.۵-).) LiveData<EventWrapper>

براساس این مقاله، ما می‌توانیم SingleLiveEvent را با LiveData<EventWrapper> جایگزین کنیم، که می‌توانیم صریحاً به دستور بگوییم که آن را استفاده کرده‌ایم، و نمی‌خواهیم دوباره از آن استفاده کنیم. که برای کنترل رویداد فقط یک‌بار ایده‌آل است، حتی اگر چند ناظر نیز داشته باشد.

این احتمالا یک روش عالی برای نشان دادن پیام خطا به نظر می‌رسد. Livedata بسیار خوب است، زیرا دارای unsubscription خودکار در onDestroy Lifecycle است، و دارای فقط یک مقدار است که هنگامی که ناظر تغییری را مشاهده کرد یک بار منتشر می‌شود، بنابراین اکتیویتی حتی اگر دوباره subscription شود آن را دریافت می‌کند.

مشکل اینحا است که فقط یک مقدار را نگهداری می‌کند، و رویدادها را در صف قرار نمی‌دهد. بنابراین می‌تواند منجر به از دست دادن رویدادها شود، و آن چیزی نیست که ما اینجا نیاز داریم.

#(۲-).) ذخیره خطا یا بارگذاری داده در یک کلاس بسته، داخل BehaviorRelay(درست مانند چیزی که MVI به شما می‌گوید)

یک اشتباه بزرگ و مرسوم، این است که حالت و داده در یک BehaviorRelay ذخیره کنید، این شیء ترکیب شده همانند زیر است:

sealed class HelloWorldViewState {
    object LoadingState : 
        HelloWorldViewState()
    data class DataState(val greeting: String) : 
        HelloWorldViewState()
    data class ErrorState(val error: Throwable) :
        HelloWorldViewState()
}

آنچه که در آموزش‌های MVI به شما نمی‌گویند این است که اگر این کار را در حالی که داده‌های جدید در حال بارگیری یا در حال نشان دادن خطایی هستید انجام دهید، بنابراین هنگام rotation یا resubscription، داده‌هایی که قبلا دریافت شده‌اند با وضعیت loading/error دوباره بازنویسی می‌شوند.

که این رویکرد باعث می‌شود بدتر از WeakReference باشد.

اگر می‌خواهید این کار را انجام بدهید، در عوض از LiveData<Resource<T>> که بسیار قابل اطمینان‌تر است استفاده کنید. که داده‌های بارگذاری شده را با خطا و progress dialog بازنویسی نمی‌کند.

نکته منفی دیگر این رویکرد bundle event with state است(مگر اینکه طراحی شما به آن نیاز داشته باشد)، چرخش صفحه باعث می‌شود چندین بار resubscription شود و همان خطا را دریافت کنید. بنابراین این قابلیت "یک بار اجرا" را به شما نمی‌دهد.

#(۱+).) PublishRelay + ObservableTransformers.Value()

با استفاده از قدرت‌های RxJava به‌راحتی می‌توانیم رویدادهای متفاوت را برای مشترکین متعدد با استفاده از Relay منتشر کنیم.

PublishRelay به ما این امکان را می‌دهد رویداد را یکبار منتشر کنیم، و مشترکینی که در حال حاضر مشترک (subsciber) هستند آن را دریافت می‌کنند، اما مشترکین جدید دیگر آن را دریافت نمی‌کنند(بنابراین نیازی به EventWrapper فقط برای ذخیره یک پرچم برای استفاده یا استفاده نشدن نیست).

مزیت دیگر آن این است که David Karnok در 28 آگوست 2018، ObservableTransformers.Value() را به RxJava2Extensions اضافه کرد که اجازه می‌دهد که کار زیر را انجام دهیم:

eventRelay.compose(
    ObservableTransformers.valve(isActivityPausedObservable, true)
)

به این معنی که می‌توانیم در حالی‌که اکتیویتی متوقف شده است رویدادها را در صف قرار دهیم، یعنی که مشترکی در دسترس نداریم. خبر خوب این است که value در صورت عدم وجود مشترکین رویدادها منتشر نمی‌کند(برخلاف RefCountSubject)، و در صورت لزوم اجازه‌ چندین مشترک را می‌دهد(برخلاف UnicastWorkSubject).

اگر تاکنون کد این اپراتور را خوانده باشید، احتمالا نمی‌خواهید آن را خودتان بنویسید، بخصوص بعد از نگاه به آن :).

#(۱.۵+).) Command Queue 0.1.1

از آنجا که در زمانی که نیاز به یک راه‌حل داشتم نتوانستم یک راه‌حل پیدا کنم، من CommandQueue را متناسب با نیازهای خود ایجاد کردم:

  • پشتیبانی از یک ناظر(observer) در لحظه
  • در صف قرار دادن رویدادها هنگامی که ناظر در دسترس نیست
  • توانایی نگه‌داشتن صریح صف رویداد تا زمانی که صریحا از سر گرفته می‌شود

از آنجا که نمی‌توانستم چیزی شبیه به آن پیدا کنم (و نمی‌توانیم از RxJava برای این کار استفاده کنیم)، بنابراین CommandQueue را ساختم، که طول آن حدود 100 خط است. این فقط صف و Boolean است(و interfacr)، کی واقعا این را میدانست.

override fun onStart() {
    super.onStart()
    viewModel.commandQueue.setReceiver { command -> 
        when (command) {
            is MainViewModel.Events.DoSomething ->
                showToast("Do something!", Toast.LENGTH_SHORT)
            is MainViewModel.Events.DoOtherThing -> 
                showToast("Do other thing!", Toast.LENGTH_SHORT)            
        }.safe()
    }
}
override fun onStop() {
    viewModel.commandQueue.detachReceiver()
    super.onStop()
}

با همچین نسخه‌ای، من تعجب می‌کنم که به همان اندازه به آن اعتماد داریم. Thread-safe نیست اما کار می‌کند، همچنین برای فهمیدن ترکیب اپراتورها نیازی به خواندن کد منبع نیست. به هرحال ما از آن فقط در thread اصلی استفاده می‌کنیم.

مطمئناً گزینه‌های کمتری برای حل این مشکل جود دارد. من فقط می‌خواستم COmmandQueue را ذکر کنم چون اگر گزینه بهتری را برای آن سه مورد می‌شناختم، از آن استفاده می‌کردم.

#(۱.۷۵+).) UnicastWorkSubject

در RxjavaExtensions در واقع موارد زیادی وجود دارد. همراه با نسخه Reactive-Streams-spec سازگار با Single(به نام solo)، یا Completeables(به نام None)، یا Maybe(به نام perhapse). چیزی که جالب است subjectهای دیگری است که دریافت می‌کنیم.

یکی از آن‌ها UnicastWorkSubject است، که امکان مشترک شدن یک ناظر جدید را به محض لغو اشتراک ناظر قبلی می‌دهد، و رویدادها را بین آن‌ها نگه می‌دارد.

این subject یک صف نامحدود از آیتم‌ها را نگه می‌دارد و آن را به یک ناظر در آن واحد relay/replay می‌کند، مطمئن شوید که وقتی یک ناظر dispose می‌شود، همه آیتم‌های مصرف نشده برای ناظر بعدی آماده باشد.

همچنین این subject به بیش از یک ناظر به صورت همزمان اجازه نمی‌دهد.

بنابراین اگر این متناسب نیازهای ما باشد(همانطور که می‌دانیم فقط یک مشترک وجود خواهد داشت)، این روش عالی می‌تواند جایگزین WeakReference با انتشار رویداد باشد.

#(۱.۲۵+).) DispatchWorkSubject

ظاهراً یک راه‌حل برای منتشرکردن/صف‌بندی رویدادها با RxJava از ViewModel(یا هرچیز دیگری) به View استفاده از DispatchWorkSubject خواهد بود.

این subject آیتم‌ها را بافر می‌کند و اجازه می‌دهد تا یک یا چند ناظر تنها یکی از آیتم‌های موجود در بافر را مصرف کند. اگر هیچ ناظری وجود نداشت(یا اینکه همه dispose شده است)، DispatchWorkSubject بافر را نگه خواهد داشت و بعدا ناظرها می‌توانند مصرف از بافر را ادامه دهند.

روش کار به این صورت است که به مشترکین متعدد اجازه می‌دهد، اما اگر مشترکی وجود نداشت، رویدادها تا رسیدن اولین مشترک در صف قرار می‌گیرند.

بنابراین بجای هک کردن، با حفظ کردن فقط یک رویداد با LiveData، یا نادیده گرفتن رویدادهای منتشر شده با استفاده از PublishRelay وقتی که هیچ مشترکی وجود ندارد، می‌توانیم با استفاده از روش بالا رویدادها را تا زمانی که View دوباره مشترک می‌شود در صف قرار دهیم.

با این حال رویداد را فقط به یک مشترک ارسال می‌کند(در صورت وجود چندین مشترک). این نکته مهم را باید در نظر داشته باشید.

#(۲.۵+).) EventEmitter

از آنجا که نمی‌توانستم یک راه مطمئن برای صف‌بندی رویدادها وقتی که هیچ ناظری وجود ندارد و همچنین از ناظرهای چندگانه پشتیبانی کند پیدا کنم، یک کتابخانه به نام EventEmitter ساختم که رویدادها را در صف قرار می‌دهد و آن‌ها را به چندین ناظر ارسال می‌کند.

درحالی که درنظر گرفته شده است که مشاهده و نوشتن در یک thread باشد(حل مشکل استفاده از Rx در یک روش غیر قابل درک)، هنوز هم ایمن‌ترین چیزی است که من تاکنون برای این مشکل پیدا کرده‌ام.

private val emitter: EventEmitter<String> = EventEmitter()
val events: EventSource<String> get() = emitter

fun doSomething() {
    emitter.emit("hello")
}

و

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewModel = getViewModel<MyViewModel>()
    viewModel.events.observe(viewLifecycleOwner) { event ->
        // ...
    }
}

این به شما اجازه می‌دهد که تا به صورت محلی در ناشر رویداد آن را بنویسید، در حالی‌که آن را به‌عنوان یک منبع رویداد ارسال می‌کنیم(که فقط مشاهده می‌شود).

نتیجه

امیدوارم این مقاله نشان دهد که چه گزینه‌هایی برای رسیدگی به نتایج asynchronou callback وجود دارد، بجای تحمیل کردن آن با استفاده از رویکرد WeakReference.

برای خاتمه این موضوع، من باید یکی از راه‌حل‌های ممکن برای انجام همه این جادوها را ذکر کنم، و آن این است که مطمئن شویم asynchronous callback در هنگام چرخش صفحه گم نمی‌شود و می‌توانیم نتایج را به Ui وصل کنیم.

یعنی اینکه می‌توانید تغییرات پیکربندی را برای چرخش مسدود کنید، و همه فراخوانی‌ها را در (onConfigurationChange(Configuration دریافت کنید، و می‌توانید بدون اینکه اکتیویتی در فرایند از بین برود آن را کنترل کنید.

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
پوریا شریفی @pouryasharifi78
ابتدا که با برنامه‌نویسی آشنا شدم به سمت php و طراحی وب رفتم، بعد از اون به توسعه‌ی اندروید علاقه‌مند شدم و تقریبا ۲ سال است که مشغول به برنامه‌نویسی...
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو