第 4 章 Android应用的调试

第 4 章 Android应用的调试

本章将讲解如何处理应用的bug,同时也会介绍如何使用LogCat、Android Lint以及Android Studio内置的代码调试器。

为练习调试,我们先搞点破坏。打开QuizActivity.java文件,在onCreate(Bundle)方法中,注释掉mQuestionTextView变量赋值的那行代码,如代码清单4-1所示。

代码清单4-1 注释掉一行关键代码(QuizActivity.java)

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate(Bundle) called");
    setContentView(R.layout.activity_quiz);

    if (savedinstanceState != null) {
        mCurrentindex = savedinstanceState.getint(KEY_iNDEX, 0);
    }

    // mQuestionTextView = (TextView)findViewById(R.id.question_text_view);

    mTrueButton = (Button)findViewById(R.id.true_button);
    mTrueButton.setOnClickListener(new View.OnClickListener() {
        ...
    });
    ...
}

运行GeoQuiz应用,看看会发生什么。图4-1是应用崩溃后的消息提示画面。不同Android版本的消息提示略有差异,但本质上都是一个意思。

图4-1 GeoQuiz应用崩溃了

显然,我们知道应用为何崩溃。假如不知道的话,接下来的全新视角或许能帮助解决问题。

4.1 异常与栈跟踪

为了方便查看,展开Android Monitor工具窗口。上下滑动LogCat窗口滚动条,应该会看到整片红色的异常或错误信息,如图4-2所示。这就是标准的AndroidRuntime异常信息报告。

如果看不到,可试着选择LogCat的No Filters过滤项。另外,如果觉得信息太多,看不过来,还可以调整Log Level为Error,让系统只输出严重问题日志。还可以使用搜索功能,比如搜“FATAL EXCEPTION”,就能直接定位到崩溃异常。

该异常报告首先给出最高层级的异常及其栈跟踪,然后是导致该异常的异常及其栈跟踪。如此不断追溯,直到找到一个没有原因的异常。

在我们编写的大部分代码中,最后一个没给出原因的异常往往就是关注点。这里,没有原因的异常是java.lang.NullPointerException。紧接着该异常语句的一行就是其栈跟踪信息的第一行。从该行可以看出发生异常的类和方法以及它所在的源文件及代码行号。单击蓝色链接,Android Studio会自动跳转到源代码的对应代码行。

Android Studio定位的这行代码是mQuestionTextView变量在updateQuestion()方法中的首次使用。名为NullPointerException的异常暗示了问题所在,即变量没有初始化。

{%}

图4-2 LogCat中的异常与栈跟踪

为修正该问题,取消对变量mQuestionTextView赋值语句的注释。

碰到运行异常时,记得在LogCat中寻找最后一个异常及其栈跟踪的第一行(对应着源代码)。这里是问题发生的地方,也是寻找解决方案的最佳起点。

如果发生应用崩溃的设备没有与计算机连接,日志信息也不会全部丢失。设备会将最近的日志保存到日志文件中。日志文件的内容长度及保留的时间取决于具体的设备,不过,获取十分钟之内产生的日志信息通常是有保证的。只要将设备连上计算机,在Devices视图里选择所用设备,LogCat会自动打开并显示日志文件保存的内容。

4.1.1 诊断应用异常

即使出了问题,应用也不一定会崩溃。某些时候,应用只是出现了运行异常。例如,每次单击NEXT按钮时,应用都毫无反应。这就是一个非崩溃型的应用运行异常。

在QuizActivity.java中,修改mNextButton监听器代码,注释掉mCurrentIndex变量递增的语句,如代码清单4-2所示。

代码清单4-2 注释掉一行关键代码(QuizActivity.java)

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    mNextButton = (Button)findViewById(R.id.next_button);
    mNextButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
            updateQuestion();
        }
    });
    ...
}

运行GeoQuiz应用,点击NEXT按钮。可以看到,应用无响应。

这个问题要比上一个棘手。它没有抛出异常,所以,解决起来不像前面跟踪追溯并消除异常那么简单。有了前面的经验,这里可以推测出导致该问题的两个因素:

  • mCurrentIndex变量值没有改变;

  • updateQuestion()方法没被调用。

如果实在没有头绪,则需要设法跟踪并找出问题所在。在接下来的几小节里,我们将学习两种跟踪问题的方法:

  • 记录栈跟踪的诊断性日志;

  • 利用调试器设置断点调试。

4.1.2 记录栈跟踪日志

QuizActivity中,为updateQuestion()方法添加日志输出语句,如代码清单4-3所示。

