第 4 章 Android应用的调试

第 4 章 Android应用的调试

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

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

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

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

    mQuestionTextView = (TextView)findViewById(R.id.question_text_view);
    // 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 DDMS工具窗口。上下滑动LogCat窗口滚动条,应该会看到如图4-2所示的整片红色的异常或错误信息。这就是标准的Android运行时的异常信息报告。如果看不到,可试着选择LogCat的No Filters过滤项。另外,还可以调整Log Level为Error,让系统只输出严重问题日志。

{%}

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

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

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

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

为修正该问题,取消对变量mQuestionTextView初始化语句的注释。

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

如果发生应用崩溃的设备没有连接到电脑上,日志信息也不会全部丢失。设备会将最近的日志保存到log文件中。日志文件的内容长度及保留的时间取决于具体的设备,不过,获取十分钟之内产生的日志信息通常是有保证的。只要将设备连上电脑,在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;
            // 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 for question #" + mCurrentIndex,
          new Exception());
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    }

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

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

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

{%}

图4-3 日志输出结果

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

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

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

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

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

public class QuizActivity extends AppCompatActivity {

    ...

    private void updateQuestion() {
        Log.d(TAG, "Updating question text for question #" + mCurrentIndex,
          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工具窗口出现在了Android Studio的底部,如图4-6所示。

{%}

图4-6 代码调试视图

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

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

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

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

代码看上去没问题。为继续追查,需跳出当前方法。单击Step Out按钮。

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

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

  • 停止程序,单击图4-6所示的Stop按钮。

  • 断开调试器,单击图4-6所示的Close按钮。

回到代码编辑区,在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;
            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 = (Button) findViewById(R.id.next_button);
    mNextButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
            updateQuestion();
        }
    });
    ...
}

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

{%}

图4-7 设置异常断点

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

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

单击新增断点按钮(+)设置一个新断点。选择下拉列表中的Java Exception Breakpoints选项。接下来选择要捕捉的异常类型。输入RuntimeException,按提示选择RuntimeException (java.lang)。点击Done按钮完成断点设置。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() 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,代码运行时,会导致TextViewButton对象间的类型转换出现错误。显然,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() 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);

    ...
}

最后,重新运行应用,确认问题已得到修正。

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寻求帮助。

目录

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