Giới thiệu về gỡ lỗi

1. Trước khi bắt đầu

Bất cứ ai khi sử dụng phần mềm đều có khả năng gặp phải một lỗi nào đó. Lỗi ở đây chính là lỗi phần mềm, gây ra hành vi ngoài ý muốn, chẳng hạn như ứng dụng gặp sự cố hoặc một tính năng nào đó của ứng dụng không hoạt động như mong đợi. Tất cả nhà phát triển, bất kể kinh nghiệm làm việc, đều tạo ra lỗi khi viết mã và một trong những kỹ năng quan trọng nhất đối với nhà phát triển Android là phát hiện và khắc phục lỗi. Không có gì lạ khi thấy toàn bộ bản phát hành ứng dụng được dành riêng cho việc sửa lỗi. Ví dụ: xem thông tin chi tiết phiên bản Google Maps bên dưới:

9d5ec1958683e173.png

Quá trình khắc phục lỗi được gọi là gỡ lỗi. Nhà khoa học máy tính nổi tiếng Brian Kernighan từng nói rằng "công cụ gỡ lỗi hiệu quả nhất vẫn là suy nghĩ cẩn thận kết hợp với việc đặt những câu lệnh in một cách thận trọng". Có thể bạn đã quen thuộc với câu lệnh println() của Kotlin từ các lớp học lập trình trước. Tuy nhiên các nhà phát triển Android chuyên nghiệp thường sử dụng tính năng ghi nhật ký để sắp xếp đầu ra của chương trình tốt hơn. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng tính năng ghi nhật ký trong Android Studio và cách dùng tính năng này làm công cụ gỡ lỗi. Bạn cũng sẽ tìm hiểu cách đọc nhật ký thông báo lỗi, còn gọi là dấu vết ngăn xếp (stack trace), để phát hiện và xử lý lỗi. Cuối cùng, bạn sẽ tìm hiểu cách tự nghiên cứu lỗi cũng như tìm hiểu cách để chụp kết quả từ trình mô phỏng Android và lưu lại dưới dạng ảnh chụp màn hình hoặc ảnh GIF của ứng dụng đang chạy.

Điều kiện tiên quyết

  • Biết cách điều hướng một dự án trong Android Studio.

Kiến thức bạn sẽ học được

Kết thúc lớp học lập trình này, bạn có thể

  • Viết nhật ký bằng android.util.Logger.
  • Biết được thời điểm nào nên sử dụng cấp độ nhật ký nào
  • Sử dụng nhật ký như một công cụ gỡ lỗi đơn giản và hiệu quả.
  • Biết cách tìm thông tin hữu ích trong dấu vết ngăn xếp.
  • Tìm kiếm thông báo lỗi để giải quyết các sự cố của ứng dụng.
  • Chụp ảnh màn hình và ảnh GIF động từ Trình mô phỏng Android.

Bạn cần có

  • Máy tính đã cài đặt Android Studio.

2. Tạo dự án mới

Thay vì sử dụng một ứng dụng lớn và phức tạp, chúng ta sẽ bắt đầu với một dự án trống để minh hoạ các câu lệnh nhật ký và cách sử dụng các lệnh này để gỡ lỗi.

Hãy bắt đầu bằng cách tạo một dự án Android Studio mới như bên dưới.

  1. Trên màn hình New Project (Dự án mới), hãy chọn Empty Activity (Hoạt động trống).

72a0bbf2012bcb7d.png

  1. Đặt tên cho ứng dụng là Debugging (Gỡ lỗi). Đảm bảo ngôn ngữ được chọn là Kotlin và giữ nguyên các thiết lập khác.

60a1619c07fae8f5.png

Sau khi tạo dự án, bạn sẽ được chào đón bằng một dự án Android Studio mới, hiển thị một tệp có tên là MainActivity.kt.

e3ab4a557c50b9b0.png

3. Ghi nhật ký và gỡ lỗi kết quả đầu ra

Trong các bài học trước, bạn đã sử dụng câu lệnh println() của Kotlin để xuất kết quả đầu ra dưới dạng văn bản. Trong một ứng dụng Android, phương pháp hay nhất để ghi nhật ký kết quả đầu ra là sử dụng lớp Log. Có một số hàm để ghi kết quả đầu ra, thể hiện dưới dạng Log.v(), Log.d(), Log.i(), Log.w() hoặc Log.e(). Các phương thức này có hai tham số: tham số thứ nhất được gọi là "tag" (thẻ), là một chuỗi xác định nguồn của thông điệp nhật ký (chẳng hạn như tên của lớp đã ghi nhật ký văn bản). Tham số thứ hai là thông điệp nhật ký thực sự.

