java调试体系之Java 调试接口(JDI)

JDI 简介

JDI(Java Debug Interface)是 JPDA 三层模块中*高层的接口,定义了调试器(Debugger)所需要的一些调试接口。基于这些接口,调试器可以及时地了解目标虚拟机的状态,例如查看目标虚拟机上有哪些类和实例等。另外,调试者还可以控制目标虚拟机的执行,例如挂起和恢复目标虚拟机上的线程,设置断点等。

目前,大多数的 JDI 实现都是通过 Java 语言编写的。比如,Java 开发者再熟悉不过的 Eclipse IDE,它的调试工具相信大家都使用过。它的两个插件 org.eclipse.jdt.debug.ui 和 org.eclipse.jdt.debug 与其强大的调试功能密切相关,其中 org.eclipse.jdt.debug.ui 是 Eclipse 调试工具界面的实现,而 org.eclipse.jdt.debug 则是 JDI 的一个完整实现。

JDI 工作方式

首先,调试器(Debuuger)通过 Bootstrap 获取唯一的虚拟机管理器,见 清单 1。

清单 1. 获取虚拟机管理器(VirtualMachineManager)

1

2

VirtualMachineManager virtualMachineManager

    = Bootstrap.virtualMachineManager();

虚拟机管理器将在*次被调用时初始化可用的链接器。一般地,调试器会默认地采用启动型链接器进行链接,见 清单 2。

清单 2. 获取默认的链接器(Connector)

1 LaunchingConnector defaultConnector = virtualMachineManager.defaultConnector();

然后,调试器调用链接器的 launch () 来启动目标程序,并完成调试器与目标虚拟机的链接,见 清单 3。

清单 3. 启动目标程序,连接调试器(Debuuger)与目标虚拟机(VirtualMachine)

1 VirtualMachine targetVM = defaultConnector.launch(arguments);

当链接完成后,调试器与目标虚拟机便可以进行双向通信了。调试器将用户的操作转化为调试命令,命令通过链接被发送到前端运行目标程序的虚拟机上;然后,目标虚拟机根据接受的命令做出相应的操作,将调试的结果发回给后端的调试器;*后,调试器可视化数据信息反馈给用户。

从功能上,可以将 JDI 分成三个部分:数据模块,链接模块,以及事件请求与处理模块。数据模块负责调试器和目标虚拟机上的数据建模,链接模块建立调试器与目标虚拟机的沟通渠道,事件请求与处理模块提供调试器与目标虚拟机交互方式,下面将逐一地介绍它们。

JDI 数据模块

Mirror

Mirror 接口是 JDI *底层的接口,JDI 中几乎所有其他接口都继承于它。镜像机制是将目标虚拟机上的所有数据、类型、域、方法、事件、状态和资源,以及调试器发向目标虚拟机的事件请求等都映射成 Mirror 对象。例如,在目标虚拟机上,已装载的类被映射成 ReferenceType 镜像,对象实例被映射成 ObjectReference 镜像,基本类型的值(如 float 等)被映射成 PrimitiveValue(如 FloatValue 等)。被调试的目标程序的运行状态信息被映射到 StackFrame 镜像中,在调试过程中所触发的事件被映射成 Event 镜像(如 StepEvent 等),调试器发出的事件请求被映射成 EventRequest 镜像(如 StepRequest 等),被调试的目标虚拟机则被映射成 VirtualMachine 镜像。但是,JDI 并不保证目标虚拟机上的每份信息和资源都只有唯一的镜像与之对应,这是由 JDI 的具体实现所决定的。例如,目标虚拟机上的某个事件有可能存在多个 Event 镜像与之对应,例如 BreakpointEvent 等。

Mirror 实例或是由调试器创建,或是由目标虚拟机创建,调用 Mirror 实例 virtualMachine() 可以获取其虚拟机信息,如下所示。

清单 4. 获取 Mirror 对象实例的虚拟机

1 VirtualMachine virtualMachine = mirror.virtualMachine();

返回的目标虚拟机对象实现了 VirtualMachine 接口,该接口提供了一套方法,可以用来直接或间接地获取目标虚拟机上所有的数据和状态信息,也可以挂起、恢复、终止目标虚拟机,详情见 图 1。

图 1. 虚拟机接口

图 1. 虚拟机接口

这样,调试器便可以获取目标虚拟机上的信息,维持与目标虚拟机间的通信,并且检查,修改和控制目标虚拟机上资源等。

Value 和 Type

Value 和 Type 接口分别代表着目标虚拟机中对象、实例变量和方法变量的值和类型。通过 Value 接口的 type(),可以获取该值对应的类型。JDI 中定义了两种基本的数据类型:原始类型(PrimitiveType)和引用类型(ReferenceType)。与其对应的数值类型分别是原始值(PrimtiveValue)和对象引用(ObjectReference)。Value 和 Type 的具体对应关系,请参见 表 1。

表 1. JDI 中 Value-Type 的对照表

(Value, Type) 说明
(PrimtiveValue, PrimtiveType) (ByteValue, ByteType) 表示一个字节
(CharValue, CharType) 表示一个字符
(ShortValue, ShortType) 表示一个短整型数据
(IntegerValue, IntegerType) 表示一个整型数据
(LongValue, LongType) 表示一个长整型数据
(FloatValue, FloatType) 表示一个浮点型数据
(DoubleValue, DoubleType) 表示一个双精度浮点型数据
(BooleanValue, BooleanType) 表示一个布尔型数据
(ObjectReference, ReferenceType) (ObjectReference, ReferenceType) 表示目标虚拟机上的一个对象
(ArrayReference, ArrayType) 表示目标虚拟机上的一个数组
(StringReference, ClassType) 表示目标虚拟机上的一个字符串对象
(ThreadReference, ClassType) 表示目标虚拟机上的一个线程对象,有一套方法可以获得当前设置的断点,堆栈,也能挂起和恢复该线程等
(ThreadGroupReference, ClassType) 表示目标虚拟机上的一个线程组对象
(ClassObjectReference, ClassType) 表示目标虚拟机上的一个类的 java.lang.Class 实例
(ClassLoaderReference, ClassType) 表示目标虚拟机上的一个 ClassLoader 对象
(VoidValue, VoidType) 表示 void 类型

PrimitiveType 包括 Java 的 8 种基本类型,ReferenceType 包括目标虚拟机中装载的类,接口和数组的类型(数组也是一种对象,有自己的对象类型)。ReferenceType 有三种子接口:ClassType 对应于加载的类,InterfaceType 对应于接口,ArrayType 对应于数组。另外,ReferenceType 还提供了一组方法,可以用来获取该类型中声明的所有变量、方法、静态变量的取值、内嵌类、运行实例、行号等信息。

PrimtiveValue 封装了 PrimitiveType 的值,它提供一组方法可将 PrimtiveValue 转化为 Java 原始数据。例如,IntegerValue 的 value () 将返回一个 int 型数据。对应地,VirtualMachine 也提供了一组方法,用以将 Java 原始数据转化为 PrimtiveValue 型数据。例如 mirrorOf(float value) 将给定的 float 数据转化为 FloatValue 型数据。

ObjectReference 封装了目标虚拟机中的对象,通过 getValue() 和 setValue() 方法可以访问和修改对象中变量的值,通过 invokeMethod() 可以调用该对象中的指定方法,通过 referringObjects() 可以获得直接引用该对象的其他对象,通过 enableCollection() 和 disableCollection() 可以允许和禁止 GC 回收该对象。

TypeComponent

TypeComponent 接口表示 Class 或者 Interface 所声明的实体(Entity),它是 Field 和 Method 接口的基类。Field 表示一个类或者实例的变量,调用其 type() 可返回域的类型。Method 表示一个方法。TypeComponent 通过方法 declaredType() 获得声明该变量或方法的类或接口,通过 name() 获得该变量或者方法的名字(对于 Field 返回域名,对于一般方法返回方法名,对于类构造函数返回 <init>,对于静态初始化构造函数返回 <clinit>)。

JDI 的链接模块

链接是调试器与目标虚拟机之间交互的渠道,一次链接可以由调试器发起,也可以由被调试的目标虚拟机发起。一个调试器可以链接多个目标虚拟机,但一个目标虚拟机*多只能链接一个调试器。链接是由链接器(Connector)生成的,不同的链接器封装着不同的链接方式。JDI 中定义三种链接器接口,分别是依附型链接器(AttachingConnector)、监听型链接器(ListeningConnector)和启动型链接器(LaunchingConnector)。在调试过程中,实际使用的链接器必须实现其中一种接口。

根据调试器在链接过程中扮演的角色,可以将链接方式划分为主动链接和被动链接。主动链接是较常见一种链接方式,表示调试器主动地向目标虚拟机发起链接。下面将举两个主动链接的例子:

由调试器启动目标虚拟机的链接方式:这是*常见、*简单的一种链接方式。

  • 调试器调用 VirtualMachineManager 的 launchingConnectors() 方法获取所有的启动型链接器实例;
  • 根据传输方式或其他特征选择一个启动型链接器,调用其 launch() 方法启动和链接目标虚拟机;
  • 启动后,返回目标虚拟机的实例。

更高级的,当目标虚拟机已处于运行状态时,可以采用调试器 attach 到目标虚拟机的链接方式:

  • 目标虚拟机必须以 -agentlib:jdwp=transport=xxx,server=y 参数启动,并根据传输方式生成监听地址;(其中,xxx 是传输方式,可以是 dt_socket 和 share_memory)
  • 调试器启动,调用 VirtualMachineManager 的 attachingConnectors() 方法获取所有的依附型链接器实例;
  • 根据目标虚拟机采用的传输方式选择一个依附型链接器,调用其 attach() 方法依附到目标虚拟机上;
  • 完成链接后,返回目标虚拟机的实例。

