第 5 章 5.1 异常处理 异常 (Exception) 指程序运行过程中出现的非正常现象, 例如用户输入错误 需要处理的文件不存在 在网络上传输数据但网络没有连接等 由于异常情况总是可能发生, 良好健壮的应用程序除了具备用户所要求的基本功能外, 还应该具备预见并处理可能发生的各种异常的功能 所以, 开发应用程序时要充分考虑到各种可能发生的异常情况, 使程序具有较强的容错能力 通常把这种对异常情况进行处理的技术称为异常处理 在 Android 系统中应用 Java 语言的异常处理机制进行异常处理 1. 异常处理机制在 Android 系统的异常处理中, 引入了一些用来描述和处理异常的 Java 类, 每个异常类反映一类运行错误, 在类的定义中包含了该类异常的信息和对异常进行处理的方法 当程序运行过程中发生某个异常现象时, 系统会产生一个与之相对应的异常类对象, 并交由系统中的相应机制进行处理, 以避免系统崩溃或其他对系统有害的结果发生, 保证了程序运行的安全性 这就是 Android 系统的异常处理机制 2. 异常类的定义在 Android 系统中, 按 Java 语言对异常的分类, 把异常分为错误 (Error) 与异常 (Exception) 两大类 错误 (Error) 通常是指程序本身存在非法的情形, 这些情形常常是因为代码存在问题而引起的 而且, 编程人员可以通过对程序进行更加仔细的检查, 把这种错误的情形减到最小 从理论上讲, 这些情形可以避免 异常情况 (Exception) 表示另一种 非同寻常 的错误 这种错误通常是不可预测的 常见的异常情况包括内存不足 找不到所需的文件等 Throwable 类派生了两个子类 :Exception 和 Error 其中,Error 类描述内部错误, 它由系统保留, 程序不能抛出这个类型的对象,Error 类的对象不可捕获 不可以恢复, 出错时系统通知用户并终止程序 ; 而 Exception 类则供应程序使用 所有的 Android 异常类都是系统类库中 Exception 类的子类 同其他类一样,Exception 类有自己的方法和属性 它的构造方法有两个 : public Exception(); public Exception(String s);
第二个构造方法接受字符串参数传入的信息, 该信息通常是对异常所对应的错误的描述 Exception 类从父类 Throwable 那里还继承了若干种方法, 其中常用的方法有 : (1)public String tostring() tostring() 方法返回描述当前 Exception 类信息的字符串 (2)public void printstacktrace() printstacktrace() 方法没有返回值, 它的功能是完成打印操作, 在当前的标准输出 ( 一般是屏幕 ) 上打印输出当前异常对象的堆栈使用轨迹, 即程序先后调用执行了哪些对象, 或类的哪些方法使得运行过程中产生了这个异常对象 3. 系统定义的运行异常 Exception 类有若干个子类, 每个子类代表一种特定的运行错误 有些子类是系统事先定义的, 并包含在 Android 系统的 Java 类库中, 称为系统定义的运行异常 系统定义的运行异常通常对应系统运行错误 由于这种错误可能导致操作系统错误甚至是整个系统的瘫痪, 所以需要定义异常类来特别处理 表 5-1 中列出了若干常见的系统定义异常 表 5-1 Android 系统引用 Java 定义的运行异常类系统定义的运行异常说明 ClassNotFoundException ArrayIndexOutOfBoundsException FileNotFoundException IOException NullPointerException 找不到要装载的类, 由 Class.forName 抛出数组下标出界找不到指定的文件或目录输入 / 输出错误非法使用空引用 ArithmeticException 算术错误, 如除数为 0 InterruptedException UnknownHostException SecurityException MalfomedURLException 一个线程被另一个线程中断无法确定主机的 IP 地址安全性错误 URL 格式错误 由于定义了相应的异常, 程序即使产生某些致命的错误, 如应用空对象等, 系统也会自动产生一个对应的异常对象来处理和控制这个错误, 避免其蔓延或产生更大的问题 例 5-1 异常处理示例: 当除数为 0 时, 抛出异常 1 package com.ex05_01; 2 import android.app.activity; 3 import android.os.bundle; 4 import android.app.progressdialog; 5 import android.widget.textview; 6 import android.widget.toast; 7 8 public class MainActivity extends Activity { 9 TextView txt1; 10 ProgressDialog log; 11 Toast toast; 125 第 5 章
Android 应用程序设计 ( 修订版 ) 126 12 public void oncreate(bundle savedinstancestate) { 13 super.oncreate(savedinstancestate); 14 txt1=new TextView(this); 15 int x=15,y=0,z; 16 String c=" "; 17 txt1.settext(" 运行结果 "); 18 try{ z=x/y; c=string.valueof(z); } 19 catch(exception e) 20 { c="x=15, y=0, z=x/y 错误, 除数不能为 0!";} 21 toast=toast.maketext(this, c, Toast.LENGTH_LONG); 22 toast.settext(c); 23 toast.show(); 24 setcontentview(txt1); 25 } 26 } 程序的运行结果如图 5.1 所示 Toast 使用静态方法 图 5.1 除数为 0 时抛出异常 5.2 多线程 5.2.1 线程与多线程 1. 线程概述线程是指进程中单一顺序的执行流 设某程序的地址空间在 0x0000~0xffff, 其线程 A 运行在 0x2000~0x4000, 线程 B 运行在 0x4001~0x6000, 线程 C 运行在 0x6001~ 0x8000, 多个线程共同构成一个大的进程, 如图 5.2 所示 线程间的通信非常简单而有效, 上下文切换非常快, 它们是同一个进程中其中两部分之间所进行的切换 每个线程彼此独立执行, 一个程序可以同时使用多个线图 5.2 每个线程彼此独立, 但有公共数据区程来完成不同的任务 一般用户在使用多线程时并不需要考虑底层处理的详细细节
2. 多线程概述多线程是指一个程序中包含有多个执行流, 多线程是实现并发机制的一种有效手段 例如, 在传统的单进程环境下, 用户必须等待一个任务完成后才能进行下一个任务 即使大部分时间空闲, 也只能按部就班地工作 而多线程可以避免用户的等待 又如, 传统的并发服务器是基于多线程机制的, 每个客户需要一个进程, 而进程的数目是受操作系统限制的 基于多线程的并发服务器, 每个客户一个线程, 多个线程可以并发执行 进程与多线程的区别如图 5.3 所示 图 5.3 进程与多线程的区别 从图中可以看到, 多任务状态下各进程的内部数据和状态都是完全独立的, 而多线程共享一块内存空间和一组系统资源, 有可能相互影响 5.2.2 线程的生命周期 每个线程都要经历创建 就绪 运行 阻塞和死亡 5 个状态, 线程从产生到消失的状态变化过程称为生命周期 线程的生命周期如图 5.4 所示 1. 创建状态当通过 new 命令创建了一个线程对象后, 该线程对象就处于创建状态 如下面语句 : Thread thread1=new Thread(); 创建状态是线程已被创建但未开始执行的一个特殊状态 此时, 线程对象拥有自己的内存空间, 但没有分配 CPU 资源, 需通过 start() 方法调度进入就绪状态等待 CPU 资源 2. 就绪状态 图 5.4 线程的生命周期 处于创建状态的线程对象通过 start() 方法进入就绪状态 如下面语句 : Thread thread1=new Thread(); Thread1.start(); 127 第 5 章
Android 应用程序设计 ( 修订版 ) 128 start() 方法同时调用了线程体, 也就是 run() 方法, 表示线程对象正等待 CPU 资源, 随时可被调用执行 处于就绪状态的线程已经被放到某一队列等待系统为其分配对 CPU 的控制权 至于何时真正地执行, 取决于线程的优先级以及队列的当前状况 3. 运行状态若线程处于正在运行的状态, 表示线程已经拥有了对处理器的控制权, 其代码目前正在运行, 除非运行过程中控制权被另一优先级更高的线程抢占, 否则这一线程将一直持续到运行完毕 4. 阻塞状态如果一个线程处于阻塞状态, 那么该线程将无法进入就绪队列 处于阻塞状态的线程通常必须由某些事件唤醒 至于是何种事件, 则取决于阻塞发生的原因 例如 : 处于休眠中的线程必须被阻塞固定的一段时间唤醒 ; 被挂起或处于消息等待状态的线程则必须由一外来事件唤醒 5. 死亡状态死亡状态 ( 或终止状态 ) 表示线程已退出运行状态, 并且不再进入就绪队列 其原因可能是线程已执行完毕 ( 正常结束 ), 也可能是该线程被另一线程强行中断, 即线程自然撤销或被停止 自然撤销是从线程的 run() 方法正常退出 即, 当 run() 方法结束后, 该线程自然撤销 调用 stop() 方法可以强行停止当前线程 但这个方法已在 JDK2 中作废, 应当避免使用 如果需要线程死亡, 可以进行适当的编码触发线程提前结束 run() 方法, 使其自行消亡 简单归纳一下, 一个线程的生命周期一般经过以下几个步骤 : (1) 一个线程通过 new() 操作实例化后, 进入新生状态 (2) 通过调用 start() 方法进入就绪状态, 一个处在就绪状态的线程将被调度执行, 执行该线程相应的 run() 方法中的代码 (3) 通过调用线程的 ( 或从 Object 类继承过来的 )sleep() 或 wait() 方法, 这个线程进入阻塞状态 一个线程可能自己完成阻塞操作 (4) 当 run() 方法执行完毕, 或者有一个例外产生, 或者执行 System.exit() 方法, 则一个线程就会进入死亡状态 5.2.3 线程的数据通信 1. 消息 (Message) 在 Android 的多线程中, 把需要传递的数据称为消息 由于 Android 的用户界面 UI(User Interface) 是单线程的, 如果 UI 线程花费太多的时间做后台的事情, 超过 5 秒钟,Android 就会给出错误提示 因此, 为了避免 拖累 UI, 一些较费时的工作应该交给独立的后台线程去执行 但是如果后台的线程直接执行 UI 对象,Android 会发出错误信息, 所以,UI 线程与后台线程需要进行消息通信 UI 线程将工作分配给后台线程, 后台线程执行后将相应的状态消息返回给 UI 线程, 让 UI 线程对 UI 完成相应地更新 Message 是一个描述消息的数据结构类,Message 包含很多成员变量和方法 消息的
常用方法见表 5-2 表 5-2 消息 (Message) 的常用方法方法说明 Message() gettarget() settarget(handler target) sendtotarget() int arg1 int arg2 int what 创建消息对象的构造方法获取将接收此消息的 Handler 对象, 此对象必须实现 Handler.handleMessage() 方法设置接收此消息的 Handler 对象向 Handler 对象发送消息用于当仅需要存储几个整型数据消息时用于当仅需要存储几个整型数据消息时用户自定义消息标识, 避免各线程的消息冲突 2. 消息处理工具 Handler Handler 是 Android 中多个线程间消息传递和定时执行任务的 工具 类 Handler 是消息的处理者, 负责在多个线程之间发送 Message 和处理 Message Handler 类在多线程中有两个方面的应用 : 发送消息 在不同的线程间传递数据, 使用的方法为 send () 定时执行任务 在指定的未来某时间执行某任务, 使用的方法为 post () 一个线程只能有一个 Handler 对象, 通过该对象向所在线程发送消息 Handler 除了给其他线程发送消息外, 还可以给本线程发送消息 Handler 类的常用方法见表 5-3 表 5-3 Handler 类的常用方法方法说明 Handler() handlemessage(message msg) sendemptymessage(int) sendmessage(message) sendmessageattime(message,long) sendmessagedelayed(message,long) post(runnable) postattime(runnable,long) postdelayed(runnable,long) Handler 对象的构造方法 Handler 的子类必须使用该方法接收消息发送一个空的消息发送消息, 消息中可携带参数在未来某一时间点发送消息延时 N 毫秒发送消息提交计划任务马上执行提交计划在未来的时间点执行提交计划任务延时 N 毫秒执行 应用 Handler 对象处理线程发送消息的一般形式如下 (1) 在线程的 run() 方法中发送消息 : public void run() { Message msg=new Message(); 129 第 5 章
Android 应用程序设计 ( 修订版 ) 130 // 消息标志 msg.what=1; // 由 Handler 对象发送这个消息 handler.sendmessage(msg); } (2) 用 Handler 对象处理消息 : private class mhandler extends Handler { public void handlemessage(message msg) { switch(msg.what) { case 1:... case 2:... } } } 其中,handleMessage(Message msg) 的参数 msg 是接收多线程 run() 方法中发送的 Message 对象,msg.what 为消息标志 5.2.4 创建线程 1. 创建线程的两种方式在 Android 中创建线程的方法与在 Java 中创建线程的方法相同, 可采用两种方式创建线程 : (1) 通过创建 Thread 类的子类来构造线程 Java 定义了一个直接从根类 Object 中派生的 Thread 类, 所有从这个类派生的子类或间接子类均为线程 (2) 通过实现一个 Runnable 接口的类来构造线程 注意,Android 中的线程抛弃了 Java 线程中一些不安全的做法 例如, 在 Java 中终止一个 Thread 线程, 可以调用 stop() destroy() 等方法实现, 但在 Android 中, 这些方法都不能实现, 故不能直接使用 2. 创建 Thread 子类构造线程可以通过继承 Thread 类建立一个 Thread 类的子类并重新设计 ( 重载 ) 其 run() 方法来构造线程 Thread 类是用来创建一个新线程的类 使用该类的方法, 可以处理线程的优先级和改变线程的状态 要创建和执行一个线程需完成下列步骤 : (1) 创建一个 Thread 类的子类 ; (2) 在 Thread 子类中重新定义自己的 run() 方法, 在这个 run() 方法中包含线程要实现的操作, 并通过 Handler 对象发送 Message 消息 ; (3) 用关键字 new 创建一个线程对象 ;
(4) 调用 start() 方法启动线程 线程启动后当执行 run() 方法完毕时, 会自然进入终止状态 例 5-2 创建一个多线程 Thread 类的子类, 每隔 1 秒钟发送一个信号给主程序, 主程序进行计数 设计思路 : (1) 创建多线程 Thread 子类 mthread 在 run() 方法中通过 Thread.skeeo(1000) 延时 1 秒钟, 实现每隔 1 秒钟发送一次 Message 消息的功能 (2) 创建 Handler 的子类 mhandler, 在 handlemessage(message nsg) 方法中接收多线程发送的 Message 消息 (3) 每接收一次消息, 计数器加 1, 并显示出来 新建工程 ex05_02, 在界面布局文件 activity_main.xml 中安排一个 启动线程 按钮和一个 停止线程 按钮, 再安排一个文本标签, 用于显示计数 界面布局如图 5.5 所示 图 5.5 线程每隔 1 秒钟向 (1) 界面布局文件 activity_main.xml 的代码如下 : 主程序发送一个信号 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="fill_parent" 4 android:layout_height="fill_parent" 5 android:background="@color/white" 6 android:orientation="vertical" > 7 <LinearLayout 8 android:layout_width="fill_parent" 9 android:layout_height="wrap_content" 10 android:gravity="center_horizontal" 11 android:orientation="horizontal" > 12 <Button 13 android:id="@+id/mbutton" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 android:text=" 启动线程 " 17 android:textsize="20px" /> 18 <Button 19 android:id="@+id/sbutton" 20 android:layout_width="wrap_content" 21 android:layout_height="wrap_content" 22 android:text=" 停止线程 " 23 android:textsize="20px" /> 24 </LinearLayout> 25 <TextView 26 android:id="@+id/txt" 27 android:layout_width="wrap_content" 131 第 5 章
Android 应用程序设计 ( 修订版 ) 132 28 android:layout_height="wrap_content" 29 android:layout_centerhorizontal="true" 30 android:layout_centervertical="true" 31 android:padding="@dimen/padding_medium" 32 android:text="@string/hello_world" 33 tools:context=".mainactivity" 34 android:textsize="24dp"/> 35 </LinearLayout> (2) 控制文件 MainActivity.java 的代码如下 : 1 package com.example.ex05_02; 2 import android.os.bundle; 3 import android.os.handler; 4 import android.os.message; 5 import android.app.activity; 6 import android.view.view; 7 import android.view.view.onclicklistener; 8 import android.widget.button; 9 import android.widget.textview; 10 11 public class MainActivity extends Activity 12 { 13 private boolean STOP=true; 定义线程是否停止的标志位 14 private int count=0 定义按秒计时的计数器 15 private mhandler handler; 16 private mthread thread; 17 private Button mbutton, sbutton; 18 private TextView mtextview; 19 @Override 20 public void oncreate(bundle savedinstancestate) 21 { 22 super.oncreate(savedinstancestate); 23 setcontentview(r.layout.activity_main); 24 handler=new mhandler(); 25 thread=new mthread(); 26 mtextview=(textview)findviewbyid(r.id.txt); 27 mbutton=(button)findviewbyid(r.id.mbutton); 28 mbutton.setonclicklistener(new mclick()); 29 sbutton=(button)findviewbyid(r.id.sbutton); 30 sbutton.setonclicklistener(new mclick()); 31 } 32 // 定义监听按钮的事件, 启动线程或停止线程 33 class mclick implements OnClickListener 34 { 35 @Override 36 public void onclick(view arg0)
37 { 38 if(arg0==mbutton) 39 { 40 // 设置标志位 41 STOP = false; 处理 启动线程 按钮事件 42 // 开启新的线程 43 thread.start(); 44 } 45 else if(arg0==sbutton) 46 { 47 STOP = true; 设置标志位 处理 停止线程 按钮事件 48 } 49 } 50 } 51 // 定义 Handler 的子类接收和处理线程发送来的消息 52 private class mhandler extends Handler 53 { 54 public void handlemessage(message msg) 55 { 56 switch(msg.arg1) 以消息标志为条件 57 { 58 case 1: 消息标志为 1 时, 执行本复合语句 59 { 60 count++; 秒数增加 61 mtextview.settext(integer.tostring(count)); 数值转换字符串 62 break; 63 } 64 } 65 } 66 } 67 // 定义 Thread 子类, 实现每隔 1 秒钟发送一次消息的功能 68 private class mthread extends Thread 69 { 70 @Override 71 public void run() 线程启动时执行这个函数 72 { 73 while(!stop) 一直循环, 直到标志位为 真 74 { 75 try{ 76 Thread.sleep(1000); 延时 1 秒 77 } catch(interruptedexception e){ 78 e.printstacktrace(); 79 } 80 Message msg = new Message(); 81 msg.arg1=1; 消息标志 133 第 5 章
Android 应用程序设计 ( 修订版 ) 134 82 handler.sendmessage(msg); 发送消息 83 } 84 } 85 } 86 } 3. 实现 Runnable 接口构造线程 Runnable 接口是在程序中使用线程的另一种方法 在许多情况下, 一个类已经继承了 父类, 因而这样的类不能再继承 Thread Runnable 接口为一个类提供了一种手段, 无须扩 展 Thread 类就可以执行一个新的线程或者被一个新的线程控制 这就是通过建立一个实现 Runnable 接口的对象, 并以它作为线程的目标对象来构造线程 它打破了单一继承方式的 限制 在 Java 语言的代码中,Runnable 接口只包含一个抽象方法, 其定义如下 : public interface Runnable { public abstract void run(); } 因此, 一个类实现 Runnable 接口时需要实现多线程的 run() 方法 为了实现 Runnable 对象的线程, 可使用下列方法来生成 Thread 对象 : Thread(Runnable 对象名 ); Thread(Runnable 对象名,String 线程名 ); 例 5-3 应用多线程设计一个小球移动的程序 设计一个绘制小球图形的 ballview, 并由一个线程来控制小球运动 由于 ballview 要继承 View, 因此, 要实现多线程就需要使用 Runnable 接口 关于图形的绘制, 在第 6 章有详细讲解, 这里不作具体说明 (1) 控制文件 MainActivity.java 的代码如下 : 1 package com.example.ex05_03; 2 import android.app.activity; 3 import android.os.bundle; 4 import android.os.handler; 5 import android.os.message; 6 import android.view.view; 7 import android.view.view.onclicklistener; 8 import android.widget.button; 9 10 public class MainActivity extends Activity 11 { 12 int i=80, j=10, step; i j 为小球坐标位置 ;step 为移动的步长值 13 ballview view; 14 Button btn; 15 Handler handler; 16 Thread thread; 17 boolean STOP=true; 线程是否停止标志 18 public void oncreate(bundle savedinstancestate)
19 { 20 super.oncreate(savedinstancestate); 21 setcontentview(r.layout.main); 22 view=(ballview)findviewbyid(r.id.view1); 23 btn=(button)findviewbyid(r.id.btn1); 24 btn.setonclicklistener(new mclick()); 25 handler=new mhandler(); 26 thread=new mthread(); 27 view.setxy(i, j); 28 } 29 class mclick implements OnClickListener 30 { 31 @Override 32 public void onclick(view arg0) 33 { 34 STOP=false; 设置线程中循环的标志位 35 thread.start(); 开启新线程 36 } 37 } 38 private class mhandler extends Handler 39 { 40 public void handlemessage(message msg) 41 { 42 switch(msg.what) 以消息标志为条件 43 { 44 case 1: 消息标志为 1 时, 执行下面的复合语句 45 { 46 step=step+5; 47 j=j+step; 48 if(j>220) STOP=true; 设置停止线程中循环的标志位 49 break; 50 } 51 } 52 view.setxy(i, j); 53 view.invalidate(); 更新小球的坐标位置 54 } 55 } 56 private class mthread extends Thread 57 { 58 @Override 59 public void run() 线程启动时执行 run() 函数 60 { 61 while(!stop) 一直循环, 直到标志位为 真 62 { 63 try 135 第 5 章
Android 应用程序设计 ( 修订版 ) 136 64 { 65 Thread.sleep(500); 延时 0.5 秒 66 } 67 catch(interruptedexception e) 68 { 69 e.printstacktrace(); 70 } 71 Message msg=new Message(); 72 msg.what=1; 消息标志 发送消息 73 handler.sendmessage(msg); 74 } 75 } 76 } 77 } (2) 绘制图形文件 ballview.java 的代码如下 : 1 package com.example.ex05_03; 2 import android.content.context; 3 import android.graphics.canvas; 4 import android.graphics.color; 5 import android.graphics.paint; 6 import android.util.attributeset; 7 import android.view.view; 8 9 public class ballview extends View 10 { 11 int x, y; 12 public ballview(context context, AttributeSet attrs) 13 { 14 super(context, attrs); 15 } 16 void setxy(int _x, int _y) 17 { 18 x=_x; 19 y=_y; 20 } 21 protected void ondraw(canvas canvas) 22 { 23 super.ondraw(canvas); 24 canvas.drawcolor(color.cyan); 25 Paint paint = new Paint(); 26 paint.setcolor(color.black); 27 paint.setantialias(true); 绘制小球 28 canvas.drawcircle(x, y, 15, paint); 29 paint.setcolor(color.white); 30 canvas.drawcircle(x-6, y-6, 3, paint); 31 } 32 }
(3) 用户界面设计 在用户界面程序中, 除设置一个按钮组件之外, 还设置了自定义的 ballview 组件 设置自定义的组件时, 要注意添加包路径 com.example.ex05_03.ballview 其代码如下: 1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="fill_parent" 4 android:layout_height="fill_parent" 5 android:orientation="vertical" > 6 <Button 7 android:id="@+id/btn1" 8 android:layout_width="80dp" 9 android:layout_height="wrap_content" 10 android:text=" 开始 " /> 11 <com.example.ex05_03.ballview 12 android:id="@+id/view1" 13 android:layout_width="fill_parent" 定义自定义组件 ballview 14 android:layout_height="fill_parent" /> 15 </LinearLayout> 程序的运行结果如图 5.6 所示 该示例说明了处理异步更新用户界面的方法 图 5.6 由线程控制小球运动 习题 5 设计两个独立线程, 分别计数, 如图 5.7 所示 图 5.7 设计两个独立运行的线程 137 第 5 章