Thực hiện các bước sau để bắt đầu sử dụng tính năng ghi nhật ký trong một dự án trống.

  1. Trong MainActivity.kt, trước phần khai báo lớp, thêm một hằng số có tên là TAG và thiết lập giá trị của hằng số này bằng tên của lớp MainActivity.
private const val TAG = "MainActivity"
  1. Thêm một hàm mới vào lớp MainActivity tên là logging() như dưới đây.
fun logging() {
    Log.v(TAG, "Hello, world!")
}
  1. Gọi logging() trong onCreate(). Phương thức onCreate() mới sẽ có dạng như sau.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. Chạy ứng dụng để xem nhật ký hoạt động. Nhật ký sẽ xuất hiện trong cửa sổ Logcat ở đáy màn hình. Vì Logcat sẽ hiển thị kết quả đầu ra từ các quy trình khác trên thiết bị (hoặc trình mô phỏng), nên bạn có thể chọn ứng dụng (com.example.debugging) từ trình đơn thả xuống để lọc ra những nhật ký không liên quan đến ứng dụng của mình.

199c65d11ee52b5c.png

Trong cửa sổ kết quả đầu ra, bạn có thể thấy thông báo "Hello, world!". Nếu cần, hãy nhập "hello" vào hộp tìm kiếm ở phần trên cửa sổ Logcat để tìm tất cả nhật ký.

92f258013bc15d12.png

Các cấp độ nhật ký

Lý do tồn tại các hàm nhật ký khác nhau và được đặt tên bằng các chữ cái khác nhau là vì những hàm này sẽ được dùng để ghi kết quả tương ứng cho từng cấp độ nhật ký. Tuỳ vào loại thông tin cần xuất ra, bạn sẽ sử dụng một cấp độ nhật ký tương ứng để lọc thông tin đó trong dữ liệu đầu ra của Logcat. Có 5 cấp độ nhật ký chính mà bạn thường xuyên sử dụng.

Cấp độ nhật ký

Trường hợp sử dụng

Ví dụ

ERROR (LỖI)

Nhật ký ERROR cho biết đã xảy ra sự cố nghiêm trọng, chẳng hạn như lý do khiến ứng dụng bị lỗi.

Log.e(TAG, "The cake was left in the oven for too long and burned.").

WARN (CẢNH BÁO)

Nhật ký WARN ít nghiêm trọng hơn lỗi nhưng vẫn cho biết điều gì đó cần khắc phục để tránh xảy ra lỗi nghiêm trọng hơn. Ví dụ: khi gọi một hàm không dùng nữa, điều này có nghĩa rằng bạn không nên sử dụng hàm này mà tốt nhất là sử dụng một hàm thay thế khác.

Log.w(TAG, "This oven does not heat evenly. You may want to turn the cake around halfway through to promote even browning.")

INFO (THÔNG TIN)

Nhật ký INFO cung cấp thông tin hữu ích, chẳng hạn như một thao tác nào đó được thực hiện thành công.

Log.i(TAG, "The cake is ready to be served.").println("The cake has cooled.")

DEBUG (Gỡ lỗi)

Nhật ký DEBUG chứa những thông tin có thể hữu ích khi điều tra sự cố. Các nhật ký này không hiển thị trong các bản phát hành, chẳng hạn như các nhật ký bạn đã phát hành trên Cửa hàng Google Play.

Log.d(TAG, "Cake was removed from the oven after 55 minutes. Recipe calls for the cake to be removed after 50 - 60 minutes.")

VERBOSE (Chi tiết)

Giống như tên gọi, VERBOSE là cấp độ nhật ký ít rõ ràng nhất. So với nhật ký gỡ lỗi, nhật ký chi tiết hơi mang tính chủ quan, nhưng nhìn chung, nhật ký chi tiết có thể bị xoá sau khi triển khai một tính năng nào đó, còn nhật ký gỡ lỗi vẫn được sử dụng cho quá trình gỡ lỗi. Các nhật ký này cũng không được đưa vào các bản phát hành.

Log.v(TAG, "Put the mixing bowl on the counter.")Log.v(TAG, "Grabbed the eggs from the refrigerator.")Log.v(TAG, "Plugged in the stand mixer.")