被动链接表示调试器将被动地等待或者监听由目标虚拟机发起的链接,同样也举两个被动链接的例子:

目标虚拟机 attach 到已运行的调试器上的链接方式:

  • 调试器通过 VirtualMachineManager 的 listeningConnectors() 方法获取所有的监听型链接器实例;
  • 为每种传输类型分别选定一个链接器,然后调用链接器的 startListening() 方法让链接器进入监听状态;
  • 通过 accept() 方法通知链接器开始等待正确的入站链接,该方法将返回调试器正在监听的地址描述符;
  • 终端用户以 -agentlib:jdwp=transport=xxx,address=yyy 参数启动目标虚拟机(其中,yyy 是调试器的监听地址);
  • 目标虚拟机会自动地 attach 到调试器上建立链接,然后返回目标虚拟机的实例。

即时(Just-In-Time)链接方式:

  • 以 -agentlib:jdwp=launch=cmdline,onuncaught=y,transport=xxx,server=y 参数启动目标虚拟机;
  • 虚拟机将抛出一个未捕获的异常,同时生成特定于 xxx 传输方式的监听地址,用于确立一次链接;
  • 目标虚拟机启动调试器,并告知调试器传输方式和监听地址;
  • 启动后,调试器调用 VirtualMachineManager 的 attachingConnectors() 方法获取所有依附型链接器实例;
  • 根据指定的 xxx 传输方式,选择一个链接器;
  • 调用链接器的 attach 方法依附到对应地址的目标虚拟机上;
  • 完成链接后,返回目标虚拟机的实例。

Connector.Argument 是 Connector 的内嵌接口,表示链接器的一个参数,不同类型的链接器支持不同的链接器参数,LaunchingConnector 支持 home,main,suspend 等,AttachingConnector 和 ListeningConnector 支持 timeout,hostname,port 等参数,见 表 2。

表 2. 常见的链接器参数

Connector 类型 参数名称 说明
LaunchingConnector home 表示 java.home 的值,指向 JRE
main 表示所要执行的 Java 类的类名
options 表示使用的 Java 命令行参数
suspend 表示是否在启动目标虚拟机后挂起虚拟机
AttachingConnector
ListeningConnector
hostname 表示被链接一端的地址
port 表示被链接一端的端口
timeout 表示等待链接的时间

下面将举一个简单例子,描述如何设置 main 链接参数,并启动目标虚拟机。首先,调用链接器的 defaultArguments() 获取该链接器所支持的一组默认参数,见 清单 5。

清单 5. 获取链接器的默认参数

1

2

Map<String,Connector.Argument> defaultArguments

    = connector.defaultArguments();

默认参数存储在一个 Key-Value 对的 Map 中,Key 是该链接器参数的唯一标识符(对终端用户不可见),Value 是对应的 Connector.Argument 实例(包括具体参数的信息和默认值)。返回的 Map 不能再新增或者删除元素,只能修改已有元素的值。

然后,从返回的 Map 中获取标识符为 main 的链接器参数,如 清单 6。

清单 6. 返回链接器的 main 参数

1 Connector.Argument mainArgument = defaultArguments.get(“main”);

*后,将 main 参数值设置为 com.ibm.jdi.test.HelloWorld,以修改后的参数启动目标虚拟机,见 清单 7。

清单 7. 设置 main 参数的值并启动虚拟机

1

2

mainArgument.setValue(“com.ibm.jdi.test.HelloWorld”);

VirtualMachine targetVM = connector.launch(defaultArguments);

JDI 事件请求和处理模块

JDI 事件分类

JDI 的 com.sun.jdi.event 包定义了 18 种事件类型,如 表 3 所示。其中,与 Class 相关的有 ClassPrepareEvent 和 ClassUnloadEvent;与 Method 相关的有 MethodEntryEvent 和 MethodExitEvent;与 Field 相关的有 AccessWatchpointEvent 和 ModificationWatchpointEvent;与虚拟机相关的有 VMDeathEvent,VMDisconnectEvent 和 VMStartEvent 等。

表 3. JDI 中的事件类型

事件类型 描述
ClassPrepareEvent 装载某个指定的类所引发的事件
ClassUnloadEvent 卸载某个指定的类所引发的事件
BreakingpointEvent 设置断点所引发的事件
ExceptionEvent 目标虚拟机运行中抛出指定异常所引发的事件
MethodEntryEvent 进入某个指定方法体时引发的事件
MethodExitEvent 某个指定方法执行完成后引发的事件
MonitorContendedEnteredEvent 线程已经进入某个指定 Monitor 资源所引发的事件
MonitorContendedEnterEvent 线程将要进入某个指定 Monitor 资源所引发的事件
MonitorWaitedEvent 线程完成对某个指定 Monitor 资源等待所引发的事件
MonitorWaitEvent 线程开始等待对某个指定 Monitor 资源所引发的事件
StepEvent 目标应用程序执行下一条指令或者代码行所引发的事件
AccessWatchpointEvent 查看类的某个指定 Field 所引发的事件
ModificationWatchpointEvent 修改类的某个指定 Field 值所引发的事件
ThreadDeathEvent 某个指定线程运行完成所引发的事件
ThreadStartEvent 某个指定线程开始运行所引发的事件
VMDeathEvent 目标虚拟机停止运行所以的事件
VMDisconnectEvent 目标虚拟机与调试器断开链接所引发的事件
VMStartEvent 目标虚拟机初始化时所引发的事件

不同的事件需要被分类地添加到不同的事件集合(EventSet)中,事件集是事件发送的*小单位。事件集一旦创建出来,便不可再被修改。JDI 定义了一些规则,用以规定应该如何将事件分别加入到不同的事件集中:

  • 每个 VMStartEvent 事件应该分别加入到单独的一个事件集中;
  • 每个 VMDisconnectEvent 事件应该分别加入到单独的一个事件集中;
  • 所有的 VMDeathEvent 事件应该加入到同一个事件集中;
  • 同一线程的 ThreadStartEvent 事件应该加入到同一事件集中;
  • 同一线程的 ThreadDeathEvent 事件应该加入到同一事件集中;
  • 同一类型的 ClassPrepareEvent 事件应该加入到同一个事件集中;
  • 同一类型的 ClassUnloadEvent 事件应该加入到同一个事件集中;
  • 同一 Field 的 AccessWatchpointEvent 事件应该加入到同一个事件集中;
  • 同一 Field 的 ModificationWatchpointEvent 事件应该加入到同一个事件集中;
  • 同一异常的 ExceptionEvent 事件应该加入到同一个事件集中;
  • 同一方法的 MethodExitEvents 事件应该加入到同一个事件集中;
  • 同一 Monitor 的 MonitorContendedEnterEvent 事件应该加入到用一个事件集中;
  • 同一 Monitor 的 MonitorContendedEnteredEvent 事件应该加入到用一个事件集中;
  • 同一 Monitor 的 MonitorWaitEvent 事件应该加入到同一个事件集中
  • 同一 Monitor 上的 MonitorWaitedEvent 事件应该加入到同一个事件集中
  • 在同一线程执行过程中,具有相同行号信息的 BreakpointEvent、StepEvent 和 MethodEntryEvent 事件应该加入到同一个事件集合中。

生成的事件集将被依次地加入到目标虚拟机的事件队列(EventQueue)中。然后,EventQueue 将这些事件集以“先进先出”策略依次地发送到调试器端。EventQueue 负责管理来自目标虚拟机的事件,一个被调试的目标虚拟机上有且仅有一个 EventQueue 实例。特别地,随着一次事件集的发送,目标虚拟机上可能会有一部分的线程因此而被挂起。如果一直不恢复这些线程,有可能会导致目标虚拟机挂机。因此,在处理好一个事件集中的事件后,建议调用事件集的 resume() 方法,恢复所有可能被挂起的线程。

JDI 事件请求

Event 是 JDI 中所有事件接口的父接口,它只定义了一个 request() 方法,用以返回由调试器发出的针对该事件的事件请求(EventRequest)。事件请求是由调试器向目标虚拟机发出的,目的是请求目标虚拟机在发生指定的事件后通知调试器。只有当调试器发出的请求与目标虚拟机上发生的事件契合时,这些事件才会被分发到各个事件集,进而等待发送至调试器端。在 JDI 中,每一种事件类型都对应着一种事件请求类型。一次事件请求可能对应有多个事件实例,但不是每个事件实例都存在与之对应的事件请求。例如,对于某些事件(如 VMDeathEvent,VMDisconnectEvent 等),即使没有对应的事件请求,这些事件也必定会被发送给调试器端。

另外,事件请求还支持过滤功能。通过给 EventRequest 实例添加过滤器(Filter),可以进一步筛选出调试器真正感兴趣的事件实例。事件请求支持多重过滤,通过 EventRequest 的 add*Filter() 方法可以添加多个过滤器。多个过滤器将共同作用,*终只有满足所有过滤条件的事件实例才会被发给调试器。常用的过滤器有:

  • 线程过滤器:用以过滤出指定线程中发生的事件;
  • 类型过滤器:用以过滤出指定类型中发生的事件;
  • 实例过滤器:用以过滤出指定实例中发生的事件;
  • 计数过滤器:用以过滤出发生一定次数的事件;

过滤器提供了一些附加的限制条件,减少了*终加入到事件队列的事件数量,从而提高了调试性能。除了过滤功能,还可以通过它的 setSuspendPolicy(int) 设置是否需要在事件发生后挂起目标虚拟机。