代码清单4-3 方便实用的调试方式(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {
    ...
    private void updateQuestion() {
        Log.d(TAG, "Updating question text ", new Exception());
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    }

如同前面AndroidRuntime的异常,Log.d(String, String, Throwable)方法记录并输出整个栈跟踪日志。这样,就可以很容易看出updateQuestion()方法在哪些地方被调用了。

作为参数传入Log.d(String, String, Throwable)方法的异常不一定就是已捕获的抛出异常。可以创建一个全新的Exception,把它作为不抛出的异常对象传入该方法。借此,我们得到异常发生位置的记录报告。

运行GeoQuiz应用,点击NEXT按钮,然后在LogCat中查看输出结果,如图4-3所示。

{%}

图4-3 输出结果

栈跟踪日志的第一行即调用异常记录方法的地方。紧接着的两行表明,updateQuestion()方法是在onClick(...)实现方法里被调用的。点击该行链接跳转至注释掉的问题索引递增代码行。暂时不要修正,下一节还会使用设置断点调试的方法重新查找该问题。

记录栈跟踪日志虽然是个强大的工具,但也存在缺陷。比如,大量的日志输出很容易导致LogCat窗口信息混乱难读。此外,通过阅读详细直白的栈跟踪日志并分析代码意图,竞争对手可以轻易剽窃我们的创意。

另一方面,既然有可能从栈跟踪日志看出代码的真实意图,在网站http://stackoverflow.com或者论坛http://forums.bignerdranch.com上求助时,附上一段栈跟踪日志往往有助于解决问题。如果需要这样做,你可以直接从LogCat中复制并粘贴日志内容。

继续学习之前,先删除日志记录代码,如代码清单4-4所示。

代码清单4-4 再见,老朋友(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {
    ...
    private void updateQuestion() {
        Log.d(TAG, "Updating question text", new Exception());
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    }

4.1.3 设置断点

要使用Android Studio自带调试器调试上一节中的问题,首先要在updateQuestion()方法中设置断点,以确认该方法是否被调用。断点会在断点设置行的前一行处停止代码执行,然后我们可以逐行检查代码,看看接下来到底发生了什么。

在QuizActivity.java文件中,找到updateQuestion()方法,点击第一行代码左边的灰色栏区域。可以看到,灰色栏上出现了一个红色圆点。这就是已设置的一处断点,如图4-4所示。

{%}

图4-4 已设置的一处断点

为启用代码调试器并触发已设置的断点,我们需要调试运行而不是直接运行应用。要调试运行应用,单击Run按钮旁边的Debug按钮,或选择Run → Debug 'app' 菜单项。设备会报告说正在等待调试器加载,然后继续运行。

应用启动并加载调试器运行后,就会暂停。应用首先调用QuizActivity.onCreate(Bundle)方法,该方法又调用updateQuestion()方法,然后触发断点。

如图4-5所示,QuizActivity.java代码已经在代码编辑区打开了,断点设置所在行的代码也被加亮显示了。应用在断点处停止运行。

{%}

图4-5 代码在断点处停止执行

这时,由Frames和Variables视图组成的Debug工具窗口出现在屏幕底部,如图4-6所示。

{%}

图4-6 代码调试视图

使用视图顶部的箭头按钮可单步执行应用代码。从栈列表可以看出,updateQuestion()方法已经在onCreate(Bundle)方法中被调用了。不过,我们关心的是NEXT按钮被点击后的行为。因此,单击“继续运行”按钮。然后,再次点击GeoQuiz中的NEXT按钮,观察断点是否被激活(应该会激活)。

既然程序执行停在了断点处,就可以趁机看看其他视图。变量视图(Variables)可以让我们观察到程序中各对象的值。应该可以看到在QuizActivity中创建的变量,以及一个特别的this变量值(QuizActivity本身)。

展开this变量后可看到很多变量。它们是QuizActivity类的Activity超类、Activity超类的超类(一直追溯到继承树顶端)的全部变量。

我们只需关心mCurrentIndex变量值。在变量视图里滚动查看并找到mCurrentIndex。显然,它现在的值为0。

代码看上去没问题。为继续追查,需跳出当前方法。单击“单步执行跳出”按钮。

查看代码编辑视图,我们现在跳到了mNextButtonOnClickListener方法,正好是在updateQuestion()方法被调用之后。真是相当方便的调试,问题解决了。

接下来就是代码修正。不过,要修改代码,必须先停止调试应用。停止调试有以下两种方式:

  • 停止程序,单击图4-6所示的“停止”按钮;

  • 断开调试器,单击图4-6所示的“关闭”按钮。

回到代码编辑区,在OnClickListener方法中,取消对mCurrentIndex语句的注释,如代码清单4-5所示。

代码清单4-5 取消代码注释(QuizActivity.java)

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    mNextButton = (Button)findViewById(R.id.next_button);
    mNextButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
            updateQuestion();
        }
    });
    ...
}