Lưu ý rằng không có quy tắc nào quy định thời điểm thích hợp để sử dụng từng loại cấp độ nhật ký, đặc biệt là khi nào nên sử dụng DEBUGVERBOSE. Các nhóm phát triển phần mềm có thể tạo ra các nguyên tắc riêng về thời điểm sử dụng từng cấp độ nhật ký hoặc có thể quyết định không sử dụng một số cấp độ nhật ký nào đó, chẳng hạn như VERBOSE. Điều quan trọng cần nhớ là hai cấp độ nhật ký này không có trong các bản phát hành. Vì vậy, việc sử dụng nhật ký để gỡ lỗi sẽ không ảnh hưởng đến hiệu suất của các ứng dụng đã phát hành. Ngược lại, các câu lệnh println() vẫn tồn tại trong các bản phát hành và gây ảnh hưởng tiêu cực đến hiệu suất ứng dụng.

Hãy xem các cấp độ nhật ký này trông như thế nào trong Logcat.

  1. Trong MainActivity.kt, thay thế nội dung của phương thức logging() bằng nội dung sau.
fun logging() {
    Log.e(TAG, "ERROR: a serious error like an app crash")
    Log.w(TAG, "WARN: warns about the potential for serious errors")
    Log.i(TAG, "INFO: reporting technical information, such as an operation succeeding")
    Log.d(TAG, "DEBUG: reporting technical information useful for debugging")
    Log.v(TAG, "VERBOSE: more verbose than DEBUG logs")
}
  1. Chạy ứng dụng và quan sát kết quả trong Logcat. Nếu cần, hãy lọc kết quả đầu ra để chỉ hiển thị nhật ký từ quá trình com.example.debugging. Bạn cũng có thể lọc kết quả để chỉ hiển thị nhật ký chứa thẻ "MainActivity". Để thực hiện điều này, hãy chọn Edit Filter Configuration (Chỉnh sửa cấu hình bộ lọc) từ trình đơn thả xuống ở phía trên cùng bên phải của cửa sổ Logcat.

383ec6d746bb72b1.png

  1. Sau đó, nhập "MainActivity" cho Log Tag (Thẻ nhật ký) rồi nhập tên cho bộ lọc như hiển thị bên dưới.

e7ccfbb26795b3fc.png

  1. Bây giờ, bạn chỉ thấy thông điệp nhật ký chứa thẻ "MainActivity".

4061ca006b1d278c.png

Hãy lưu ý chữ cái xuất hiện trước tên lớp, ví dụ: W/MainActivity, được dùng để thể hiện cấp độ nhật ký tương ứng. Ngoài ra, nhật ký WARN được hiển thị bằng màu xanh lam trong khi nhật ký ERROR được hiển thị bằng màu đỏ, tương tự như lỗi nghiêm trọng trong ví dụ trước.

  1. Tương tự như việc lọc kết quả gỡ lỗi theo quy trình, bạn cũng có thể lọc kết quả theo cấp nhật ký. Theo mặc định, bộ lọc được chọn sẵn là Verbose, cho phép hiển thị nhật ký VERBOSE và các mức nhật ký cao hơn. Chọn Warn từ trình đơn thả xuống và lưu ý rằng bây giờ kết quả chỉ hiển thị nhật ký cấp WARNERROR.

c4aa479a8dd9d4ca.png

  1. Một lần nữa, chọn Assert (Xác nhận) trong trình đơn thả xuống. Quan sát kết quả sẽ không thấy nhật ký nào được hiển thị. Thao tác này sẽ lọc ra mọi cấp độ từ ERROR trở xuống.

ee3be7cfaa0d8bd1.png

Mặc dù có vẻ như chúng ta đang hơi nghiêm trọng hoá vấn đề đối với câu lệnh println(), nhưng khi tạo các ứng dụng lớn hơn, bạn sẽ có nhiều kết quả Logcat hơn. Lúc này, việc sử dụng nhiều cấp độ nhật ký sẽ cho phép bạn tìm ra thông tin hữu ích và có liên quan nhất. Sử dụng Nhật ký được xem là phương pháp hay nhất và được ưu tiên hơn so với câu lệnh println() trong quá trình phát triển ứng dụng Android. Điều này là do nhật ký gỡ lỗi và nhật ký chi tiết sẽ không ảnh hưởng đến hiệu suất của các bản phát hành. Bạn cũng có thể lọc nhật ký theo các cấp độ nhật ký. Việc chọn cấp độ nhật ký chính xác sẽ mang lại lợi ích cho những người khác trong nhóm phát triển, những người có thể không quen thuộc với mã như bạn, đồng thời, cũng giúp bạn phát hiện và xử lý lỗi dễ dàng hơn.