事件请求是由事件请求管理器(EventRequestManager)进行统一管理的,包括对请求的创建和删除。一个目标虚拟机中有且仅有一个 EventRequestManager 实例。通常,一个事件请求实例有两种状态:激活态和非激活态。非激活态的事件请求将不起任何作用,即使目标虚拟机上有满足此请求的事件发生,目标虚拟机将不做停留,继续执行下一条指令。由 EventRequestManager 新建的事件请求都是非激活的,需要调用 setEnable(true) 方法激活该请求,而通过 setEnable(false) 则可废除该请求,使其转化为非激活态。

JDI 事件处理

下面将介绍 JDI 中调试器与目标虚拟机事件交互的方式。首先,调试器调用目标虚拟机的 eventQueue() 和 eventRequestManager() 分别获取唯一的 EventQueue 实例和 EventRequestManager 实例。然后,通过 EventRequestManager 的 createXxxRequest() 创建需要的事件请求,并添加过滤器和设置挂起策略。接着,调试器将从 EventQueue 获取来自目标虚拟机的事件实例。

一个事件实例中包含着事件发生时目标虚拟机的一些状态信息。以 BreakpointEvent 为例:

调用 BreakpointEvent 的 thread() 可以获取产生事件的线程镜像(ThreadReference),调用 ThreadReference 的 frame(int) 可获得当前代码行所在的堆栈(StackFrame),调用 StackFrame 的 visibleVariables() 可获取当前堆栈中的所有本地变量(LocaleVariable)。通过调用 BreakpointEvent 的 location() 可获得断点所在的代码行号(Location),调用 Location 的 method() 可获得当前代码行所归属的方法。通过以上调用,调试器便可获得了目标虚拟机上线程、对象、变量等镜像信息。

另外,根据从事件实例中获取的以上信息,调试器还可以进一步控制目标虚拟机。例如,可以调用 ObjectReference 的 getValue() 和 setValue() 访问和修改对象中封装的 Field 或者 LocalVariable 等,进而影响虚拟机的行为。更多的 JDI 的事件处理的详情,请参见图 2。

图 2. JDI 事件处理框架(查看大图

图 2. JDI 事件处理框架

一个 JDI 的简单实例

下面给出一个简单例子,说明如何实现 JDI 的部分接口来提供一个简易的调试客户端。首先是被调试的 Java 类,这里给出一个简单的 Hello World 程序,main 方法*行声明一个“Hello World!”的字符串变量,第二行打印出这个字符串的内容,见 清单 8。

清单 8. HelloWorld 类文件

1

2

3

4

5

6

7

8

package com.ibm.jdi.test;

 

public class HelloWorld {

    public static void main(String[] args) {

        String str = "Hello world!";

        System.out.println(str);

    }

}

接着是一个简单的调试器实现 SimpleDebugger,清单 9 列出了实现该调试器所需要导入的类库和变量。简单起见,所有的变量都声明为静态全局变量。这些变量分别代表了目标虚拟机镜像,目标虚拟机所在的进程,目标虚拟机的事件请求管理器和事件对列。变量 vmExit 标志目标虚拟机是否中止。

清单 9. SimpleDebugger 导入的类和声明的全局变量

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

package com.ibm.jdi.test;

 

import java.util.List;

import java.util.Map;

import com.sun.jdi.Bootstrap;

import com.sun.jdi.LocalVariable;

import com.sun.jdi.Location;

import com.sun.jdi.ReferenceType;

import com.sun.jdi.StackFrame;

import com.sun.jdi.StringReference;

import com.sun.jdi.ThreadReference;

import com.sun.jdi.Value;

import com.sun.jdi.VirtualMachine;

import com.sun.jdi.connect.Connector;

import com.sun.jdi.connect.LaunchingConnector;

import com.sun.jdi.connect.Connector.Argument;

import com.sun.jdi.event.BreakpointEvent;

import com.sun.jdi.event.ClassPrepareEvent;

import com.sun.jdi.event.Event;

import com.sun.jdi.event.EventIterator;

import com.sun.jdi.event.EventQueue;

import com.sun.jdi.event.EventSet;

import com.sun.jdi.event.VMDisconnectEvent;

import com.sun.jdi.event.VMStartEvent;

import com.sun.jdi.request.BreakpointRequest;

import com.sun.jdi.request.ClassPrepareRequest;

import com.sun.jdi.request.EventRequest;

import com.sun.jdi.request.EventRequestManager;

 

public class SimpleDebugger {

    static VirtualMachine vm;

    static Process process;

    static EventRequestManager eventRequestManager;

    static EventQueue eventQueue;

    static EventSet eventSet;

    static boolean vmExit = false;

随后是 SimpleDebugger 的 main() 方法,见 清单 10。首先从 VirtualMachineManager 获取默认的 LaunchingConnector,然后从该 Connector 取得默认的参数。接着,设置 main 和 suspend 参数,使得目标虚拟机运行 com.ibm.jdi.test.HelloWorld 类,并随后进入挂起状态。下一步,调用 LaunchingConnector.launch() 启动目标虚拟机,返回目标虚拟机的镜像实例,并且获取运行目标虚拟机的进程( Process)。

然后,创建一个 ClassPrepareRequest 事件请求。当 com.ibm.jdi.test.HelloWorld 被装载时,目标虚拟机将发送对应的 ClassPrepareEvent 事件。事件处理完成后,通过 process 的 destroy() 方法销毁目标虚拟机进程,结束调试工作。

清单 10. SimpleDebugger 的 main() 方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

public static void main(String[] args) throws Exception{

    LaunchingConnector launchingConnector

        = Bootstrap.virtualMachineManager().defaultConnector();

    

    // Get arguments of the launching connector

    Map<String, Connector.Argument> defaultArguments

        = launchingConnector.defaultArguments();

    Connector.Argument mainArg = defaultArguments.get("main");

    Connector.Argument suspendArg = defaultArguments.get("suspend");

    // Set class of main method

    mainArg.setValue("com.ibm.jdi.test.HelloWorld");

    suspendArg.setValue("true");

    vm = launchingConnector.launch(defaultArguments);

 

    process = vm.process()

 

    // Register ClassPrepareRequest

    eventRequestManager = vm.eventRequestManager();

    ClassPrepareRequest classPrepareRequest

        = eventRequestManager.createClassPrepareRequest();

    classPrepareRequest.addClassFilter("com.ibm.jdi.test.HelloWorld");

    classPrepareRequest.addCountFilter(1);

    classPrepareRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);

    classPrepareRequest.enable();

 

    // Enter event loop

    eventLoop();

 

    process.destroy();

}

下面是 eventLoop() 函数的实现:首先获取目标虚拟机的事件队列,然后依次处理队列中的每个事件。当 vmExit(初始值为 false)标志为 true 时,结束循环。

清单 11. SimpleDebugger 的 eventLoop() 的实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

private static void eventLoop() throws Exception {

    eventQueue = vm.eventQueue();

    while (true) {

        if (vmExit == true) {

            break;

        }

        eventSet = eventQueue.remove();

        EventIterator eventIterator = eventSet.eventIterator();

        while (eventIterator.hasNext()) {

            Event event = (Event) eventIterator.next();

            execute(event);

        }

    }

}

具体事件的处理是由 execute(Event) 实现的,这里主要列举出 ClassPreparEvent 和 BreakpointEvent 事件的处理用法,请参见 清单 12。

清单 12. SimpleDebugger 的 execute () 方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

private static void execute(Event event) throws Exception {

    if (event instanceof VMStartEvent) {

        System.out.println("VM started");

        eventSet.resume();

    } else if (event instanceof ClassPrepareEvent) {

        ClassPrepareEvent classPrepareEvent = (ClassPrepareEvent) event;

        String mainClassName = classPrepareEvent.referenceType().name();

        if (mainClassName.equals("com.ibm.jdi.test.HelloWorld")) {

            System.out.println("Class " + mainClassName

                    + " is already prepared");

        }

        if (true) {

            // Get location

            ReferenceType referenceType = prepareEvent.referenceType();

            List locations = referenceType.locationsOfLine(10);

            Location location = (Location) locations.get(0);

 

            // Create BreakpointEvent

            BreakpointRequest breakpointRequest = eventRequestManager

                    .createBreakpointRequest(location);

            breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);

            breakpointRequest.enable();

        }

        eventSet.resume();

    } else if (event instanceof BreakpointEvent) {

        System.out.println("Reach line 10 of com.ibm.jdi.test.HelloWorld");

        BreakpointEvent breakpointEvent = (BreakpointEvent) event;

        ThreadReference threadReference = breakpointEvent.thread();

        StackFrame stackFrame = threadReference.frame(0);

        LocalVariable localVariable = stackFrame

                .visibleVariableByName("str");

        Value value = stackFrame.getValue(localVariable);

        String str = ((StringReference) value).value();

        System.out.println("The local variable str at line 10 is " + str

                + " of " + value.type().name());

        eventSet.resume();

    } else if (event instanceof VMDisconnectEvent) {

        vmExit = true;

    } else {

        eventSet.resume();

    }

}

*后列出了以上程序的运行结果,见 清单 13。

清单 13. 运行结果

1

2

3

4

VM started

Class com.ibm.jdi.test.HelloWorld is already prepared

Reach line 10 of com.ibm.jdi.test.HelloWorld

The local variable str at line 10 is Hello world! of java.lang.String

结束语

本文介绍了 Java 调试接口(JDI)的体系结构和工作方式,JDI 是 JPDA 体系结构的调试器后端接口,开发人员通过使用它所提供的接口与 JDWP(Java 调试线协议)前端 Agent 通信,以这种方式访问和控制被调试的目标虚拟机。*后,本文希望能够帮助开发人员在无须掌握 JPDA 的技术细节的情况下,能够编写出实用、高效的 Java 调试器程序。

