Bạn đã bao giờ bạn thấy hiện tượng block UI đột ngột trên ứng dụng bạn đã phát triển chưa? Bạn đã bao giờ đào sâu vào tìm hiểu nó vì sao bị như thể chưa. Hoặc có bao giờ bạn muốn nâng cao hiệu năng của ứng dụng hơn không ?
1. Tìm hiểu về Android Profiler
Android Profiler là một bộ các công cụ có sẵn từ Android Studio 3.0 mà thay thế các công cụ Android Monitor trước. Bộ phần mềm mới được thêm rất nhiều cải tiến trong việc chẩn đoán các vấn đề hiệu suất ứng dụng. Nó đi kèm với một cái nhìn thời gian chia sẻ và CPU chi tiết, bộ nhớ và Mạng profilers. Bằng cách sử dụng nó một cách khéo léo, mình có thể tiết kiệm rất nhiều thời gian lãng phí trên gỡ lỗi trong một cửa sổ Logcat.
Để truy cập các công cụ định hình, bấm View > Tool Windows > Android Profiler. Để xem dữ liệu thời gian thực, bạn cần kết nối thiết bị với gỡ lỗi USB được kích hoạt hoặc sử dụng trình giả lập Android và chọn quy trình ứng dụng.
2. Thử nghiệm
Mình sẽ bắt đầu từ việc phát triển một ứng dụng Android đơn giản hiển thị danh sách các ngày liên tiếp. Dưới mỗi ngày, mình có thể hiển thị thời gian còn lại theo ngày, giờ, phút và giây. Mã từ cả hai mẫu đều có sẵn trên GitHub , vì vậy bạn có thể dễ dàng sao chép kho lưu trữ và mở dự án trong Android Studio. Để bây giờ, kiểm tra sửa đổi được gắn thẻ sample-1-before. Bắt đầu với việc xác định một bố cục bao gồm RecyclerView được đặt bên trong SwipeRefreshLayout. Nó sẽ cho phép dữ liệu làm mới bằng việc vuốt dọc:
<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="android.support.v7.
widget.LinearLayoutManager"
tools:listitem="@layout/simple_list_item" />
</android.support.v4.widget.SwipeRefreshLayout>
Tiếp theo, tạo ra Activity bố cục của mình, xử lý tương tác của người dùng và thực hiện các thao tác trên luồng chính để hiển thị dữ liệu được làm mới:
class Sample1Activity : AppCompatActivity() {
private val items = mutableListOf<Item>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.sample_1_activity)
recyclerView.adapter = basicAdapterWithLayoutAndBinder(
items, R.layout.simple_list_item, ::bindItem
)
swipeRefreshLayout.setOnRefreshListener(::refreshData)
}
private fun refreshData() {
items.run { clear(); addAll(generateItems()) }
recyclerView.adapter.notifyDataSetChanged()
swipeRefreshLayout.isRefreshing = false
}
}
private fun generateItems(): List<Item> {
val now = LocalDateTime.now()
return List(1_000) { createItem(now, it + 1) }
}
private fun createItem(now: LocalDateTime, offset: Int): Item {
val date = now.plusDays(offset.toLong())
.toLocalDate().atStartOfDay()
return Item(
formattedDate = date.format(DateTimeFormatter.ISO_LOCAL_DATE),
remainingTime = getRemainingTime(now, date)
)
}
private fun getRemainingTime(start: LocalDateTime, end: LocalDateTime): String {
val duration = Duration.between(start, end)
val days = duration.toDays()
val hours = duration.minusDays(days).toHours()
val minutes = duration.minusDays(days)
.minusHours(hours).toMinutes()
val seconds = duration.minusDays(days)
.minusHours(hours).minusMinutes(minutes).seconds
return buildString {
if (days > 0) append("$days d")
if (hours > 0) append(" $hours h")
if (minutes > 0) append(" $minutes min")
if (seconds > 0) append(" $seconds s")
}.trim() }
private fun bindItem(holder: ViewHolderBinder<Item>, item: Item) = with(holder.itemView) {
dateView.text = item.formattedDate
remainingTimeView.text = resources
.getString(R.string.remaining, item.remainingTime)
}
private data class Item(val formattedDate: String, val remainingTime: String)
Trong dòng 11 mình sử dụng RecyclerView. Chúng ta đang sử dụng thư viện recycler từ android-commons. Mục đích là để giữ cho mã ngắn gọn và RecyclerView bộ điều hợp thiết lập mà không có bất kỳ mã soạn sẵn nào.
Khi kết thúc hàm onCreate, mình đặt một sự kiện lắng nghe về các hành động làm mới dữ liệu khi sử dụng SwipeRefreshLayout. Hàm được tham chiếu refreshData thay thế danh sách bằng các mục mới và thông báo về bất kỳ thay đổi dữ liệu nào.
Mình tạo ra một danh sách 1000 mục. Mỗi mục đặt các thuộc tính của nó liên quan đến thời gian ngày hiện tại và phần bù theo ngày. Offset lấy các giá trị từ 0 đến 999 và ảnh hưởng đến ngày được hiển thị theo mục. Mình sử dụng ThreeTenABP làm API ngày và thời lượng. Đây là một java.time. backport of java.time. gói được tối ưu hóa bởi Jake Wharton cho Android.
Trong funtion getRemainingTime, mình thực hiện một số thao tác để nhận thời gian còn lại là thời lượng dễ đọc hơn cho người dùng.
Sau đó, mình liên kết một mục với chế độ xem để cập nhật itemView tại một vị trí đã chỉ định. Mình truy cập tài nguyên để có được chuỗi được định dạng với remainingTime giá trị.
Item giữ formattedDate và remainingTime các giá trị sẵn sàng để hiển thị trong các TextView thành phần tương ứng . Hãy sử dụng cách bố trí view như sau:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/dateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:lines="1"
android:textColor="@android:color/black"
android:textSize="20sp"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/remainingTimeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:lines="1"
tools:text="Remaining: 2 d 1 h 23 min 5 s" />
</LinearLayout>
Khởi chạy ứng dụng và refresh data để làm mới dữ liệu. Bạn có nhận thấy hơi đơ? Có thể không. Điều đó phụ thuộc nhiều vào CPU của thiết bị của bạn và các quá trình khác tiêu tốn thời gian của CPU. Bây giờ, hãy khởi chạy Android Profiler Tool Window và chọn dòng thời gian thích hợp để mở CPU Profiler. Kết nối thiết bị của bạn và vuốt để refresh data một lần nữa. Lưu ý rằng các luồng cấu hình được thêm vào quy trình ứng dụng và tiêu tốn thêm thời gian CPU. Mình giả sử rằng bây giờ bạn đã có kinh nghiệm. Hãy nhìn vào Logcat vì nó đã cảnh báo bạn về việc xử lý nặng:
I/Choreographer: Skipped 147 frames! The application may be doing too much work on its main thread.
Chúng ta thể bắt đầu kiểm tra. Nhìn vào dòng thời gian CPU Profiler:
Phía trên biểu đồ có chế độ xem thể hiện sự tương tác của người dùng với ứng dụng. Tất cả các sự kiện đầu vào của người dùng hiển thị ở đây dưới dạng các vòng tròn màu tím. Bạn có thể thấy một vòng tròn đại diện cho thao tác vuốt mà mình đã thực hiện để làm mới dữ liệu. Thấp hơn một chút bạn có thể tìm thấy hiện đang hiển thị Sample1Activity. Khu vực này được gọi là dòng thời gian sự kiện .
Bên dưới các sự kiện, có một dòng thời gian CPU, hiển thị bằng đồ họa việc sử dụng CPU của ứng dụng và các quá trình khác liên quan đến tổng thời gian CPU có sẵn. Hơn thế nữa, bạn có thể xem số lượng threads mà ứng dụng của bạn đang sử dụng.
Ở phía dưới, bạn có thể thấy dòng thời gian hoạt động của ứng dụng. Mỗi luồng ở một trong ba trạng thái được chỉ định bởi các màu: hoạt động (màu xanh lá cây), chờ đợi (màu vàng) hoặc ngủ (màu xám). Ở đầu danh sách, bạn có thể tìm thấy chủ đề chính của ứng dụng. Trên thiết bị của mình (Nexus 5X), nó sử dụng ~ 35% thời gian CPU trong khoảng 5 giây. Đó là rất nhiều! Chúng ta có thể ghi lại một dấu vết phương thức để xem những gì đang xảy ra trong đó.
Nhấp vào nút Record🔴 ngay trước khi refresh data làm mới hành động và dừng ghi ngay sau khi hoàn tất làm mới dữ liệu.
Mình sẽ bắt đầu phân tích từ Call Chart cuộc gọi được hiển thị trong tab đầu tiên. Trục hoành biểu thị thời gian trôi qua. Hàm gọi và hàm được gọi (từ trên xuống dưới) được hiển thị trên trục dọc. Các cuộc gọi phương thức cũng được phân biệt bằng màu sắc tùy thuộc vào việc nó gọi API hệ thống, API của bên thứ ba hay phương thức của mình. Lưu ý rằng tổng thời gian cho mỗi cuộc gọi phương thức là tổng thời gian tự phương thức và thời gian tính toán của phương thức. Từ biểu đồ này, bạn có thể suy luận rằng vấn đề hiệu năng nằm ở đâu đó bên trong phương thức generateItems. Di chuyển chuột trên thanh để kiểm tra thêm chi tiết về thời gian đã trôi qua. Bạn cũng có thể bấm đúp vào thanh để xem khai báo phương thức bên trong. Khá khó để suy luận nhiều hơn từ tab này vì nó đòi hỏi phải phóng to và cuộn nhiều, vì vậy mình sẽ chuyển sang tab tiếp theo.
Flame Char là tốt hơn nhiều để lộ ra những phương thức chiếm nhiều thời gian CPU quý của mình. Nó tổng hợp các ngăn xếp cuộc gọi tương tự, đảo ngược biểu đồ từ tab trước đó. Thay vì nhiều thanh ngang ngắn, thanh đơn dài hơn được hiển thị. Chỉ cần nhìn vào nó bây giờ:
Hai thứ đáng ngờ được tìm thấy. Bạn có tin rằng getRemainingTime tổng thời gian thực hiện phương thức sẽ mất hơn 2 giây và LocalDateTime.format hơn 1 giây thời gian của CPU không?
Lưu ý rằng thời gian này cũng bao gồm bất kỳ khoảng thời gian nào khi luồng không hoạt động. Ở góc trên bên phải của ngăn theo dõi phương thức, bạn có thể chuyển thông tin thời gian sẽ được hiển thị trong Thread Time. Nếu mình phân tích một luồng duy nhất có thể là tùy chọn ưa thích vì nó cho thấy mức tiêu thụ thời gian của CPU không bị ảnh hưởng bởi các luồng khác.
Ok, chúng ta hãy tiếp tục. Bây giờ hãy mở tab cuối cùng để xem biểu đồ Bottom Up . Nó hiển thị một danh sách các cuộc gọi phương thức được sắp xếp giảm dần theo mức tiêu thụ thời gian của CPU. Biểu đồ này sẽ cung cấp cho tôi thông tin thời gian chi tiết (tính bằng micrô giây). Mở rộng các phương thức bạn có thể tìm thấy nơi gọi đến.
Kiểm biểu đồ thời gian về các method mà chúng mình nghi ngờ đã tiêu tốn quá nhiều thời gian của CPU.
Bạn có thể thấy điều đó getRemainingTime và LocalDateTime.format tiêu thụ hơn 80%. Để khắc phục tình trạng đóng băng đó, chúng ta cần phải tạo ra các mục. Đó là hiển nhiên.
Vậy làm gi? Bạn có thể đã đưa ra một số giải pháp. Mình thực hiện một xử lý khá nặng để tạo ra 1000 item (một con số không nhỏ). Bạn có thể nghĩ về việc thực hiện phân trang để dần dần tạo và hiển thị dữ liệu. Đó là một ý tưởng tuyệt vời vì nó sẽ mở rộng. Tuy nhiên, lần này tôi muốn đi một con đường khác. Điều gì xảy ra nếu chúng ta thực hiện tất cả các định dạng gần đây trước khi hiển thị dữ liệu ở RecyclerView vị trí được chỉ định - khi chúng ta liên kết Item với RecyclerView.ViewHolder? Nhờ đó, mình sẽ gọi getRemainingTime và LocalDateTime.format phương thức chỉ cho một số hiện đang hiển thị và sẵn sàng để hiển thị các mục - không phải hàng ngàn lần như trước đây. Để đạt được nó, chúng ta cần cập nhật Item các thuộc tính để chỉ giữ dữ liệu cần thiết để thực hiện định dạng sau:
data class Item(val now: LocalDateTime, val offset: Int)
Điều đó đòi hỏi phải áp dụng các thay đổi sau trong generateItems và bindItem chức năng:
private fun generateItems(): List<Item> {
val now = LocalDateTime.now()
return List(1_000) { Item(now, it + 1) }
}
private fun bindItem(holder: ViewHolderBinder<Item>, item: Item) = with(holder.itemView) {
val date = item.now.plusDays(item.offset.toLong())
.toLocalDate()
.atStartOfDay()
val remainingTime = getRemainingTime(item.now, date)
dateView.text = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
remainingTimeView.text = resource.getString(
R.string.remaining,remainingTime)
}
Mọi người thấy rằng mình đã xử lý ở bên trong creatItem vì tất cả các định dạng bây giờ xảy ra bên trong phương thức bindItem. Kiểm tra bản sửa đổi được gắn thẻ sample-1-after để xem thay đổi này.
Bây giờ hãy khởi chạy lại CPU Profiler và ghi lại kết quả sau khi các thay đổi trong code của chúng ta được thực hiện. Nhìn vào Call Chart để kiểm tra xem việc tối ưu hóa của mình có diễn ra tốt không:
Nếu bạn di chuyển chuột qua generateItems chức năng, bạn sẽ thấy rằng bây giờ nó tiêu tốn ~ 0,3 giây. Đó là thời gian CPU ít hơn 13 lần so với trước khi tối ưu hóa! Trước khi vui mừng, hãy chuyển sang Flame Chart ngọn lửa để đảm bảo các thay đổi của mình không có tác động tiêu cực đến tổng thời lượng của method bindItem. May mắn thay, nó tiêu thụ tới 0,1 giây.
Ngoài ra, bạn có thể xem hết lại để đảm bảo tối ưu hóa của mình không ảnh hưởng đến hiệu suất ứng dụng tổng thể. Có vẻ như mọi thứ đã ổn hơn và bài toán hiệu năng đã được giải quyết.
3. Kết luận
Profiler rất hữu dụng trong quá trình đo đạc và tính toán hiệu năng giúp bạn kiểm soát tốt code sạch cũng như hiệu quả hơn.
Comments