4. Nhật ký chứa thông báo lỗi

Giới thiệu về lỗi

Với một dự án trống, bạn chưa có nhiều lỗi để tiến hành gỡ lỗi. Tuy nhiên, với vai trò là một nhà phát triển Android, bạn sẽ gặp phải nhiều lỗi gây nên sự cố cho ứng dụng, vốn không mang lại trải nghiệm tốt cho người dùng. Hãy thêm một số mã để tạo ra sự cố cho ứng dụng này.

Bạn vẫn còn nhớ kiến thức toán học về việc không thể chia một số cho số 0 không? Hãy xem điều gì sẽ xảy ra khi chúng ta cố gắng chia một số cho số không trong mã nhé.

  1. Thêm hàm sau vào MainActivity.kt phía trên hàm logging(). Mã này bắt đầu bằng hai số và dùng lệnh repeat để ghi lại kết quả chia tử số cho mẫu số năm lần. Mỗi lần chạy mã trong khối lệnh repeat, giá trị của mẫu số sẽ giảm đi một. Trong lần lặp thứ năm và lần cuối cùng, ứng dụng sẽ chia tử số cho số không.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}
  1. Sau lời gọi đến hàm logging() trong onCreate(), hãy thêm lời gọi hàm division().
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
    division()
}
  1. Chạy lại ứng dụng và lưu ý rằng lần này ứng dụng sẽ bị lỗi. Di chuyển xuống các nhật ký cho lớp MainActivity.kt, bạn sẽ thấy các nhật ký cho hàm logging() được định nghĩa trước đó, nhật ký chi tiết cho hàm division() và sau đó là nhật ký lỗi màu đỏ giải thích lý do ứng dụng bị lỗi.

12d87f287661a66.png

Phân tích chi tiết dấu vết ngăn xếp

Nhật ký lỗi mô tả sự cố (còn gọi là ngoại lệ) được gọi là dấu vết ngăn xếp. Dấu vết ngăn xếp hiển thị tất cả hàm được gọi dẫn đến ngoại lệ, bắt đầu bằng hàm được gọi gần đây nhất. Bạn có thể xem toàn bộ kết quả như bên dưới.

Process: com.example.debugging, PID: 14581
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:14)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Thật sự là một văn bản rất dài! Rất may, thông thường bạn chỉ cần một số trong đó để thu hẹp chính xác phạm vi lỗi. Hãy bắt đầu từ những dòng trên cùng.

  1. java.lang.RuntimeException:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero

Dòng đầu tiên nêu rõ ứng dụng này không thể hoạt động. Đây chính là lý do ứng dụng gặp sự cố. Dòng tiếp theo cung cấp thông tin chi tiết hơn. Cụ thể, hoạt động này không thể bắt đầu là do xảy ra ngoại lệ ArithmeticException. Cụ thể hơn, loại ngoại lệ ArithmeticException này là "chia cho 0".

  1. Caused by:
Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)

Cuộn xuống dòng "Caused by" (Nguyên nhân), hệ thống một lần nữa cho biết đang có lỗi "chia cho 0". Lần này, bạn sẽ thấy chính xác hàm (division()) cũng như số dòng gây ra lỗi (21). Tên tệp và số dòng trong cửa sổ Logcat chứa siêu liên kết. Kết quả này cũng hiển thị tên của hàm gây ra lỗi, division() và hàm gọi đến hàm này, onCreate().

Không có điều gì đáng ngạc nhiên ở đây vì lỗi này do chúng ta cố ý đưa ra. Tuy nhiên, nếu bạn cần xác định nguyên nhân gây ra một lỗi chưa xác định nào đó thì việc biết chính xác loại ngoại lệ, tên hàm và dòng gây ra lỗi sẽ là đem lại những thông tin vô cùng hữu ích.

Tại sao gọi là "dấu vết ngăn xếp"?

Thuật ngữ "dấu vết ngăn xếp" dường như khá mới mẻ khi nói đến kết quả đầu ra dưới dạng văn bản cho một lỗi nào đó. Để hiểu rõ hơn về cách hoạt động của chức năng này, bạn cần biết thêm thông tin về ngăn xếp hàm (function stack).