Treiber Stack简单分析

Abstract

Treiber Stack Algorithm是一个可扩展的无锁栈,利用细粒度的并发原语CAS来实现的,Treiber Stack在 R. Kent Treiber在1986年的论文Systems Programming: Coping with Parallelism中首次出现。

基本原理

该算法的基本原理是:只有当您知道要添加的项目是自开始操作以来唯一添加的项目时,才会添加新的项目。 这是通过使用比较和交换完成的。 在添加新项目时使用堆栈,将堆栈的顶部放在新项目之后。 然后,将这个新构造的头元素(旧头)的第二个项目与当前项目进行比较。 如果两者匹配,那么你可以将旧头换成新头,否则就意味着另一个线程已经向堆栈添加了另一个项目,在这种情况下,你必须再试一次。

当从堆栈中弹出一个项目时,在返回项目之前,您必须检查另一个线程自操作开始以来没有添加其他项目。

正确性

在某些语言中,特别是那些没有垃圾回收的语言,Treiber栈可能面临ABA问题。当一个进程要从堆栈中移除一个元素时(就在下面的pop例程比较和设置之前),另一个进程可以改变堆栈,使得头部是相同的,但是第二个元素是不同的。比较和交换将堆栈的头部设置为堆栈中旧的第二个元素,混合完整的数据结构。但是,由于Java运行时提供了更强大的保证,所以此页面上的Java版本不受此问题的影响(新创建的不混淆的对象引用不可能与任何其他可到达的对象引用相同)。

对诸如ABA之类的故障进行测试可能会非常困难,因为有问题的事件序列非常少见。

Java示例

下面是Java中Treiber Stack的实现,它基于Java Concurrency in Practice提供的

  1. public class ConcurrentStack<E> {
  2. private AtomicReference<Node<E>> top = new AtomicReference<>();
  3. public void push(E item) {
  4. Node<E> newHead = new Node<>(item);
  5. Node<E> oldHead;
  6. do {
  7. oldHead = top.get();
  8. newHead.next = oldHead;
  9. } while (!top.compareAndSet(oldHead, newHead));
  10. }
  11. public E pop() {
  12. Node<E> oldHead;
  13. Node<E> newHead;
  14. do {
  15. oldHead = top.get();
  16. if (oldHead == null)
  17. return null;
  18. newHead = oldHead.next;
  19. } while (!top.compareAndSet(oldHead, newHead));
  20. return oldHead.item;
  21. }
  22. private static class Node<E> {
  23. public final E item;
  24. public Node<E> next;
  25. public Node(E item) {
  26. this.item = item;
  27. }
  28. }
  29. }

流程分析

PUSH操作

图片描述
根据上述的描述做图如上,并分析其工作流程。

  1. 首先单链表保存了各个Stack中的各个元素,成员变量top持有了栈的栈顶元素。
  2. 当执行push操作时,首先创建一个新的元素为newHead,并让该新节点的next指针指向top节点(此时top=oldHead)。
  3. *后通过CAS替换top=newHead,CAS的交换条件是top=oldHead
  4. 当条件满足后,操作后的状态如下:

图片描述

POP操作

图片描述
根据上述的描述做图如上,并分析其工作流程。

  1. 当执行pop操作时,创建一个新的指针,该指针指向topnext元素。
  2. 然后通过CAS替换top=newHead,CAS的交换条件是top=oldHead

3.当条件满足后,操作后的状态如下:
图片描述

PS:

java.util.concurrent.FutureTask中waiters就使用了Treiber Stack

var与let、const的区别

一、var声明的变量会挂载在window上,而let和const声明的变量不会:

 

var a = 100;
console.log(a,window.a);    // 100 100

let b = 10;
console.log(b,window.b);    // 10 undefined

const c = 1;
console.log(c,window.c);    // 1 undefined

 

二、var声明变量存在变量提升,let和const不存在变量提升

console.log(a); // undefined  ===>  a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined  ===> 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined  ===> 找不到c这个变量
const c = 10;

三、let和const声明形成块作用域

 

if(1){
    var a = 100;
    let b = 10;
}

console.log(a); // 100
console.log(b)  // 报错:b is not defined  ===> 找不到b这个变量

 

 

if(1){

    var a = 100;
        
    const c = 1;
}
 console.log(a); // 100
 console.log(c)  // 报错:c is not defined  ===> 找不到c这个变量

 

四、同一作用域下let和const不能声明同名变量,而var可以

var a = 100;
console.log(a); // 100

var a = 10;
console.log(a); // 10

let a = 100;
let a = 10;

//  控制台报错:Identifier 'a' has already been declared  ===> 标识符a已经被声明了。

五、暂存死区

 


var a = 100;

if(1){
    a = 10;
    //在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
    // 而这时,还未到声明时候,所以控制台Error:a is not defined
    let a = 1;
}

 

六、const

 

/*
*   1、一旦声明必须赋值,不能使用null占位。
*
*   2、声明后不能再修改
*
*   3、如果声明的是复合类型数据,可以修改其属性
*
* */

const a = 100; 

const list = [];
list[0] = 10;
console.log(list);  // [10]

const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj);  // {a:10000,name:'apple'}

Android哪几种情况会导致:应用未安装

出现这种情况,感觉遇到过就三种情况会出现:

1、手机内存不足

出现这种情况的时候,如果是使用AS连接的话是可以看到log提示说手机内存不足的

2、手机的安全设置,不允许安装未知来源应用

出现这种情况的时候,是因为手机设置了不允许安装未知来源应用,这个会提示,不会出现“应用未安装”的情况

3、签名不一致

注意我要说的就是这种情况,首先要明确是什么签名?为什么会出现不一致?

(1)是什么签名?

jks,我们打包apk的时候用到

(2)为什么会出现签名不一致

我在使用真机调试的时候,as是使用默认的jks签名来进行打包,然后安装到手机上;但是我现在要发布,就使用自己的jks来打包。

我的手机之前真机调试的apk1(jks是默认的)还安装在手机上;当我用一个自己的jks打包了一个apk2,我没先把apk1卸载掉就直接安装apk2的时候,就会提示“应用未安装”

所以把现在手机里安装的默认的jks打包的apk1卸载掉,再安装apk2就不会出现“应用未安装”了

(3)相同的jsk的时候,还要注意versionCode的大小问题,要安装的应用的versionCode要大于已经已安装的versionCode

 

解决共享库不存在或失效导致的应用未安装

找了很久,,资源都是分开的 这次我把他汇总,打包,方便下载了 不用一个一个的找了下了

很多使用安卓的朋友应该都遇到过安装某些软件出现”应用未安装“,用电脑手机助手进行安装时会出现没有该软件的共享库或已失效,这个问题大概困扰了我3天时间,终于在我的坚持下找到了解决方法,希望出现过这样问题的朋友严格安装我下面的步骤操作即可解决以上问题!

*步:使用第三方工具中的ROOT获取工具,进行获取ROOT权限。

第二步:下载ROOT EXPLORER 具有*高权限的文件管理工具,安装并打开(打开时出现是否允许此软件使用*高权限,允许)。

第三步:下载以下三个文件。

com.google.android.maps.jar

com.google.android.maps.xml

NetworkLocation.apk

第四步:使用ROOT EXPLORER 文件工具将三个文件以下面的说明操作。

com.google.android.maps.jar 拷贝到根目录的 system/framework/ 文件夹下面。

然后将此文件的权限设为 root 权限,然后在设置访问权限对话框上选项上对话框选项有9个可打钩的方格,把左边三个的全部勾选,中间的三个的*个勾上即可。

com.google.android.maps.xml 拷贝到根目录的 system/etc/permissions/ 文件夹下面。

同上面的设置权限的方法一样。

NetworkLocation.apk 拷贝到根目录的 data/app_s/ 文件夹下面。

(如果找不到 app_s这个文件夹,可以新建一个 app_s文件夹,然后在将其拷贝进去)!将此文件的权限设置为 system 然后在设置访问权限对话框上选项上对话框选项有9个可打钩的方格,将左边三个全部勾上,中间的*个和第二个勾上,右边全部勾上即可。然后重启手机,开始安装你以前安装失败的软件,然后你就会看到安装成功的兴奋了。这样你就永远不会在遇到软件未安装或没有该软件的共享库或已失效的情况了。

应用未安装!安装包似乎已经损坏

一、描述
新建了做了一个项目,通过Android Studio直接安装没有问题,把测试包直接发送给测试却提示“应用未安装!安装包似乎已经损坏”,没有其他任何提示信息。

当时怀疑是签名问题,所以就给debug设置了签名,通过 build APK 来打包 debug 版本,可正常安装到手机,但是通过 run 产生的apk发送给测试还是不可以正常安装。

网上搜索出的结果大部分都是让我再打包的时候够短V1和V2,这和我遇到的场景不同,勾选V1和V2是build时的操作,而我的问题出在了run上边。

二、思路初现
后来翻到一片文章,博主分析的原因是 targetSdkVersion 的版本比已安装的版本低,是通过修改了targetSdkVersion的版本号来解决的,我这边没有变动过 targetSdkVersion ,所以也不是这个问题。但是看博主的解决流程给我提供了思路。

https://blog.csdn.net/weixin_34185320/article/details/87365694

博主在发送安装失败后,使用ADB直接安装,来查看错误信息。

所以,按照这个思路,我安装了一下run产生的apk,看到如下错误信息:

>>>adb install -r app/debug/app-debug.apk