至此,我们尝试了两种不同的代码跟踪调试方法:

  • 记录栈跟踪的诊断性日志;

  • 利用调试器设置断点调试。

哪种方式更好?没有肯定的答案,它们各有所长。实际体验之后,或许各有所爱吧。

栈跟踪记录的优点是,在同一日志记录中可以看到多处栈跟踪信息;缺点是,必须学习如何添加日志记录方法,重新编译、运行应用并跟踪排查应用问题。相对而言,代码调试的方法更为方便。应用以调试模式运行后,可在应用运行的同时,在不同的地方设置断点,寻找解决问题的线索。

4.1.4 使用异常断点

前面介绍的调试方法还不够用?那就试试使用调试器来捕捉异常吧。在QuizActivity.java中,注释掉一行代码,让应用崩溃,如代码清单4-6所示。

代码清单4-6 使GeoQuiz再次崩溃(QuizActivity.java)

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
    ...
    // mNextButton = (Button) findViewById(R.id.next_button);
    mNextButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
            updateQuestion();
        }
    });
    ...
}

选择Run → View Breakpoints...菜单项调出异常断点设置窗口,如图4-7所示。

{%}

图4-7 设置异常断点

可以看到,当前设置的断点都显示在左边窗口,选中先前设置的断点,点击删除按钮(–)删除。

可以使用该对话窗口设置新断点。这样,无论任何时候,只要应用抛出异常就可以触发该断点。如果需要,可限制断点仅针对未捕获的异常生效,也可以设置为对两种类型的异常(未捕获的和已捕获的异常)都生效。

单击新增断点按钮(+)设置一个新断点。选择下拉列表中的Java Exception Breakpoints选项。接下来选择要捕捉的异常类型。输入RuntimeException,按提示选择RuntimeException(java.lang)。RuntimeExceptionNullPointerExceptionClassCastException及其他常见异常的超类,因此该设置基本适用于所有异常。

点击Done按钮完成设置。调试GeoQuiz应用。这次,调试器很快就定位到异常抛出的代码行。真是太棒了。

异常断点影响极大,建议及时清除那些不需要的断点。否则,在调试的时候,如果我们不关心的一些系统框架代码或者其他地方发生异常,就会触发先前设置的断点。继续学习之前,删除刚才设置的断点。

取消对QuizActivity.java的代码注释,让GeoQuiz应用恢复正常。

4.2 Android特有的调试工具

总体来讲,Android应用调试和Java应用调试没什么两样。然而,Android也有其特有的应用调试场景,如应用资源问题。显然,Java编译器并不擅长处理此类问题。

4.2.1 使用Android Lint

该是Android Lint发挥作用的时候了。Android Lint是Android应用代码的静态分析器(static analyzer)。作为一个特殊程序,它能在不运行代码的情况下检查代码错误。凭着对Android框架的熟练掌握,Android Lint能深入检查代码,找出编译器无法发现的问题。在大多数情况下,Android Lint检查出的问题都值得重视。

在第6章,我们会看到Android Lint对设备兼容问题的警告。此外,Android Lint能够检查定义在XML文件中的对象类型。在QuizActivity.java中,人为制造一处错误,如代码清单4-7所示。

代码清单4-7 不匹配的对象类型(QuizActivity.java)

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate(Bundle) called");
    setContentView(R.layout.activity_quiz);
    ...
    mQuestionTextView = (TextView)findViewById(R.id.question_text_view);

    mTrueButton = (Button)findViewById(R.id.true_button);
    mTrueButton = (Button)findViewById(R.id.question_text_view);
    ...
}

因为使用了错误的资源ID,所以代码运行时,会尝试把TextView转换为Button类型,这会导致类型转换错误。显然,Java编译器无能为力,而Android Lint就能捕获到该错误。可以看到Lint立即高亮显示该行代码,指出问题。

假如想主动查看项目中的所有潜在问题,可以选择Analyze → Inspect Code...菜单项手动运行Lint。在被问及检查项目的哪部分时,选择Whole project。Android Studio会立即运行Lint和其他一些静态分析器开始分析代码。

检查完毕,所有的潜在问题会按类别列出。展开Android Lint类别,可看到具体的Lint信息,如图4-8所示。

{%}

图4-8 Lint警告信息

继续展开还可以看到更加详细的信息,包括问题发生的地方。

Mismatched view type错误是我们人为制造的。现在,恢复代码如初,如代码清单4-8所示。

代码清单4-8 修正类型不匹配的代码错误(QuizActivity.java)

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate(Bundle) called");
    setContentView(R.layout.activity_quiz);

    mQuestionTextView = (TextView)findViewById(R.id.question_text_view);

    mTrueButton = (Button)findViewById(R.id.question_text_view);
    mTrueButton = (Button)findViewById(R.id.true_button);
    ...
}