Khi một hàm gọi một hàm khác, thiết bị sẽ không chạy bất cứ mã nào từ hàm đầu tiên cho đến khi hàm thứ hai kết thúc. Khi hàm thứ hai hoàn tất quá trình thực thi, hàm đầu tiên sẽ tiếp tục từ nơi dừng lại trước đó. Nguyên tắc này sẽ áp dụng tương tự đối với bất cứ hàm nào khi được gọi từ một hàm thứ hai. Hàm thứ hai sẽ không tiếp tục thực thi cho đến khi hàm thứ ba (và bất kỳ hàm nào khác mà hàm này gọi tới) kết thúc và hàm đầu tiên sẽ không tiếp tục cho đến khi hàm thứ hai hoàn tất quá trình thực thi. Hình thức này tương tự như một ngăn xếp trong thực tế, chẳng hạn như một chồng đĩa hoặc một chồng thẻ. Nếu muốn lấy một chiếc đĩa, bạn sẽ lấy chiếc trên cùng. Bạn không thể lấy một chiếc đĩa ở vị trí thấp hơn trong chồng đĩa nếu trước đó chưa lấy hết toàn bộ các đĩa ở trên chiếc đĩa này.

Ngăn xếp hàm có thể được minh hoạ bằng mã sau.

val TAG = ...

fun first() {
    second()
    Log.v(TAG, "1")
}

fun second() {
    third()
    Log.v(TAG, "2")
    fourth()
}

fun third() {
    Log.v(TAG, "3")
}

fun fourth() {
    Log.v(TAG, "4")
}

Nếu gọi first() thì các số này sẽ được ghi trong nhật ký theo thứ tự sau.

3
2
4
1

Tại sao lại như vậy? Khi hàm đầu tiên được gọi, hàm này sẽ gọi second() ngay tức thì, vì vậy, 1 không thể được ghi lại ngay lúc đó. Ngăn xếp hàm sẽ giống như thế này.

second()
first()

Hàm thứ hai sau đó gọi hàm third(), hàm này sẽ được thêm vào ngăn xếp hàm.

third()
second()
first()

Sau đó, hàm thứ ba sẽ in số 3. Sau khi thực thi xong, hàm này sẽ bị xoá khỏi ngăn xếp hàm.

second()
first()

Hàm second() ghi vào nhật ký số 2 rồi gọi hàm fourth(). Đến thời điểm hiện tại, các số 32 đã được ghi vào nhật ký và ngăn xếp hàm hiện tại như sau.

fourth()
second()
first()

Hàm fourth() in số 4 và bị xoá (bật ra) khỏi ngăn xếp hàm. Sau đó, hàm second() hoàn tất quá trình thực thi và xoá khỏi ngăn xếp hàm. Bây giờ, hàm second() và tất cả hàm được hàm này gọi đến đã hoàn tất. Tiếp đó, công cụ này sẽ thực thi mã còn lại trong first() để in ra số 1.

Do đó, các con số được ghi lại theo thứ tự: 4, 2, 3, 1.

Hãy dành thời gian xem kỹ đoạn mã và liên tưởng đến hình ảnh của ngăn xếp hàm, bạn có thể thấy chính xác mã nào được thực thi và thực thi theo thứ tự nào. Đây có thể là một kỹ thuật gỡ lỗi hiệu quả dành cho các lỗi như lỗi chia cho số không ở ví dụ trên. Việc duyệt qua từng bước trong mã này có thể giúp bạn hình dung được vị trí nên đặt câu lệnh nhật ký để gỡ lỗi cho các vấn đề phức tạp hơn.

5. Sử dụng nhật ký để phát hiện và khắc phục lỗi

Trong phần trước, bạn đã nghiên cứu về dấu vết ngăn xếp, cụ thể là dòng bên dưới.

Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)

Tại đây, bạn có thể biết được sự cố đã xảy ra trên dòng 21 và có liên quan đến việc chia cho 0. Do đó, tại một thời điểm nào đó trước khi mã này thực thi, giá trị của mẫu số là 0. Với ví dụ nhỏ như trên, sẽ không có vấn đề gì khi bạn tự mình kiểm tra kết quả của từng bước trong mã này. Tuy nhiên, bạn cũng có thể sử dụng câu lệnh nhật ký để tiết kiệm thời gian bằng cách in giá trị của mẫu số trước khi xảy ra lỗi chia cho 0.

  1. Trước câu lệnh Log.v(), thêm lệnh gọi Log.d() để ghi lại giá trị của mẫu số. Bạn sử dụng Log.d() vì hàm này dùng riêng cho việc gỡ lỗi, đồng thời cho phép bạn lọc ra các nhật ký chi tiết.