Performing Streamed Install
adb: failed to install app/build/outputs/apk/debug/app-debug.apk: Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI]
三、问题解决
原因很明显:INSTALL_FAILED_TEST_ONLY 。

经过查询资料发现在AndroidManifest.xml文件中添加了属性testOnly=true,

https://developer.android.com/guide/topics/manifest/application-element

查看apk中的清单文件确实testOnly=true,原来Android Studio 3.0以后会在debug.apk的清单文件中自动添加testOnly=true属性,导致安装失败,那么为什么Android Studio直接运行可以安装呢?  后来查看运行日志输出发现Studio的运行命令是

adb shell pm install -t -r “/data/local/tmp/com.XXXX.qa”
adb install 一共有留个选项

-l 锁定该应用程序
-r 替换已存在的应用程序,也就是说强制安装
-t 允许测试包
-s 把应用程序安装到sd卡上
-d 允许进行将见状,也就是安装的比手机上带的版本低
-g 为应用程序授予所有运行时的权限
可以看出Android Studio run运行是允许测试包安装的。尝试了一下adb install -t  , 果然有效。

 

但是测试是手机直接下载的App,不能调用adb来安装,能不能去掉testOnly=true这个标记呢?直接百度就能找到答案:在gradle.properties(项目根目录或者gradle全局配置目录 ~/.gradle/)文件中添加android.injected.testOnly=false 。

再运行run,查看apk中的清单文件,testOnly属性消失了。

通过下载上传新的app,可正常安装。

四、总结
当遇到安装失败时,可以模拟条件使用adb来安装查找原因。

INSTALL_FAILED_TEST_ONLY解决方法有两种:

1、使用adb install -t debug.apk 来安装

2、在gradle.properties中配置 android.injected.testOnly=false

 

Android安装APK时提示安装包异常导致安装失败

今天本人在签名打包APK的时候遇到了一个问题 那就是

%title插图%num

这个问题很奇葩,百度了居然没有任何答案,我在OPPO论坛百度到了相关问题,管理员回复这是OPPO为看保障用户权益XXX的,吓死宝宝了

百度无果,只能去群里问了,在群里好心朋友的提醒下 我察觉到签名的问题(签名:这个锅我不背)

因为不打签名可以运行,打了就不能安装

尝试了无数次之后发现 问题果然出现在签名过程中

%title插图%num

本人一开始是只选了第二个,所以导致了错误,正确的姿势应该是选*个 或者两个都选,切记 不要单独选*个

Android 做一个简单记事本app

1、引用了Android studio自带的*个模板(右下角带小圆按钮的),用这个按钮做点击事件用来跳到编辑笔记界面。项目创建会多带两个java文件(FirstFragment、SecondFragment)以及对应的两个xml文件,这里我尝试将这几个文件删掉结果运行会崩,然后我将这几个文件的主题代码注释掉又没问题。暂时不管那么多。

2、创建一个编辑笔记的activity和对应的xml,新建的activity要在androidManifest.xml文件里面注册。然后在自建Activity的标签里面添加android:windowSoftInputMode=“adjustResize”,这样弹出输入法的时候就不会遮挡文本输入框。(这行代码意思作用是当弹出输入法时以更小的布局去显示界面,相当于输入法把界面挤上去了。还有一个adjustPan,能使当前内容自动移动避免被输入法遮挡)。xml文件里面很简单就一个EditView和一个Button,EditView里面填入的提示文字用android:hint,这样输入时就不用像android:text一样再删一次提示文字了。然后设置android:inputType=“textMultiLine” android:singleLine=“false”,这样就可以像文本文档一样一直输入多行了。然后背景色android:background设置一个颜色以区分,虽然简陋但还是要有个样子哈哈。以前设置颜色的时候都要去网上搜颜色代码,这次才发现这一行设置左边行数旁边会有一个小颜色框显示的当前颜色,点一下就可以调颜色了。我就说这么功能全面的开发软件怎么会没有调色板呢!

%title插图%num
然后就是Button的点击事件了,首先判断EditView里面不为空,然后就是痛苦的数据库环节。但好在AndroidStudio里面自带的SQLite操作简单且方便,提供了一系列方法避免了直接写sql语句。

3,要操作数据库,首先就要先创建一个类继承SQLiteOpenHelper类,看名字就知道,数据库开启助手,专门用来进行数据库操作的类,虽然我了解的不多,但是要进行数据库操作应该是离不了它了。创建好我的NoteDatabasehelper后,首先当然是实现父类的两个抽象方法,一个onCreate(SQLiteDatabase db){},一个onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){}。前一个当然就是建表等sql语句的执行地方了。后面一个则是升级数据库用的,只要传进去的新版本号大于老版本号,就可以在里面执行升级逻辑。然后我们还要一个构造函数,传一个Context对象进来。这个Context我搞得不是很清楚,反正就是到处都有用。然后构造方法里面会有一个父类的构造实现,传进四个参数,一个就是Context,然后数据库名,factory(一般都为null),数据库版本号。数据库名和版本在前面定义一个static final的就好了,然后再onCreate里面执行建表语句。这样当我们调用这个构造方法的时候就会建库,然后会自动调用onCreate方法建表(这里我是自己写的建表语句然后在onCreate里面执行的,之前老是建库建表傻傻分不清楚)。但是如果后续想要再加表的话貌似会失败?因为数据库已经存在,不会再调用onCreate方法了,这里就需要onUpgrade方法来更新数据库了。

4、写好NoteDataBaseHelper类以后,我们就可以在之前的Activity的点击事件里面调用了,首先当然是new 一个NoteDatabaseHelper类,传进一个当前的Context对象(当前Activity.this),然后就是声明一个SQLitDatabase对象 = 我们new出来的NoteDataBaseHelper对象.getReadableDatabase()。一直到这里数据库才算真正创建出来。这里注意其实有两个方法都可以创建数据,即getReadableDatabase()和getWritableDatabase()。这两个都可以创建数据库,数据库如果已经存在就会自动打开数据库,然后返回一个可对数据库进行操作的对象。**区别在于当数据库不可写入(如磁盘满了)的情况下,getReadableDatabase()方法返回的对象将以只读的方式打开数据库,而getWritableDatabase()会出现异常。**这里我使用的前者。然后就是往表里存数据了(建库的时候表就已经一起建好了)。存数据当然也是有快捷方法的。

创建好数据库之后可以setting-plugins-marketplace里面搜索database navigator插件并下载,再重启,然后就可以用这个工具查看数据库了。具体步骤首先要点击view-tools windows-device file explorer,在右边弹出的窗口浏览设备文件,data-data-包名-databases下面会有一个database文件,右键另存为,然后在Android Studio*左边应该会有一个DB Browser,点进去然后点击绿色加号,浏览刚刚存储database文件的目录打开文件就可以了。

5,首先new出一个ContentValues对象,这个对象会帮助我们存储数据到时候一并插入表。用.put()方法向对象里面传入数据,这里有两个参数,前一个是String类型的,即插入表中的数据的key名,后面一个则是对应的value了。这里我传入了两个,一个是获取的EidtView里面的数据,对应我表里的note,第二个是当前时间,对应表里的time。(我的表里就三项数据,id,note和time)。再然后就是用之前get的数据库操作对象的insert方法插入数据,insert方法有三个参数,*个表名,第二个不知道是啥填null,第三个就是ContentValues对象了。然后我调用了一个startActivity方法跳回了MainActicity界面,这样就直接刷新MainActivity界面了,就能直接看到笔记的显示记录。

%title插图%num
6,想要在主界面看到笔记记录,当然不可能就这样就好了,MainActivity的界面都还没做呢!到activity_main.xml文件里面,会看到已经有一些代码了,这是这个模板自带的,把没用的去掉(我不记得这个xml有没有没用的组件了,反正留一个右下角的小圆按钮就行了)。然后在*上面写一个ListView就ok了。ListView的属性里面加一个id,高度选匹配内容wrap_content,然后添加上下左右的约束就好。*好用layout_marginXXX加点边距,显得好看一点。这里有一点,上边距(marginTop)要设一个合适的数值,因为应用的label会占用屏幕上方部分空间,如果上边距不设或者设的太少,后面会发现*行视图少了一截。这个之前看到过有解决方式但是我没有去深究,在这里记一下!

7、然后就是很很很很让人头疼的ListView的使用了,我是在网上看了好多别人做的,越看越头疼,可能是我脑子不太好使吧。反正*后我是照着实体书(《Android*行代码》)的入门再结合网上别人做的才整出来。

这个是先写了一个存一条数据的bean类,里面有两个属性即note和time。然后在MainActivity里面声明一个ArrayList数组,数据类型存的这个bean类的对象(我写的类名为ItemInfo)。然后再写了一个initItemInfo()方法,故名思意用来向ArrayList里面存储数据。说到数据当然还是要数据库操作了。老样子NoteDatabaseHelper DBHelper = new NoteDatabaseHelper(this);
SQLiteDatabase db = DBHelper.getReadableDatabase();获取数据库操作对象实例之后,声明一个Cursor对象存储db.query()方法查询出来的数据。这个query方法应该是数据库增删查改里*复杂的了,*少都有七个参数当然这里我们查询整张表只需要*个参数填表名,其他统统null就好。但还是要说一下其他几个参数,后面有的会用到。第二个是columns,即指定查询的列名,第三个是seletion,选择条件,相当于where,填入比如note=? , ?当然就是占位符,第四个就是选择条件的数据,用来替换占位符。