最后,重新运行GeoQuiz应用,确认已恢复。

4.2.2 R类的问题

对于引用还未添加的资源,或者删除仍被引用的资源而导致的编译错误,我们已经很熟悉了。通常,在添加资源或删除引用后重新保存文件,Android Studio会准确无误地重新编译项目。

不过,资源编译错误有时会一直存在或莫名其妙地出现。如遇这种情况,请尝试如下操作。

  • 重新检查资源文件中XML文件的有效性

    如果最近一次编译时未生成R.java文件,项目中资源引用的地方都会出错。通常,这是由某个布局XML文件中的拼写错误引起的。既然布局XML文件有时无法得到有效校验,拼写错误自然也就难以发现了。修正找到的错误并重新保存XML文件,Android Studio会生成新的R.java文件。

  • 清理项目

    选择Build → Clean Project菜单项。Android Studio会重新编译整个项目,消除错误。建议经常做深度项目清理。

  • 使用Gradle同步项目

    如果修改了build.gradle配置文件,就需要同步更新项目的编译设置。选择Tools → Android → Sync Project with Gradle Files菜单项,Android Studio会使用正确的项目设置重新编译项目。这会解决Gradle配置变更带来的问题。

  • 运行Android Lint

    仔细查看Lint警告信息,没准就会有新发现。

如果仍有资源相关问题或其他问题,建议仔细阅读错误提示并检查布局文件。慌乱时往往找不出问题。不妨休息冷静一下,再重新查看Android Lint报告的错误和警告,或许就能找出代码错误或拼写输入错误。

如果上述操作无法解决问题,或遇到其他Android Studio使用问题,还可以访问网站http://stackoverflow.com或本书论坛http://forums.bignerdranch.com求助。

4.3 挑战练习:探索布局检查器

为了调试布局文件,可使用布局检查器以交互的方式检查布局文件,研究它是如何在屏幕上渲染显示的。要使用布局检查器,首先在模拟器上启动GeoQuiz应用,然后在Android Monitor工具窗口点击最左边的布局检查器按钮,如图4-9所示。布局检查器激活后,点击布局检查器视图里的元素,就可以查看布局属性了。

图4-9 布局检查器按钮

4.4 挑战练习:探索内存分配跟踪

针对应用里内存分配的频率和次数,内存分配跟踪器能给出详细的跟踪报告。这大大方便了应用的性能优化。在Android Monitor工具窗口,点击内存分配跟踪器按钮启动它,如图4-10所示。

{%}

图4-10 启动内存分配跟踪器

随后,你在前台操作应用,后台就开始记录内存分配状况。一旦找到你想优化的点,就可以再次点击内存分配跟踪器按钮停止。内存分配报告随之展现,如图4-11所示。

{%}

图4-11 内存分配报告

内存分配报告会列出内存分配发生的次数以及字节大小。在工具顶端选择你需要的报表类型,报表展现形式可以是表,也可以是图。

目录

  • 版权声明
  • 献词
  • 致谢
  • 如何学习Android开发
  • 开发必备工具
  • 第 1 章 Android开发初体验
  • 第 2 章 Android与MVC设计模式
  • 第 3 章 activity的生命周期
  • 第 4 章 Android应用的调试
  • 第 5 章 第二个activity
  • 第 6 章 Android SDK版本与兼容
  • 第 7 章 UI fragment与fragment管理器
  • 第 8 章 使用RecyclerView显示列表
  • 第 9 章 使用布局与组件创建用户界面
  • 第 10 章 使用fragment argument
  • 第 11 章 使用ViewPager
  • 第 12 章 对话框
  • 第 13 章 工具栏
  • 第 14 章 SQLite数据库
  • 第 15 章 隐式intent
  • 第 16 章 使用intent拍照
  • 第 17 章 双版面主从用户界面
  • 第 18 章 应用本地化
  • 第 19 章 Android辅助功能
  • 第 20 章 数据绑定与MVVM
  • 第 21 章 音频播放与单元测试
  • 第 22 章 样式与主题
  • 第 23 章 XML drawable
  • 第 24 章 深入学习intent和任务
  • 第 25 章 HTTP与后台任务
  • 第 26 章 Looper、Handler和HandlerThread
  • 第 27 章 搜索
  • 第 28 章 后台服务
  • 第 29 章 broadcast intent
  • 第 30 章 网页浏览
  • 第 31 章 定制视图与触摸事件
  • 第 32 章 属性动画
  • 第 33 章 地理位置和Play服务
  • 第 34 章 使用地图
  • 第 35 章 material design
  • 第 36 章 编后语