Log.d(TAG, "$denominator")
  1. Chạy lại ứng dụng. Ứng dụng vẫn còn lỗi, bạn nên ghi nhật ký cho mẫu số nhiều lần. Bạn có thể sử dụng Cấu hình bộ lọc để chỉ hiển thị những nhật ký chứa thẻ "MainActivity".

d6ae5224469d3fd4.png

  1. Bạn có thể thấy nhiều giá trị được in ra. Dường như vòng lặp được thực thi một vài lần trước khi xảy ra sự cố ở vòng lặp thứ 5, khi mẫu số có giá trị 0. Điều này rất hợp lý, mẫu số ban đầu là 4 và giá trị của mẫu số giảm đi 1 qua 5 vòng lặp. Để khắc phục lỗi này, bạn có thể thay đổi số lần lặp trong vòng lặp từ 5 thành 4. Nếu bạn chạy lại ứng dụng, sự cố sẽ không xuất hiện nữa.
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

6. Ví dụ về gỡ lỗi: truy cập một giá trị không tồn tại

Theo mặc định, mẫu Blank Activity (Hoạt động trống) bạn dùng để tạo dự án sẽ thêm một hoạt động duy nhất, trong đó có một TextView được căn giữa trên màn hình. Như đã tìm hiểu trước đó, bạn có thể tham chiếu các thành phần hiển thị trong mã bằng cách thiết lập một mã nhận dạng trong trình chỉnh sửa bố cục và truy cập thành phần hiển thị đó bằng findViewById(). Trước khi gọi onCreate() trong một lớp hoạt động, bạn phải gọi setContentView() để tải tệp bố cục (chẳng hạn như activity_main.xml). Nếu bạn cố gắng gọi findViewById() trước khi gọi setContentView(), ứng dụng sẽ gặp sự cố do thành phần hiển thị đó không tồn tại. Hãy thử truy cập thành phần hiển thị này để minh hoạ cho một lỗi khác.

  1. Mở activity_main.xml, chọn TextView Hello, world! TextView và đặt id thành hello_world.

c94be640d0e03e1d.png

  1. Quay lại ActivityMain.kt, thêm mã trong onCreate() để lấy TextView này và đổi phần văn bản thành "Hello, debugging!" (Xin chào, gỡ lỗi!) trước khi gọi setContentView().
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val helloTextView: TextView = findViewById(R.id.hello_world)
    helloTextView.text = "Hello, debugging!"
    setContentView(R.layout.activity_main)
    division()
}
  1. Chạy lại ứng dụng và lưu ý rằng ứng dụng vẫn gặp sự cố ngay khi khởi chạy. Có thể bạn cần xoá bộ lọc trong ví dụ trước để xem kết quả nhật ký không chứa thẻ "MainActivity". 840ddd002e92ee46.png

Trường hợp ngoại lệ sẽ là một trong những điều cuối cùng xuất hiện trong Logcat (nếu không, bạn có thể tìm RuntimeException). Kết quả sẽ có dạng như sau.

Process: com.example.debugging, PID: 14896
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:14)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Tương tự như phần trước, ở trên cùng, bạn sẽ thấy thông báo "Unable to start activity" (Không thể bắt đầu hoạt động). Điều này rất hợp lý vì ứng dụng đã gặp sự cố trước khi sự kiện MainActivity được phát hành. Dòng tiếp theo sẽ cung cấp thông tin chi tiết hơn về lỗi.

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

Ở phía dưới trong dấu vết ngăn xếp, bạn cũng sẽ thấy dòng này hiển thị chính xác lệnh gọi hàm và số dòng.

Caused by: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

Chính xác thì lỗi này có nghĩa là gì và giá trị "null" (rỗng) là gì? Đây là một ví dụ minh hoạ và có thể bạn đã hình dung ra lý do ứng dụng gặp sự cố, nhưng chắc chắn bạn sẽ gặp những thông báo lỗi mà bạn chưa từng thấy trước đây. Khi gặp tình huống này, rất có thể bạn không phải là người đầu tiên nhìn thấy lỗi đó và thậm chí những nhà phát triển có kinh nghiệm nhất cũng thường tìm kiếm thông tin về lỗi này trên Google để xem cách giải quyết vấn đề từ những người khác. Việc tra cứu này thường dẫn đến một số kết quả trên Stackoverflow, một trang web mà các nhà phát triển có thể đặt câu hỏi và đưa ra câu trả lời về các đoạn mã bị lỗi hoặc những chủ đề lập trình phổ biến hơn.