获取到数据库的数据之后,就是要将之存入ArrayList里面了。先if(cursor.moveToFirst)判断一下,里面好像是让cursor从*行开始读取?实体书上用的是kotlin,我记得kotlin的if与java的if用法好像是基本相同的,所以我就照着写了,具体为什么判断一下,暂时不了解。然后在if里面做一个do{}while()循环。循环里面调用ArrayList的add方法,循环存入ItemInfo对象,即add(new ItemInfo( cursor.getString(cursor.getColumnIndex(“note”)) , cursor.getString(cursor.getColumnIndex(“time”)) ))。这里忘了说了,我的ItemInfo的构造方法传了note和time进去的。然后就可以循环把所有的item存入ArrayList里面了。每一条item信息都会分别显示在listview里面的每一个item组件里。

%title插图%num
8、那么要怎么显示呢?首先当然是为item组件创建一个属于它自己的界面,即新建一个xml文件,这个文件也不需要很复杂,只需要两个Textview,分别展示记的内容和记录时间,以及给它限制一个高度。然后,很重要!我们需要一个装载器Adapter,负责将item组件一个个装载进ListView的展示列表。我是做完再回头看才终于了解了一些ListView和装载器的关系的。之前一直被几个实体类,xml,实现类砸的头晕。

这里我先写了一个NoteAdapter的实体类继承系统的Adapter,这里有好几个可选,我用的是ArrayAdapter。然后重写构造函数,我选的*后一个,三个参数。*个context没什么好说的,第二个是一个int类型的resource,待会儿用来传入之前写的那个属于item自己的xml文件,也是这里我才知道,原来R.layout.组件文件ID原来是int类型啊!*后一个参数List类型当然就是用来传入之前存储数据的ArrayList了。然后重写getView方法,也是三个参数,这里并不需要我们动手调用这个方法,我也没去弄明白参数意思。在这个方法里面,先声明一个View view= LayoutInflaterfrom(context).inflate(resource, parent, false);resource就是刚说的要传入xml文件的。另外两个不懂,跟着写的。这样item的组件布局就被填充进装载器了。然后我们就可以声明TextView把item组件里的两个TextView根据id声明出来。如TextView noteInfo = (TextView)view.findViewById(R.id.novteInfo);跟在MainActivity里面声明组件也差不多。然后要声明Item itemInfo = (ItemInfo) getItem(position);所以这里还得重写一个getItem()方法,position指的是当前item在ListView中的坐标位置(从0开始),getView会传一个进来。再就是setText将对应item的内容显示出来了。这里判断一下,如果note大于一定长度就只显示一截,毕竟空间有限嘛!*后return view。

9、接下来就是在主界面显示条目了。首先执行一次initItem方法把数据都依次加载进ArrayList里面来,然后new一个刚刚写的装载器adapter的实例,再执行listview的setAdapter方法,传入一个adapter参数进去。(这里可能要转一次型,跟着代码提示就行)。然后运行app就可以看到笔记记录都加载进来了

%title插图%num

10、光能显示记录可还不行,我们要给他一个点击事件和一个长按点击事件。点击事件用来进入界面修改笔记,这里我用的笨方法,直接再写了一个UpdateNote的Activity用来更新笔记。绑定的xml文件与之前用来写笔记的Activity的xml文件一样,只是去除了android:hintText文字提示,因为要在这个EditView里面添加note数据嘛。回到点击事件,跟Button差不多,调用listView.setOnItemClickListener方法,然后在里面new 一个OnItemCilickListener(),然后会自动重写一个onItemClick方法。这个方法会传进来四个参数,不用我们管,应该是点击的时候系统自动传进来的。其中pisition即之前所说的每个条目在ListView中的位置,这个我们会用得到。在这个方法里面,我先实例化了一个ItemInfo(即之前的条目信息bean类)对象,用来接listView的.getItemAtPosition()方法返回的数据。这个方法即根据位置获取单个item信息,传进去position。然后强转一下。再然后要用到一个Bundle函数。我们先实例化Bundle,然后用.putString()方法传入数据。这里传入的是一个键值对,key随便写就好了,后面根据这个key取出数据的。value即传入对应的数据,调用先前创建的ItemInfo对象的get方法获取数据。如:bundle.putString(“note”, ii.getNote());。那么用这个Bundle存入数据是干嘛的呢?

当点击ListView条目的时候,我们要跳到相应的Acitivity(即之前创建的updateNote)以修改笔记内容。要跳界面当然要用到Intent i = new Intent(MainActivity.this, UpdateNote.class);
startActivity(i);这两个方法。但是这样是无法把数据传过去。如果我们要在UpdateNote修改数据肯定要先获得相应的数据,这里就会有点麻烦,没有搜索条件怎样取到对应的数据呢?所以我们这里就可以用Intent的.putExtras()让Intent对象帮我们传递额外的数据信息过去。而这个方法传递的就是这个Bundle对象了。所以这里就是先在点击时获取相应条目的数据,因为有position这个参数和getItemAtPosition方法,获取起来就十分简单。然后将数据传到UpdateNote这个Activity。这样既可以直接获取note信息预显示在更新笔记界面,又能在之后更新数据库的时候有条件可用。

%title插图%num
11、然后来到UpdateNoteActivity界面,首先声明一个Bundle对象来接Intent额外传过来的Bundle参数。如:Bundle b = this.getIntent().getExtras();然后用Bundle的getString方法根据自己之前的key获取对应数据,再EditText.setText显示出来。这样当我们点进对应的条目时,就会预先显示之前写的笔记数据了。然后就是做笔记修改后的点击事件。在按钮点击事件里,先判断一下现在getText获取到的EditText的数据是否等于之前的值也就是Bundle传进来的那个数据。如果不等于就进行数据库操作修改note数据。
跟之前存储数据一样,先打开数据库,再new一个ContentValues对象,把修改后的数据put进去,列名一定要跟数据库一样。再然后根据bundle传进来的数据选一个有代表性的作为条件(我用的time,因为肯定不会重复。)。调用db.update方法,传入四个参数:表名,ContentValues对象,条件(time=?),替换占位符的数据(这个一定要看清是String数组,要new一个然后把time数据放进去)。然后就更新成功了。跳一个界面回到MainAcitivity,这样主界面会再加载一次,就能实时更新ListView的条目信息了。(这里如果不跳界面,手机手动返回不知道能不能达到实时更新ListView信息的效果,感觉应该是可以的)。

%title插图%num
12、*后就是长按点击事件删除内容了,ListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener()),然后也会自动重写一个OnLongClick方法。在这个方法里,首先要实例化一个可互动的警告框,这里我使用的AlertDialog.Builder,实例化时照例传入一个当前Context对象。这里这个Builder我看别人的资料说是一种设计模式,当你想用一个控件或者对象却又无法直接new出来时,那么这个对象多半就是用到了Builder模式,至于为什么不能直接new出来,一般可能是太过复杂吧(Builder详解)。new出这个对象后,调用setTitle和setMessage两个方法为警告框添加标题和警告内容。然后调用setPositiveButton做确认事件,setNegativiButton做取消事件。这两个事件都要传入一个text,即按钮的内容(yes/no),然后new一个DialogInterface.OnClickListener()。它会自动重写一个onClick方法,在取消事件setNegativeButton的OnClick里面我们什么都不用写,或者可以写一个Toast弹出提示。而在确认事件setPositiveButto的OnClickn里面要进行数据库操作删除信息数据。这跟之前差不多,条件语句的条件通过获取当前item的ItemInfo对象然后get一个数据就可以。db.delete相比之前的更新操作db.update还要少了一个ContentValues参数。删除完以后要记得重新加载页面,即在执行一个获取页面的代码。但是这里要注意,在加载场景之前一定要将存储item信息的ArrayList通过removeAll方法清空,然后再重新initItemInfo往ArrayList里面存新的数据库信息再加载界面场景。不然会发现ListView之前的界面没删除,然后下面接着的又是新的删除指定信息后的item界面(就是会重复显示内容)。做完两个点击事件之后,记得要执行AlertDialog.Builder对象的create方法和show方法,不然警告框不会显示。*后return true。

%title插图%num

%title插图%num

13、*后有两个点要注意,我之前在adapter设置的是note数据超过十个字符时就省略后面的,但是实际过程如果一个字符换一行的话会导致该信息的item界面显示异常。还有一个问题就是如果有几十条几百条甚至更多的话,这个记事本是要一次性全部加载完再一次性装载进Listview的,这个的优化涉及到到了adapter的getView里面的convertView参数。这个参数好像是可以实现未显示的条目释放资源,当显示新的条目时自动实时加载内容。这样可以大大减轻资源负担。

14、到此一个简单的记事本就实现了,能力有限只能做到这样了。说实话确实费了我很大功夫,结果回头再看发现其实也没我做的时候想的那么难。主要还是自己平时积累太少,很多东西照着别的人用一遍就不管了,这样的话根本就得不到任何收获。个人感觉还是要多写博客,虽然记的东西可能不是什么高技术含量的东西,但是过一遍总归能加深印象,下次再做就算忘了,照着自己写的东西看也比看别人的东西更容易也更有成就感。因此,这次特地下决心一定要把这个博客写出来,这可能比我目前为止其他的博客加起来还长了。可能本文排版做的不好,写的很乱废话也多,不过这个初衷本来就是为自己所写的,所以也就不管那么多了。

如果有幸被各位大佬看到了,也欢迎指点一二。

今天解决的当少于十个字符换行会导致item界面显示出问题的问题。加一个判断就好了,判断要显示的字符是否存在回车,存在就分割字符串,再拿出分割出来的*个字符串判断长度是否大于预定要显示的长度。

关于装载器里的view复用,只需要在给view赋值时判断是否为空,为空则创建。
给每个view传一个id方便识别:

