Như bạn đã biết, dù cho chúng ta truy cập tới một View hoặc các tài nguyên hệ thống (Asset), phần lớn chúng ta đều tham chiếu đến một Context. Hôm nay mình muốn trình bày với các bạn về Context trong Android.
Bạn có công nhận là bạn gọi rất nhiều đến Context trong ứng dụng không? Vậy bạn đã hiểu bản chất của Content trong Android là gì chưa? Và có khi nào bạn bị cảnh báo là dùng Context không đúng sẽ làm ứng dụng bị Memory Leak chưa?
Bài viết này, chúng ta sẽ cùng đi tìm cội nguồn của Context nhé!
Nội dung chính của bài viết
Context trong Android là gì?
Theo documentation của Google:
Context is an interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
Tạm dịch:
Context là một interface chứa thông tin toàn cục về môi trường ứng dụng. Đây là một lớp trừu tượng được triển khai bởi hệ thống Android. Nó cho phép truy cập đến các tài nguyên và các lớp ứng dụng cụ thể, cũng như gọi đến các tác vụ trên mức ứng dụng như khởi chạy các activity, gửi và nhận intents, v.v..
Nói cách khác, chúng ta đều đã biết và sử dụng nó, trong nhiều cách khác nhau. Nhưng thường là sai cách hoặc đều không quan tâm đến các việc Garbage Collector (GC – trình dọn rác của JVM) sẽ dọn dẹp context như nào v.v.. dẫn đến việc bị leak memory…
Cứ bình tĩnh, chúng ta sẽ tiếp tục tìm hiểu bên dưới nhé
Có bao nhiêu cách để gọi Context trong Android
Có những cách nào để có thể get được một context?
Theo kinh nghiệm bản thân thì có một cách gọi như sau:
- this / getActivity()
- getContext()
- getBaseContext()
- getApplicationContext()
this ở đây chính là Activity hiện tại. Ví dụ, hàm hiển thị Toast quen thuộc:
Toast.makeText(this, "Boom!", Toast.LENGTH_LONG).show();
getContext() là một hàm của lớp View, sẽ nhận được context hiện tại của View đó.
getBaseContext() là một trường hợp đặc biệt. Cách gọi này ít thông dụng nhưng không có nghĩa là hoàn toàn vô dụng. Nó chỉ sử dụng khi bạn biết rõ nó là gì, và có lý do chính đáng để sử dụng. Đơn cử như bạn muốn override một Context bằng một context khác.
getApplicationContext() được sử dụng ở những nơi mà bạn không quan tâm hoặc không cần phải truy cập đến context của activity. Những gì bạn muốn là thông tin của context trong ứng dụng của bạn.
Lưu ý này: Bạn phải rất thận trọng để không giữ context lâu hơn cần thiết. Đặc biệt là với các activity. Rất tiếc là tất cả chúng ta đều phạm lỗi này bằng cách tạo ra các anonymous inner classes với một tham chiếu mạnh (strong reference) đến Context trong Android.
Mình lấy ví dụ điển hiển đó là gọi Context trong AsyncTask.
AsyncTasks là công cụ mà việc thực thi không dừng lại sau khi activity bị hủy. Do đó, garbage collector không thể thu hồi nó khi Activity bị hủy. Và nguy cơ Memory Leak sẽ xảy ra rất lớn.
Vậy phải làm sao đây?
Như ví dụ bên dưới, mình sử dụng Handler để đảm bảo context của Activity không bị leak. Nhưng cách này khá tốn effort, đặc biệt khi viết một ứng dụng phức tạp. (Đọc thêm bài về Handler trong Android nhé)
private static final Handler handler = new Handler(); private final Runnable messageTask = new SetMessageTask(this, "This is the message!"); private void postTask() { handler.postDelayed(messageTask, 8000); } private static class SetMessageTask implements Runnable { private final WeakReference activity; pivate final String message; SetMessageTask(MainActivity activity, String message) { this.activity = new WeakReference<>(activity); this.message = message; } @Override public void run() { if (message == null || message.isEmpty()) { Log.e("SetMessageRunnable", "The message is null!"); return; } MainActivity activity = (MainActivity) this.activity.get(); if (activity == null) { Log.w("SetMessageRunnable", "Activity is not available anymore."); return; } // do something with activity instance here... } }
Sử dụng Context trong Fragment
Với việc bổ sung fragments, mọi thứ trở nên phức tạp hơn. Bởi vì vòng đời của fragments kết nối tới vòng đời của các activity. Điều này gây ra nhiều lỗi về state và transactions, cũng như việc quản lý stack của ứng dụng.
Cá nhân mình, đã chứng kiến lỗi crash java.lang.IllegalStateException Crash: Can not perform this action after onSaveInstanceState rất nhiều lần. Lỗi này xảy ra do việc tạo các transaction mới sau khi lưu trạng thái hoạt động, hoặc trước khi activity được tái tạo lại.
Đôi khi bạn nghĩ transactions của fragments ở ngoài hàm onCreate() sẽ gây ra một exception. Điều này không đúng. Bởi vì bạn cũng có thể thực hiện một transactions trong onPostResume() hoặc trong onResumeFragments().
Tuy nhiên, đó chỉ là ba nơi bạn có thể gọi, điều này có vẻ hơi hạn chế nhỉ.
Do tài nguyên điện thoại có hạn, nên activity có thể bị hủy bất cứ lúc nào bởi hệ thống để giải phóng tài nguyên. Đó là lý do tại sao bạn không nên dựa quá nhiều vào context và tính khả dụng của nó.
Quy tắc sử dụng Context trong Android
Đôi khi, có những tình huống rất “nực cười” trong ứng dụng. ví dụ, việc kiểm soát hiển thị view và load data cho view trong cùng một hàm (bạn không nên làm điều này nhé, chỉ ví dụ thui!).
Khi mà việc trộn lẫn giữa khai báo view và tải dữ liệu cho view đó cũng trong 1 hàm. Điều này làm mọi thứ phụ thuộc lẫn nhau dẫn đến khó kiểm soát.
Bạn có thể bị sa đà vào việc code mọi thứ trong onCreateView()… Và sự xuất hiện của Fragments chỉ gây ra các vấn đề thêm trầm trọng, đặc biệt là việc sử dụng Context.
Một phần vì Context không phải lúc cũng sẵn sàng để gọi được. Chỉ trong onActivityCreated() hoặc onAttach() chúng ta mới chắc chắn về tính khả dụng của Context.
Ở hình bên dưới, bạn có thể thấy MainActivity đã bị memory leak. Đây là kết quả mình sử dụng thư viện LeakCanary để phát hiện memory leak
6 Quy tắc sử dụng Context trong Android
Tóm lại, để hạn chế để xảy ra lỗi memory leak khi sử dụng Context, mình đúc kết được một số quy tắc như sau:
- Sử dụng getContext() hoặc Activity.this khi cần xử lý đến các Views nằm trên activity.
- Sử dụng getApplicationContext() nếu bạn cần context ở cấp ứng dụng, không phù hợp với bất kỳ view/ activity nào. ví dụ: sử dụng hàm này với BroadcastReceiver hoặc Service)
- Không sử dụng getBaseContext(). Khi mà không thực sự hiểu thì tốt nhất tìm cách khác an toàn hơn
- Sử dụng WeakReference nếu bạn cần truy cập đến context từ bên trong threads.
- Không tham chiếu đến context của một activity từ một activity khác. Tuyệt đối không sử dụng context như một biến static.
- Ở trong fragment, gán một context để sử dụng ở trong hàm onAttach(Context context)
Như vậy, chúng ta đã hiểu rõ về Context trong Android rồi đúng không? Và mình hi vọng với những quy tắc trên, bạn sẽ không còn lo lắng về vấn đề memory leak khi sử dụng context nữa
Nếu thấy bài viết hay thì đừng quên chia sẻ cho mọi người cùng đọc nhé
Bình luận. Cùng nhau thảo luận nhé!