Có thể có nhiều câu hỏi được trả lời tương tự nhưng không hoàn toàn giống nhau, hãy lưu ý các mẹo sau khi bạn tự tìm kiếm câu trả lời.

  1. Câu trả lời được đưa ra lâu chưa? Những câu trả lời từ vài năm trước có thể không còn phù hợp hoặc có thể đang sử dụng phiên bản ngôn ngữ hoặc bộ khung (framework) đã lỗi thời.
  2. Câu trả lời sử dụng ngôn ngữ Java hay Kotlin? Vấn đề của bạn chỉ liên quan đến ngôn ngữ này hay ngôn ngữ kia, hay có liên quan đến một bộ khung cụ thể nào đó?
  3. Các câu trả lời được đánh dấu là "accepted" (được chấp nhận) hoặc có nhiều lượt tán thành hơn có thể có chất lượng tốt hơn. Tuy nhiên, hãy lưu ý rằng các câu trả lời khác vẫn có thể mang lại những thông tin có giá trị.

1636a21ff125a74c.png

Con số ở trên cho biết số lượt tán thành (hoặc phản đối) và dấu kiểm màu xanh lục cho biết câu trả lời được chấp nhận.

Nếu chưa thấy ai đặt câu hỏi cho vấn đề của mình, bạn có thể đặt câu hỏi mới. Khi đặt câu hỏi trên StackOverflow (hoặc bất kỳ trang web nào), bạn nên lưu ý đến các nguyên tắc này.

Hãy tiếp tục và tìm kiếm lỗi.

a60ba40e5247455e.png

Đọc qua một số câu trả lời, bạn sẽ thấy rằng lỗi này có thể do nhiều nguyên nhân. Tuy nhiên, do bạn cố tình gọi hàm findViewById() trước setContentView() nên một số câu trả lời cho câu hỏi này có vẻ rất khả quan. Câu trả lời được bình chọn nhiều thứ hai cho rằng:

"Có thể bạn đang gọi hàm FindViewById trước khi gọi setContentView? Nếu vậy, hãy thử gọi FindViewById SAU KHI gọi setContentView"

Sau khi nhìn thấy câu trả lời này, bạn có thể xác minh xem thực sự findViewById() được gọi quá sớm, trước hàm setContentView() thay vì nên gọi sau setContentView().

Hãy cập nhật mã để sửa lỗi này.

  1. Di chuyển lệnh gọi đến findViewById() và dòng thiết lập văn bản của helloTextView xuống dưới lệnh gọi đến setContentView(). Phương thức onCreate() mới sẽ có dạng như bên dưới. Bạn cũng có thể thêm nhật ký (như được hiển thị) để xác minh lỗi được khắc phục.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.d(TAG, "this is where the app crashed before")
    val helloTextView: TextView = findViewById(R.id.hello_world)
    Log.d(TAG, "this should be logged if the bug is fixed")
    helloTextView.text = "Hello, debugging!"
    logging()
    division()
}
  1. Chạy lại ứng dụng. Lưu ý rằng ứng dụng sẽ không gặp sự cố nữa và phần văn bản sẽ được cập nhật như mong đợi.

9ff26c7deaa4a7cc.png

Chụp ảnh màn hình

Đến thời điểm này, có thể bạn đã thấy nhiều ảnh chụp màn hình từ trình mô phỏng Android trong khoá học này. Việc chụp ảnh màn hình tương đối đơn giản nhưng mang lại hữu ích khi chia sẻ thông tin với các thành viên khác trong nhóm, chẳng hạn như chia sẻ các bước tái hiện lỗi. Bạn có thể chụp ảnh màn hình trong trình mô phỏng Android bằng cách nhấn vào biểu tượng máy ảnh trên thanh công cụ ở bên phải.

455336f50c5c3c7f.png

Bạn cũng có thể dùng phím tắt Command+S để chụp ảnh màn hình. Ảnh chụp màn hình sẽ tự động lưu vào thư mục Desktop (Máy tính).

Ghi lại ứng dụng đang chạy

Mặc dù ảnh chụp màn hình có thể truyền tải nhiều thông tin, nhưng đôi khi việc chia sẻ các bản ghi của một ứng dụng đang chạy sẽ giúp ích cho những người khác để tái hiện nguyên nhân gây ra lỗi. Trình mô phỏng Android cung cấp một số công cụ tích hợp sẵn, giúp bạn dễ dàng chụp ảnh GIF (ảnh động) của ứng dụng đang chạy.

  1. Trong các công cụ của trình mô phỏng ở bên phải, hãy nhấp vào nút Xem thêm 558dbea4f70514a8.png (tuỳ chọn cuối cùng) để hiển thị các tuỳ chọn gỡ lỗi bổ sung dành cho trình mô phỏng. Một cửa sổ bật lên cung cấp các công cụ bổ sung để mô phỏng chức năng của thiết bị thực tế cho mục đích thử nghiệm.