%title插图%num

页面加载完毕:

%title插图%num

当往下翻页时,*开始id为0的view被向上遮挡后又转到下面来显示下一条信息:

%title插图%num

Android简易记事本

此次做的Android简易记事本的存储方式使用了SQLite数据库,然后界面的实现比较简单,但是,具有增删改查的基本功能,这里可以看一下效果图,如下:

%title插图%num

%title插图%num

%title插图%num

具体操作就是长按可以删除操作,点击可以进行修改,点击添加笔记按钮可以添加一个笔记。

首先我们需要三个界面样式一个是我们的进入程序时的*个界面,然后*个界面里面有一个ListView,这个ListView需要一个xml来描述里面的各个元素,这也是第二个。还有一个就是我们的编辑页面的界面。
三个xml描述文件如下:

activity_main.xml:进入程序的*个界面

<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=”match_parent”
android:orientation=”vertical”
tools:context=”.MainActivity” >

<TextView
android:layout_height=”wrap_content”
android:layout_width=”fill_parent”
android:text=”记事本列表”
android:textSize=”20sp”
android:paddingTop=”10dp”
android:paddingBottom=”5dp”
android:gravity=”center”/>

<LinearLayout
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:layout_weight=”1″ >

<ListView
android:id=”@+id/listNote”
android:layout_margin=”5dp”
android:layout_width=”match_parent”
android:layout_height=”wrap_content” >
</ListView>
</LinearLayout>

<Button
android:id=”@+id/addNote”
android:layout_width=”fill_parent”
android:layout_height=”wrap_content”
android:layout_gravity=”center”
android:layout_marginBottom=”10dp”
android:text=”添加笔记”
android:textSize=”20sp” />

</LinearLayout>

note_item.xml:描述记事本列表中每个元素的各个控件

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical” >

<TextView
android:id=”@+id/noteTitle”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginLeft=”10dp”
android:singleLine=”true”
android:text=””
android:textAppearance=”?android:attr/textAppearanceLarge” />

<TextView
android:id=”@+id/noteCreateTime”
android:layout_width=”fill_parent”
android:layout_height=”wrap_content”
android:layout_marginLeft=”10dp”
android:text=”” />

</LinearLayout>

note_editor.xml:编辑界面

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical” >

<TextView
android:id=”@+id/noteId”
android:visibility=”gone”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=””/>

<EditText
android:id=”@+id/title”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:ems=”10″
android:hint=”输入标题”>
<requestFocus />
</EditText>

<EditText
android:id=”@+id/content”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_weight=”1″
android:hint=”输入内容”
android:gravity=”left”>
</EditText>

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
android:layout_gravity=”center”
android:gravity=”center”>

<Button
android:id=”@+id/save”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”center”
android:layout_marginBottom=”10dp”
android:text=”保存”
android:textSize=”20sp” />

<Button
android:id=”@+id/cancel”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”center”
android:layout_marginBottom=”10dp”
android:text=”取消”
android:textSize=”20sp” />
</LinearLayout>

</LinearLayout>

现在我们可以考虑我们底层的数据库的操作了,这里有一个类专门用于与数据库打交道,如下:
DBService.java

public class DBService {

private static SQLiteDatabase db = null;

static {
//新建或者打开db
db = SQLiteDatabase.openOrCreateDatabase(“data/data/cn.lger.notebook/NoteBook.db”, null);

String sql = “create table NoteBook(_id integer primary key autoincrement,title varchar(255),content TEXT, createTime varchar(25))”;

//判断是否存在表NoteBook,如果不存在会抛出异常,捕捉异常后创建表
try{
db.rawQuery(“select count(1) from NoteBook “,null);
}catch(Exception e){
db.execSQL(sql);
}
}

public static SQLiteDatabase getSQLiteDatabase(){
return db;
}

public static Cursor queryAll(){
return db.rawQuery(“select * from NoteBook “,null);
}

public static Cursor queryNoteById(Integer id){
return db.rawQuery(“select * from NoteBook where _id =?”,new String[]{id.toString()});
}

public static void deleteNoteById(Integer id){
if(id == null)
return ;
db.delete(“NoteBook”, “_id=?”, new String[]{id.toString()});
}

public static void updateNoteById(Integer id, ContentValues values){
db.update(“NoteBook”, values, “_id=?”, new String[]{id.toString()});
}

/**
* 添加一个笔记,并且记录当前添加的时间
* @param values 表中的各个字段值
*/
public static void addNote(ContentValues values){
values.put(“createTime”, DateFormat.format(“yyyy-MM-dd kk:mm:ss”, System.currentTimeMillis()).toString());
db.insert(“NoteBook”, null, values);
}
}

下面我们在进入*个界面的时候需要访问数据库并且将数据的值不断的更新(比如进行了删除操作的时候或者添加操作之后需要刷新),这样,我们就可能需要重写Activity的onResume(),这样就可以调用Cursor的requery()来刷新我们列表中ListView的结果。还有我们需要长按删除,点击修改,添加笔记这些都需要监听事件,因此,这里还要设置监听
具体MainActivity.java的代码如下:

public class MainActivity extends Activity {
private Cursor listItemCursor = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 设置添加笔记按钮事件,切换activity
this.findViewById(R.id.addNote).setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View arg0) {
Intent in = new Intent();
in.setClassName(getApplicationContext(),
“cn.lger.notebook.NoteEditActivity”);
startActivity(in);
}
});

// 查询所有笔记,并将笔记展示出来
listItemCursor = DBService.queryAll();
SimpleCursorAdapter adapter = new SimpleCursorAdapter(MainActivity.this,
R.layout.note_item, listItemCursor, new String[] { “_id”,
“title”, “createTime” }, new int[] { R.id.noteId,
R.id.noteTitle, R.id.noteCreateTime },
CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
((ListView) this.findViewById(R.id.listNote)).setAdapter(adapter);

initListNoteListener();

}

/**
* 初始化笔记列表的长按和点击事件
*/
private void initListNoteListener() {
// 长按删除
((ListView) this.findViewById(R.id.listNote))
.setOnItemLongClickListener(new OnItemLongClickListener() {

@Override
public boolean onItemLongClick(AdapterView<?> parent,
View view, int position, final long id) {
new AlertDialog.Builder(MainActivity.this)
.setTitle(“提示框”)
.setMessage(“确认删除该笔记??”)
.setPositiveButton(“确定”,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0,int arg1) {
DBService.deleteNoteById((int) id);
//删除后刷新列表
MainActivity.this.onResume();
Toast.makeText(
MainActivity.this,
“删除成功!!”,
Toast.LENGTH_LONG)
.show();
}
}).setNegativeButton(“取消”, null).show();
return true;
}
});

//点击进行修改操作
((ListView) this.findViewById(R.id.listNote))
.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Intent in = new Intent();
in.setClassName(view.getContext(),
“cn.lger.notebook.NoteEditActivity”);
// 将id数据放置到Intent,切换视图后可以将数据传递过去
in.putExtra(“id”, id);
startActivity(in);
}
});

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

/**
* 当从另一个视图进入该视图会调用该方法
*/
@Override
protected void onResume() {
super.onResume();
// 要求刷新主页列表笔记
if (listItemCursor != null) {
listItemCursor.requery();
}
}
}

上面的代码中还涉及到了一个视图切换后的传递信息的操作,就是通过Intent的putExtra(key, value)这样可以在切换后的视图中调用函数getIntent().get~Extra(key, replace);来接收传递的数据。

下面是我们的编辑界面中对应的具体实现代码,这里有判断是使用更新操作还是添加操作,主要是判断MainActivity.java有没有传递过来id,如果有就是通过这个id来更新操作,没有就是添加操作。
编辑界面对应的具体实现代码如下:
NoteEditActivity.java

public class NoteEditActivity extends Activity {

private EditText titleEditText = null;
private EditText contentEditText = null;
private String noteId = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.note_editor);

titleEditText = (EditText) NoteEditActivity.this
.findViewById(R.id.title);
contentEditText = (EditText) NoteEditActivity.this
.findViewById(R.id.content);

initNoteEditValue();

//取消按钮监听
this.findViewById(R.id.cancel).setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View arg0) {
NoteEditActivity.this.finish();
}
});

this.findViewById(R.id.save).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {

final String title = titleEditText.getText().toString();
final String content = contentEditText.getText().toString();

//判断标题和内容是否为空,不为空才能保存
if (“”.equals(title) || “”.equals(content)) {
Toast.makeText(NoteEditActivity.this, “标题或者内容不能为空”,
Toast.LENGTH_LONG).show();
return;
}

//提示保存
new AlertDialog.Builder(NoteEditActivity.this)
.setTitle(“提示框”)
.setMessage(“确定保存笔记吗??”)
.setPositiveButton(“确定”,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0,
int arg1) {
ContentValues values = new ContentValues();
values.put(“title”, title);
values.put(“content”, content);

//如果noteId不为空那么就是更新操作,为空就是添加操作
if (null == noteId || “”.equals(noteId))
DBService.addNote(values);
else
DBService.updateNoteById(
Integer.valueOf(noteId),
values);
//结束当前activity
NoteEditActivity.this.finish();
Toast.makeText(NoteEditActivity.this, “保存成功!!”,
Toast.LENGTH_LONG).show();
}
}).setNegativeButton(“取消”, null).show();

}
});
}