46b1743301a2d12.png

  1. Trên trình đơn bên trái, nhấp vào Record and Playback (Ghi và Phát), sau đó bạn sẽ thấy màn hình hiển thị nút để bắt đầu ghi.

dd8b5019702ead03.png

  1. Hiện tại, dự án của bạn chưa có nội dung nào thú vị để ghi lại, ngoài một TextView tĩnh. Hãy chỉnh sửa mã để cập nhật nhãn vài giây một lần để hiển thị kết quả của phép chia. Trong phương thức division() trong MainActivity, thêm một lệnh gọi tới Thread.sleep(3000) trước khi gọi đến Log(). Bây giờ, phương thức này sẽ có dạng như sau (lưu ý rằng vòng lặp chỉ nên lặp lại 4 lần để tránh gặp sự cố).
fun division() {
   val numerator = 60
   var denominator = 4
   repeat(4) {
       Thread.sleep(3000)
       Log.v(TAG, "${numerator / denominator}")
       denominator--
   }
}
  1. Trong activity_main.xml, đặt id của TextView thành division_textview.

db3c1ef675872faf.png

  1. Quay lại MainActivity.kt, thay thế lệnh gọi đến Log.v() bằng các lệnh gọi đến findViewById()setText() sau đây để đặt nội dung văn bản thành thương số.
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
  1. Bây giờ, bạn sẽ hiển thị kết quả của phép chia trong giao diện người dùng ứng dụng, nên bạn cần chú ý đến một vài chi tiết về cách chạy các bản cập nhật giao diện người dùng. Trước tiên, bạn cần tạo một luồng mới có thể chạy vòng lặp repeat. Nếu không, Thread.sleep(3000) sẽ chặn luồng chính và khung hiển thị ứng dụng sẽ không hiển thị cho đến khi onCreate() kết thúc (bao gồm division() có vòng lặp repeat).
fun division() {
   val numerator = 60
   var denominator = 4

   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
         denominator--
      }
   }
}
  1. Nếu thử chạy ứng dụng ngay bây giờ, bạn sẽ thấy FATAL EXCEPTION. Lý do cho ngoại lệ này là chỉ những luồng đã tạo khung hiển thị mới được phép thay đổi khung hiển thị đó. Thật may là bạn có thể tham chiếu luồng giao diện người dùng bằng runOnUiThread(). Thay đổi division() để cập nhật TextView trong luồng giao diện người dùng.
private fun division() {
   val numerator = 60
   var denominator = 4
   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         runOnUiThread {
            findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
            denominator--
         }
      }
   }
}
  1. Chạy ứng dụng rồi chuyển tức thì sang trình mô phỏng. Khi ứng dụng khởi chạy, nhấp vào nút Start Recording (Bắt đầu ghi) trong cửa sổ Extended Controls (Các điều khiển mở rộng). Bạn sẽ thấy thương số được cập nhật 3 giây một lần. Sau khi thương số được cập nhật vài lần, hãy nhấp vào Stop Recording (Dừng ghi).

55121bab5b5afaa6.png

  1. Theo mặc định, dữ liệu đầu ra được lưu ở định dạng .webm. Dùng trình đơn thả xuống để xuất dữ liệu đầu ra dưới dạng tệp GIF.

850713aa27145908.png

7. Xin chúc mừng

Xin chúc mừng! Trong lộ trình này, bạn tìm hiểu được rằng:

  • Gỡ lỗi là quá trình khắc phục các lỗi xảy ra trong mã.
  • Nhật ký cho phép bạn in văn bản với các cấp độ và thẻ nhật ký khác nhau.
  • Dấu vết ngăn xếp cung cấp thông tin về ngoại lệ, chẳng hạn như chỉ ra chính xác hàm gây ra lỗi và số dòng nơi ngoại lệ xảy ra.
  • Trong quá trình gỡ lỗi, có thể ai đó đã gặp phải sự cố tương tự và bạn có thể sử dụng các trang web như Stackoverflow để nghiên cứu về lỗi đó.
  • Bạn có thể dễ dàng xuất ảnh chụp màn hình cũng như ảnh GIF động bằng trình mô phỏng Android.

Tìm hiểu thêm