/**
* 初始化编辑页面的值(如果进入该页面时存在一个id的话),比如标题,内容。
*/
private void initNoteEditValue() {
// 从Intent中获取id的值
long id = this.getIntent().getLongExtra(“id”, -1L);
// 如果有传入id那么id!=-1
if (id != -1L) {
// 使用noteId保存id
noteId = String.valueOf(id);
// 查询该id的笔记
Cursor cursor = DBService.queryNoteById((int) id);
if (cursor.moveToFirst()) {
// 将内容提取出来
titleEditText.setText(cursor.getString(1));
contentEditText.setText(cursor.getString(2));
}
}
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}

以上就将我们的安卓简易记事本完成了,源码已经上传GitHub。

界面采用了拿来主义,可以参考下面文章
http://blog.csdn.net/cjs68/article/details/50211543

简易安卓APP项目

简介
现在来分享期末做的安卓大作业——生活百科。
本项目只是单纯的一个大作业,没有考虑实际的需求,所以有设计不合理的地方,请见谅。
这个项目有三大功能(因为是使用了侧边栏所以是可以继续往里面添加功能的),首先有单词查询,其次是天气查询,后来是机器聊天功能。单词查询是使用了扇贝单词提供的免费API;天气查询是使用了聚合数据提供的天气API,这里需要注册使用(有限的免费使用);机器聊天是使用了图灵机器人的API。所以,总结一下,这里我的主要工作不会很多,主要是调用API然后进行数据的分析和显示工作。
本次使用的IDE是idea(Android studio)。

文章的*后将会给出源码,但是一些API接口需要自己去申请Key

在项目开始前的学习阶段
安卓基础入门 http://www.runoob.com/android/android-tutorial.html
安卓省市联动(天气的位置选择) http://blog.csdn.net/qq_20521573/article/details/51914180
安卓fragment的使用 http://www.jikexueyuan.com/course/708.html
机器聊天界面的HTML源码 http://www.lanrenzhijia.com/js/3930.html
以上便是我着手项目的累积步骤,希望有帮助

1. 项目准备阶段
1.1. 新建项目
使用Android Studio(IDEA)新建安卓项目->选择SDK版本(Android 4.0为好)->选择Navigation Drawer Activity模板->完成项目创建。项目新建之后就会得到我们基本的界面模板了。而后,只需要删除右侧边和底部的元素就可得到下图所示的界面

%title插图%num
当然,这里使用Android ADT也是可以的,不过好像新建出来的模板不一样,比较丑一点(自己对于Android的UI不太行),所以就使用了Android Studio。

1.2. 添加依赖
由于需要使用省市联动的功能,所以需要添加外部依赖,在上面给出的链接上有详细说明,因此需要在app文件夹下的build.gradle里面的dependencies加入
compile ‘com.contrarywind:Android-PickerView:3.2.5’//自定义控件
compile ‘com.google.code.gson:gson:2.7’//解析JSON

做完了准备工作之后就进入主题

2. 单词查询功能
考虑文章的篇幅过长,所以有些详细的过程可能会省略(像界面的修改工作等等)。

%title插图%num

当我们输入了单词之后点击查询是需要访问网络的,这里采用了异步的任务机制去访问网络并且得出结果,部分代码如下:
//这是继承OnQueryTextListener 的内部类,用于处理搜索框监听事件
public class SearchViewClickListener implements SearchView.OnQueryTextListener {

@Override
public boolean onQueryTextSubmit(String s) {
if (!lastSearchResult.equals(s)){//判断上一个结果和目前查询的是否相同
System.out.println(“上个结果:”+lastSearchResult);
SearchWordTask task = new SearchWordTask();//新建查询任务
task.execute(“https://api.shanbay.com/bdc/search/?word=”+s);//访问网络的地址
searchResult.setVisibility(View.VISIBLE);//设置查询结果的TextView可见
}
lastSearchResult = s;//更新*后查询结果
System.out.println(“*新结果:”+s);
return true;
}

@Override
public boolean onQueryTextChange(String s) {
return true;
}
}

在访问网络而后传回数据我这里将这个功能提取到了一个工具类,代码如下:

public class HttpUtil {

/**
* 获取访问网络后传回的数据
* @param urlString URL
* @return String
*/
public static String getJSONResult(String urlString){
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(“GET”);
conn.setRequestProperty(“accept”, “*/*”);
conn.setRequestProperty(“connection”, “Keep-Alive”);
conn.setRequestProperty(“user-agent”, “Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36”);

InputStream is = conn.getInputStream();
byte[] buff = new byte[1024];
int hasRead;
StringBuilder result = new StringBuilder(“”);
while ((hasRead = is.read(buff)) > 0) {
result.append(new String(buff, 0, hasRead));
}
return result.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

在上面中SearchWordTask是一个继承了AsyncTask的内部类,主要是实现了访问扇贝提供的英文单词查询API,部分代码如下:

class SearchWordTask extends AsyncTask<String, Void, String> {

@Override
protected String doInBackground(String… arg0) {
//arg0是执行AsyncTask的execute函数传入的可变参数
//这里arg0[0]是”https://api.shanbay.com/bdc/search/?word=”+s
return HttpUtil.getJSONResult(arg0[0]);
}

@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if (result == null || “”.equals(result)){
Toast.makeText(getActivity(), “查询出错!”, Toast.LENGTH_LONG).show();
}else
fillResultForJSON(result);

}

/**
* 解析查询的结果
* @param JSON JSON数据
*/
private void fillResultForJSON(String JSON){
try {
JSONObject object = new JSONObject(JSON);
if (“SUCCESS”.equals(object.getString(“msg”))){
final JSONObject dataObject = object.getJSONObject(“data”);
paraphrase.setText(“基本释义:”+dataObject.getString(“definition”));
final String uk_audio = dataObject.getString(“uk_audio”);
final String us_audio = dataObject.getString(“us_audio”);
detail.setOnClickListener(new View.OnClickListener() {
//如果详细释义按钮点击则访问如下页面
@Override
public void onClick(View view) {

webView.setVisibility(View.VISIBLE);
webView.loadUrl(“https://www.shanbay.com/bdc/mobile/preview/word?word=”+lastSearchResult);
webView.setWebViewClient(new WebViewClient());
}
});
//以下是发音按钮被点击时的监听事件
UKButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
//使用UKMediaPlayer 播放声音
UKMediaPlayer = new MediaPlayer();
UKMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
UKMediaPlayer.setDataSource(uk_audio);
UKMediaPlayer.prepare(); // 这个过程可能需要一段时间,例如网上流的读取
UKMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
});

USButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//同上面的播放声音步骤

});

JSONObject pronunciations =dataObject.getJSONObject(“pronunciations”);
UKPronunciation.setText(“英式发音:[“+pronunciations.getString(“uk”)+”]”);
USPronunciation.setText(“美式发音:[“+pronunciations.getString(“us”)+”]”);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}

以上便是单词查询的主体功能了,当然,考虑文章篇幅所以不能一一解析。

3. 天气查询功能
在天气查询功能中比较重要的就是使用了网友所写的省市联动功能,这里请参考上面链接中的文章。下面我将不会涉及这方面的讲解,这里主要是说一下访问聚合数据所提供的API,这个功能的效果如下图:
%title插图%num

%title插图%num

在上面功能中的业务代码主要是跟上面的单词查询差不多,都是使用了异步查询,由内部类实现,代码如下:
class WeatherTask extends AsyncTask<String, Void, String> {

@Override
protected String doInBackground(String… arg0) {

try {
return HttpUtil.getJSONResult(arg0[0] + URLEncoder.encode(arg0[1], “UTF-8”));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}

@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if (result != null){
parseWeatherJSON(result);
}
}

/**
* 根据JSON数据解析
* @param result JSON
*/
private void parseWeatherJSON(String result){
try {
JSONObject object = new JSONObject(result);
if (object.getInt(“error_code”) == 0){
JSONObject resultObj = object.getJSONObject(“result”);
JSONObject todayObj = resultObj.getJSONObject(“today”);
String weatherResult = “温度:”+todayObj.getString(“temperature”)+”\n”;
weatherResult += “天气状况:”+todayObj.getString(“weather”)+”\n”;
weatherResult += “风向:”+todayObj.getString(“wind”)+”\n”;
weatherResult += “穿衣建议:”+todayObj.getString(“dressing_advice”);
todayWeather.setText(weatherResult);
}else {
todayWeather.setText(“请求出错!”);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}

4. 机器聊天功能
机器聊天功能是使用了HTML页面来进行人机交互,所以,这里基本上没有涉及到Java上面的问题,主要是加载HTML页面以及开启JavaScript功能,代码如下:

WebView webView = (WebView) rootView.findViewById(R.id.chat_robot);
//加载本地的HTML页面(将文件置于src/main/assets/) webView.loadUrl(“file:///android_asset/chat_robot.html”);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());

功能的实现效果如图:

%title插图%num

当然了,要是用以上的聊天功能需要注册图灵机器人申请Key,源码中不会提供Key,所以请自行注册。
在接收图灵机器人返回的数据时,它会有代码来确定返回的是什么类型,所以,需要我们来判断类型来进行相应的解析(这里的JS代码就不贴出来了)。

5. 总结
这次的是一个安卓的期末作业,但是却没有很认真的对待的样子,而且项目还是不完善的,比如,在查询了单词后做其他功能的操作再次返回单词查询功能则之前的数据不能够保存,用户体验不好。这里当然我也知道一些解决的方案,保存当前的fragment状态,但是,我还是偷懒了。懒惰真的是宿敌。
文章上写的详细程度不够,但是主要的代码都已经写出。我知道,有时候解决一个功能并不是功能本身,而是要防止功能附带出来的bug,当然了,这就是我的经历。
还有就是可能我对于面向对象还是理解上有所偏差,对于抽象还是做得很烂,接下来希望看看别人的源码来改善这个问题。