Forwards

有空的博客


  • 首页

  • 归档

  • 关于

从源码解析Android异步消息处理机制(一)

发表于 2017-07-13

从源码解析Android异步消息处理机制(一)


前言:本篇纯属个人学习笔记,故摘录《Android开发艺术探索》大量语句,特此声明


  • 目的:深入理解Looper、Handler、MessageQueue三者之间的关系。

    1. 概述


Android的消息机制主要指Handler的运行机制,Handler的运行又和MessageQueue、Looper这两者紧密相关。简单来说,异步消息处理线程启动后,Looper会以无限循环的形式去MessageQueue查找是否有新的消息,如果有的话就回调相应的消息处理函数,否则就一直等待着(阻塞)。

2.源码解析

MessageQueue

MessageQueue(Android.os.MessageQueue类),消息队列。它的内部存储了一组消息,以队列的形式对外提供插入和读取的工作(读取操作本身会伴随着删除操作)。虽然叫消息队列,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。插入和读取对应的方法分别是enqueueMessage()和next()。
我们主要看next()方法(而enqueueMessage()方法有兴趣的道友可自行查看源码)

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
43
44
45
46
47
Message next() {
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

// We can assume mPtr != 0 because the loop is obviously still running.
// The looper will not call this method after the loop quits.
nativePollOnce(mPtr, nextPollTimeoutMillis);

synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (false) Log.v("MessageQueue", "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
...
}

可以发现next()方法是一个无限循环的方法,如果MessageQueue没有消息,那么next()会一直阻塞在那里,当有新消息时,next方法会返回这条消息并将其从单链表中移除。

Looper

Looper(Android.os.Looper类),消息循环。由于MessageQueue只是一个消息的存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper会以无限循环的形式去MessageQueue查找是否有新的消息,如果有的话就回调相应的消息处理函数,否则就一直等待着(阻塞)。Looper中还有个特殊的概念——ThreadLocal,它并不是线程,它的作用是在每个线程中存储数据。
我们直接上源码开撸:
首先看一下它的构造函数:

1
2
3
4
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

我们可以看到在构造方法中它会创建一个MessageQueue即消息队列,然后通过变量mThread将当前线程保存起来。
我们知道,Handler的运行需要Looper,没有Looper的线程会报错,那如何为一个线程创建Looper?Looper.prepare()这个方法就是提供了这个功能:

1
2
3
4
5
6
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

在Looper.prepare()方法中sThreadLocal是一个ThreadLocal对象,可以在一个线程中存储变量,通过sThreadLocal.set()将一个Looper实例放入了ThreadLocal,中间的if判断则说明了Looper.prepare()方法不能被调用两次,一个线程只能有一个Looper实例。
Looper最重要的一个方法是loop(),只有调用了loop后,消息循环机制才会真正地起作用:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

msg.target.dispatchMessage(msg);

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycle();
}
}

/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static Looper myLooper() {
return sThreadLocal.get();
}

我们一行行看代码,在myLooper()方法中返回了sThreadLocal存储的Looper实例,如果me为null,则抛出异常,也就是说loop()必须在prepare()方法之后运行。接着拿到该looper实例中的mQueue(具体可见Looper的构造函数)。再接下来就进入一个for(;;)死循环,而唯一跳出循环的方式是MessageQueue的next()方法返回了null。紧接着把消息交给msg的target的disptchMessage(msg)方法去处理。而msg的target就是handler对象,下面会进行分析。最后一句msg.recycle()则释放消息占据的资源。

接着我们分析一下MessageQueue.next()什么时候会返回null?我们先看看Looper的quit()方法:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public void quit() {
mQueue.quit(false);
}

public void quitSafely() {
mQueue.quit(true);
}

//MessageQueue的quit(boolean safe)方法

void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}

synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;

if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}

// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}

private void removeAllMessagesLocked() {
Message p = mMessages;
while (p != null) {
Message n = p.next;
p.recycleUnchecked();
p = n;
}
mMessages = null;
}

private void removeAllFutureMessagesLocked() {
final long now = SystemClock.uptimeMillis();
Message p = mMessages;
if (p != null) {
if (p.when > now) {
removeAllMessagesLocked();
} else {
Message n;
for (;;) {
n = p.next;
if (n == null) {
return;
}
if (n.when > now) {
break;
}
p = n;
}
p.next = null;
do {
p = n;
n = p.next;
p.recycleUnchecked();
} while (n != null);
}
}
}

也就是说当Looper的quit方法被调用时,Looper就会调用MessageQueue的quit()或quitSafely()方法来通知消息队列退出,当消息队列被标记为退出状态时,它的next方法就会返回null。

小结
Looper主要作用:

  1. 与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue。(具体可见Looper的构造函数)
  2. looper()方法,不断从MessageQueue中去取消息,交给消息的target属性的dispatchMessage()去处理。

Java并发编程实践——读书笔记

发表于 2017-05-21

《Java并发编程实践》读书笔记(一)

第一章 简介

  • 线程的优点:使用多处理器、模型的简化、对异步事件的简单处理、用户界面的更佳响应性(避免ANR)
  • 线程的风险:

    1. 安全危险:多线程中的各个操作顺序是不可预测的(竞争条件),为了使多线程程序的行为可预见,访问共享的变量必须经过合理的协调==>Java提供了同步机制
    2. 活跃度的危险:死锁(deadlock)、饥饿(starvation)、活锁(livelock)。线程的使用引入了活跃度失败(liveness failure),例如:如果线程A等待一个线程B独立占有的资源,B永远不释放这个资源,A将永远等待下去。
    3. 性能危险:①线程切换(上下文切换)带来的巨大的系统开销,②同步机制会限值编译器的优化(个人理解:多个线程访问同一块内存导致内存总线负载增加)
    4. 线程无处不在:即使你的程序没有显示的创建线程,并不代表不需要考虑线程安全。当JVM启动后,它会创建一些线程来进行自身的常规管理(垃圾回收、终结处理),以及一个运行main函数的主线程。原文中有一句:“线程安全的需要并不仅仅在于框架调用的组件——只要它处于组件访问过的程序状态段,它就会扩展到所有代码路径。因此,线程安全的需要是具有传递性的。”

    第二章 线程安全

  • 编写线程安全的代码,本质上是管理对状态的访问,而且通常是共享的,可变的状态==>真正要做的是:在不可控制的并发访问中保护数据。

  • 什么时候使用同步机制:只要有多于一个的线程访问给定的状态变量,而且其中的某个线程会写入该变量。Java中的同步机制:①synchronized代码块,②volatile关键字、③显示锁和原子变量的使用。
    一个线程安全程序是完全由线程安全类构成的吗? ==>完全由线程安全类构成的程序未必是线程安全的,线程安全程序也可以包含非线程安全的类。
  • 线程安全性:一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。

    当多个线程访问一个类时,若不用考虑这些线程在运行时环境下的调整和交替执行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

  • 对于线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态。

Ionic2入门教程——初识Ionic2

发表于 2017-05-02

##项目结构
ionic2是基于Angular2的,所以它的项目结构和Angular2类似,具体目录结构如下:
|——ionic.config.json #ionic的配置文件
|——package.json
|——resources #打包app使用的icon图标和加载页图片
…….|——android #生成的android平台的各个尺寸的图标和加载页图图片
…….|——icon.png #应用程序图标
…….|——ios # 生成的ios平台的各个尺寸的图标和加载页图片
…….|——splash.png # 加载页图片
|——src #页面源文件
…….|——app
…….|——assets
…….|——declarations.d.ts
…….|——index.html
​ …….|——manifest.json
…….|——pages
…….|——service-worker.js
…….|——theme
|——tsconfig.json
|——tslint.json
|——www #编译后的文件夹,Cordova默认用www路径并打开里面的index.html
|——plugins
|——platforms
|——hooks

###项目环境相关文件说明

  1. package.json:每个基于node平台的项目都有这样一个文件,里面定义了项目的基本信息,还有开发和运行需要用到的库。

  2. ionic.config.json:ionic项目的基本配置文件,里面的内容部分如下:

1493718125065

设置了proxies以后,我们的服务端就不需要考虑跨域访问的问题。当然,如果你的服务器端不允许跨域访问,在你部署你的应用的时候,也需要相应的配置,例如在nginx中设置反向代理。

  1. config.xml:当项目启用了cordova并且打算封装成Hybird App就会有这个文件,里面的内容部分如下:

1493718625450

其中,name就是app的显示名,plugin就是使用的cordovas插件,更多内容可参考cordova的文档。

  1. tslint.json、tsconfig.json:这是用tslint做代码检查的配置。

  2. plugins:存放所添加的cordova插件。

  3. platforms:存放平台相关的文件,每次编译app的时候,就会根据添加的平台,在这里面生成编译文件和打包的文件。

  4. hooks:我们可以编写脚本,来定义在每次执行某些任务的时候被调用的任务。也可以定义某个插件相关的脚本。例如,如果你加了某一个微信的插件,这个插件可能有一个钩子,帮助你在每次添加完这个插件的时候,运行一些任务,来进行一些项目的配置,例如微信的appId等。

Android IPC知识点记录

发表于 2017-02-13

Android IPC知识点记录(一、基础概念介绍)

此篇文章是在看完《Android开发艺术探索》后的个人笔记小结,摘录书中大量的语句以及例子,特此声明

先谈谈IPC的使用场景:多进程,多进程的情况分为两种:第一种情况是一个应用因为某些原因需要采用多进程模式实现,第二种情况是当前应用需要向其他应用获取数据。接下来我们就从IPC是什么开始说起……

IPC(Ienter-Process Communication)

含义为进程间通信或者跨进程通信,是指两个进程间进行数据交换的过程(区分进程和线程的关系:包含与被包含)。当然,IPC不是Android中独有的,任何一个操作系统都需要有相应的IPC机制,对于Android来说,它是一种基于Linux内核的移动操作系统,它有自己的进程间通信方式。下面我们来学习多进程的相关知识。

多进程模式

正常情况下,在Android中多进程是指一个应用中存在多个进程的情况。开启方法:给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性,非常规方法:通过JNI在native层去fork一个新的进程。(JNI是什么鬼,待google资料)。多进程命名方式分为两种:一种是进程名以“:”开头的进程,它的完整进程名是当前的进程名前面附加上当前的包名,第二种是完整的命名方式;两者的区别:进程名以“:”开头的进程名属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而另一种命名方式属于全局进程,其他应用通过SharedUID方式可以和它跑在同一个进程中。###Android会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据,两个应用通过SharedUID跑在同一个进程中是有要求的,需要两个应用具有相同的SharedUID并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据。(SharedUID番外篇:http://www.cnblogs.com/mythou/p/3258715.html)

多进程模式的运行机制

我们知道Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配了一个独立的虚拟机。而不同虚拟机在内存分配上有不同的地址空间,这就导致了在不同的虚拟机访问同一个类的对象会产生多份副本:所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也就是多进程带来的主要影响。一般来说,使用多进程会造成如下几方面的问题:

  1. 静态成员和单例模式会完全失效
  2. 线程同步机制完全失效
  3. SharedPreferences的可靠性下降
  4. Application会多次创建
    总结:在多进程模式中,不同进程的组件会拥有独立的虚拟机,Application以及内存空间。

IPC的基础知识

对象的序列化:Android中存在两种序列化方式:第一种是Serializable接口,是Java所提供的一个序列化接口,第二种是Parcelable,是Android中的序列化方式。

Serializable接口

实现Serializable只需要这个类实现Serializable接口并声明一个serialVersionUID既可,这个值可以用当前类的hash值,也可以自行指定。
private static final long serialVersionUID = xxxxxL
serialVersionUID的作用:用来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化。

1
2
3
4
5
6
7
8
9
//序列化过程
User user = new User(0, "Jake", true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();

注意:恢复后的对象newUser和user的内容完全一样,但是两者并不是同一对象(内存不同)。

序列化注意事项:

  1. 静态成员变量属于类不属于对象,所以不会参与序列化过程
  2. 用transient关键字标记的成员变量不参与序列化过程

如果不手动填写serialVersionUID,序列化的时候系统会自动算出一个serialVersionUID写到文件中,然后恢复的时候会再次计算,如果两者的serialVersionUID一致,也就是类没有被修改,那么不会影响反序列化,但是,一旦修改了类,那么计算出来的hash就不同了,反序列化的时候就会报错。程序会crash;如果手动填写,就算修改了类,只要不是毁灭性的修改(例如修改类名),那么反序列化时会尽最大的限度去进行反序列化,最大限度还原对象,而不是直接crash。

Parcelable接口

什么都不说了,先上代码:

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
public class Book implements Parcelable{
public int bookId;
public String bookName;

public Book(int bookId, String bookName){
this.bookId = bookId;
this.bookName = bookName;
}

@Override
public int describeContents() {
// TODO Auto-generated method stub
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
// TODO Auto-generated method stub
dest.writeInt(bookId);
dest.writeString(bookName);
}

public static final Parcelable.Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book[] newArray(int size) {
// TODO Auto-generated method stub
return new Book[size];
}

@Override
public Book createFromParcel(Parcel source) {
// TODO Auto-generated method stub
return new Book(source);
}
};

private Book(Parcel in){
bookId = in.readInt();
bookName = in.readString();
}

}
在序列化过程需要实现的功能有序列化、反序列化和内容描述,序列化功能由writeToParcel()来完成,最终是通过Parcel中的一系列write()方法完成。反序列化由CREATOR来完成;内容描述功能由describeContents()完成,几乎所有情况下这个方法都应该返回0(仅当当前对象中存在文件描述符中他才会返回1)。

How to choose?

Serializable是Java的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O操作;而Parcelable是Android的序列化方式,效率高但是使用起来稍微麻烦点(这是Android推荐的序列化方式,因此我们要首选Parcelable)。

​

MVC与MVP的学习

发表于 2016-11-21

学习重点:

  1. 概念、工作原理、优缺点
  2. 具体在Android中的应用
  3. demo实例

    1.MVC


MVC编程模式(Model View Controller)点击这里看百度百科介绍 简单来说就是通过controller的控制去操作model层的数据,并且返回给View层显示。具体如上图所示。
工作原理:当用户发出事件的时候,view层会发送指令到controller层,接着controller通知model层更新数据,model更新完数据后会直接显示在view层上。具体在Android中的情况:布局代码就是view层,controller就是activity,而各种java bean,还有一些类似repository类就对应于model层。
缺点:xml作为view层,控制能力太弱,若要动态的改变视图,只能在activity中实现,导致了activity既是view层也是controller层,耦合度大大提高,不利于项目管理,从上图也可知,view层和model层是相互可知的,这意味着两层之间存在耦合,耦合对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。
实例demo:Meizhi

2.MVP


MVP(Model View Presenter)点击这里看百度百科介绍
MVP是MVC的升级(区别):在MVP中View并不直接使用Model,它们之间的通信是通过Presenter(MVC中的Controller)来进行的,所有的交互都在Presenter内部进行,而在MVC中View会直接从Model中读取数据而不是通过Controller。从上图我们可以知道,view层和model层不再相互可知,而是完全的解耦,取而代之的presenter层充当了桥梁的作用,用于操作view层发出的事件传递到presenter层,presenter层去操作model层,并将数据返回给view层。
核心思想:
MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成presenter接口,model不变。在MVP模式中,Activity的功能就是响应生命周期和显示界面,其他的工作都丢到presenter中完成。
如何使用MVP:
1.创建IPresenter接口,把所有的业务逻辑都放在这里,并创建它的实现类IPresenterCompl。
2.创建IView接口,把所有的视图逻辑都放在这里,其实现类是当前的activity或者fragment。
3.Activity里包含了一个IPresenter,而PresenterCompl里又包含了一个IView 并且依赖了Model。Activity里只保留对IPresenter的调用,其它工作全部留到PresenterCompl中实现。
项目中代码结构(层次分明)
[站外图片上传中……(3)]

Model层

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
public class User {           
private String userName;
private String password;

public User(String userName, String password) {
this.userName = userName;
this.password = password;
}

public String getUserName() {
return userName;
}

public String getPassword() {
return password;
}

public void setUserName(String userName) {
this.userName = userName;
}

public void setPassword(String password) {
this.password = password;
}
}

View层

view层接口代码如下:

1
2
3
4
public interface ILoginView {     
public void clearText();
public void onLoginResult(boolean result,int code);
}

​
ILogin的实现类:

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
43
44
45
46
47
48
49
public class LoginActivity extends AppCompatActivity implements ILoginView, View.OnClickListener {       
private Button clear_btn;
private Button login_btn;
private EditText userName_edt;
private EditText pw_edt;
private ILoginPresenter iLoginPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
clear_btn= (Button) findViewById(R.id.clear);
login_btn= (Button) findViewById(R.id.login);
userName_edt= (EditText) findViewById(R.id.username);
pw_edt= (EditText) findViewById(R.id.password);
clear_btn.setOnClickListener(this);
login_btn.setOnClickListener(this);
iLoginPresenter=new LoginPresenterCompl(this);
}
@Override
public void clearText() {
userName_edt.setText("");
pw_edt.setText("");
Toast.makeText(LoginActivity.this,"Clear",Toast.LENGTH_LONG).show();
}
@Override
public void onLoginResult(boolean result, int code) {
clear_btn.setEnabled(true);
login_btn.setEnabled(true);
if(result){
Toast.makeText(LoginActivity.this,"登陆成功"+code,Toast.LENGTH_LONG).show();
}else{
Toast.makeText(LoginActivity.this,"登陆失败"+code,Toast.LENGTH_LONG).show();
}
}
@Override
public void onClick(View view) {
int id=view.getId();
String username=userName_edt.getText().toString();
String password=pw_edt.getText().toString();
switch (id){
case R.id.clear:
iLoginPresenter.clear();
break;
case R.id.login:
iLoginPresenter.doLogin(username,password);
break;
}
}
}

在LoginActivity中我们可以看出,LoginActivity实现ILoginView接口,实现了未实现的方法,在代码中可以看出LoginActivity并没有做一些逻辑处理工作,数据处理的工作都是调用ILoginPresenter完成的。

Presenter层

presenter接口代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface ILoginPresenter {            
public void clear();
public void doLogin(String username,String password);
}
IPresenter实现类:
public class LoginPresenterCompl implements ILoginPresenter {
private LoginActivity loginActivity;

private User user;

public LoginPresenterCompl(LoginActivity loginActivity)
{
this.loginActivity=loginActivity;
user=new User("张三","123456");
}

​
​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
       @Override
public void clear() {
loginActivity.clearText();
}

@Override
public void doLogin(String username, String password) {
//逻辑处理
boolean result=false;
int code=0;
if(username.equals(user.getUserName())&&password.equals(user.getPassword())){
result=true;
code=1;
}else{
result=false;
code=0;
}
loginActivity.onLoginResult(result,code);
}
}

这就是最简单的MVP模式了。
MVP优势:

  1. 使activity代码更加简洁
  2. 方便进行单元测试
  3. 避免activity内存泄露

自动化测试Robotium实战学习

发表于 2016-11-21

从什么是自动化测试开始

自动化测试就是通过一定的编程手段,自动执行本来需要手动执行的一系列测试的活动。

自动化测试和手动测试的对比:

  1. 执行速度快
  2. 可靠性高
  3. 复用性高
  4. 节省人力资源成本

移动端自动化测试工具的选择

  1. Appium:可以用来测试native、hybird和mobile web APP,最突出的一点是它既支持Android的自动化测试,也支持IOS的测试;另一方面是它支持使用不同的编程语言编写测试代码。缺点:稳定性有待提高。点此进官网
  2. Uiautomator:其是Google官方提供的一款自动化测试框架,主要特点是支持跨进程操作(这一点极大的方便了对应用外控件的操作)。缺点:只支持Android Platform、API16及以上,调试编译运行比较麻烦。具体可在官网查看
  3. Robotium:支持native和hybird的自动化测试,API使用起来方便简单,执行速度快,可根据控件ID操作;另外它既可以基于源码操作,也能基于APK测试,功能强大。缺点:不支持跨进程的操作;针对APK操作,而且需要对APK重新签名(有工具:re-sign),因此操作相对复杂。点此进官网
  4. 其他的自动化测试框架:monkey,monkeyrunner,Testdroid(商业)和Eggplant(商业)等。

Robotium版“Hello World”

基于源码自动化测试

  1. 搭建实战测试项目:在eclipse中依次选择File->New->Other->Android Test Project,一路点击next,添加Robotium.jar 5.6.3,在添加jar文件时应在java build path中查看,不然在初始化solo时容易碰到NullPointerException错误。
  2. 新建第一个测试用例:新建Java类,继承ActivityInstrumentationTestCase2< >.
    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
    //泛型写入需要测试的工程的入口activity名-LoginActivity
    public class TestCase1 extends ActivityInstrumentationTestCase2<LoginActivity> {
    private Solo solo;
    public TestCase1(){
    //LoginActivity是指启动的activity名称
    super(LoginActivity.class);
    }
    //实现测试前的初始化工作,JUnit框架下的方法
    @Override
    protected void setUp() throws Exception {
    // TODO Auto-generated method stub
    super.setUp();
    solo=new Solo(getInstrumentation(),getActivity());
    }
    //实现测试完成后的垃圾回收等工作,JUnit框架下的方法
    @Override
    protected void tearDown() throws Exception {
    // TODO Auto-generated method stub
    solo.finishOpenedActivities();
    super.tearDown();
    }
    //测试用例
    public void testTestCase1(){
    //不推荐通过index的方式找到控件,应通过ID的方式
    solo.enterText(0, "abcd");
    solo.enterText(1, "1234567");
    solo.clickOnButton(0);
    assertTrue("错误提示信息没有出现,可能出现bug",
    solo.searchText("用户名或者密码错误",true));
    }
    }

小结:在setUp()中实现一些前置动作,在tearDown()做一些恢复初始状态的动作,然后在test方法实现测试动作。

  1. 运行Robotium例子:在测试类中单击鼠标右键,选择Run As->Android JUnit Test.

基于APK的自动化测试

APK重签名:在基于APK的自动化测试过程中,需要确保被测试的APK必须与测试项目具有相同的签名。利用工具re-sign.jar,用法可自行Google。(re-sign重签名找的debug.keystore是位于C://User/.android下的,而eclipse的debug.keystore要与re-sign的一致)

  1. 搭建项目和新建测试用例与上述一样

    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
    //该注解的作用是给编译器一条指令,告诉它对被注解的代码内部的某些警告保持静默
    @SuppressWarnings("rawtypes")
    public class FirstQunarTest extends ActivityInstrumentationTestCase2{
    private static final String LAUNCHER_ACTIVITY_FULL_CLASSNAME="com.Qunar.NoteActivity";
    private static Class<?> launcherActivityClass;
    /*static代码块(静态代码块):在类中独立于类成员的static语句块,可随便存放
    它不在任何的方法体中,JVM加载类时会按照它们在类中出现的先后顺序依次执行它们,
    并且只会执行一次*/
    static{
    try {
    launcherActivityClass=Class.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME);
    } catch (ClassNotFoundException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    }
    @SuppressWarnings("unchecked")
    public FirstQunarTest(){
    super(launcherActivityClass);
    }
    private Solo solo;
    @Override
    protected void setUp() throws Exception {
    // TODO Auto-generated method stub
    solo=new Solo(getInstrumentation(),getActivity());
    }
    //测试用例:黑盒测试方式——无被测项目的源代码(基于APK)
    //robotium通过反射的方法来调用被测应用程序的代码(具体原理??)
    public void testCanOpenSettings(){
    if(solo.searchText("软件更新")){
    solo.clickOnView(solo.getView("button3"));
    }
    solo.pressMenuItem(0);
    }
    @Override
    protected void tearDown() throws Exception {
    // TODO Auto-generated method stub
    solo.finishOpenedActivities();
    super.tearDown();
    }
    }
  2. 运行Robotium例子:在测试类中单击鼠标右键,选择Run As->Android JUnit Test.

Robotium API简介

官网(http://recorder.robotium.com/javadoc/)

Service 学习要点

发表于 2016-11-21

1.Service概述

Service的主要作用是让系统在后台在后台做一些不与用户交互的操作(例如耗时操作:下载网络资源,长期运行的操作:播放音乐)
Service与Thread的区别:(1)Service不是在一个独立的进程中,它与我们的应用程序在同一进程(process)中 (2)Service也不是一个线程,相反,它是运行在主线程的(即UI线程),因此若我们要在Service中进行耗时操作时,需要开启一个子线程,在其中进行耗时操作,否则很容易出现ANR错误(Application Not Responding 程序无响应)

2.Service用法

ServiceTest.java如下:

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
public class ServiceTest extends Service {
private MyLocalBinder myLocalBinder=new MyLocalBinder();
@Override
public void onCreate() {
super.onCreate();
Log.e("TAG","onCreate");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e("TAG","onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.e("TAG","onBind");
return myLocalBinder;
}
@Override
public boolean onUnbind(Intent intent) {
Log.e("TAG","onUnbind");
return super.onUnbind(intent);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e("TAG","onDestroy");
}
//对外提供的访问方法
public void downLoad(){
Log.e("downLoad","正在下载...");
}
public void undownLoad(){
Log.e("downLoad","取消下载...");
}
class MyLocalBinder extends Binder{
public ServiceTest getServiceTestInstance(){
return ServiceTest.this;
}
//...这里也可以继续写方法对外提供
}
}

与Activity相似,使用Service也需要通过intent,不能忘记的是在使用Service前,需要在AndroidManifest.xml中进行声明

启动方式一:startService()

通过打印Service的生命周期,我们发现第一次启动Service的时候,会执行onCreate()和onStartCommand(),
再次启动时,只会执行onStartCommand(),也就是说onCreate()只会在第一次启动的时候进行初始化
点击“stopService”后,Service被销毁,进入onDestroy()方法。不管我们启动了多少次Service,只要我们在外部调用一次Context.stopService()或者在Service内部调用stopSelf(),Service就会被销毁

上面这种启动方式的缺点:启动完Service后,这个Service就在后台运行了,同时也与启动它的Activity失去了联系,因为不能通过ServiceTest service = new ServiceTest()的方式启动Service,因而我们的Activity中不能获取到ServiceTest的实例。
为了解决与启动Service的组件的通信能力,还有一个解决方案就是通过广播的形式。我们在Activity中发出一些想用操作广播,在Service中注册该广播,Service接收到该广播信息后,完成相应的功能。但是频繁发送广播比较消耗性能,同时,由于广播接受者中的onReceive()中,不能执行长时间的工作,时间超过后,可能就直接跳出了方法。因此,这种方案不是首选。

启动方式二:bindService() Bound机制

通过bindService()方式第一次启动后,会执行onCreate()和onBind()方法,当我们点击“unBindService“时,走的是onUnbind()和onDestroy()方法。如果有另一个组件对同一个Service进行bindService()操作(也就是在bindService()中传入不同的ServiceConnection,此时只会进入onBind()方法,即onCreate()只会在第一次启动的时候进行初始化

总结:可以看到,不管是通过哪种方式启动Service,同一个Service在整个应用程序中只有一个实例存在。区别:(1)两种方式所走的生命周期是不一样的(2)何时被销毁:当我们通过startService()启动时,不管我们启动了多少次Service,只要我们在外部调用一次Context.stopService()或者在Service内部调用stopSelf(),Service就会被销毁;而当我们通过bindService()启动时,前面我们多次启动service后,当所有客户端发出unBindService(),这个Service将被系统销毁。(3)当Service即被startService()启动也被bindService()启动时,这种情况下,Service必须在既没有任何activity关联又停止的情况下,Service才会被销毁。

3.IntentService

我们在第一部分谈到,有时需要在service中进行耗时操作,此时就需要开启一个子线程,而对于这种需求,Android提供了IntentService给用户,intentservice内部已经帮我们开启了线程,我们只需要实现它的onHandleIntent方法,在里面实现我们的功能即可,注:intentservice不能处理多个线程的请求,但是可以处理多个service的请求(此处求解?)

IntentService提供的功能:(1)所有请求处理完成后自动停止服务(2)提供了默认onBind()的实现,直接返回null,意味着我们只能通过startService()的方式启动IntentService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyIntentService extends IntentService {
public MyIntentService(){
super("MyIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
Log.e("MyIntentService","Thread
is"+Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e("OnDestroy","OnDestroy");
}
}

使用时需注意两点:首先要提供一个无参的构造方法,里面调用父类的有参构造方法,第二是实现onHandleIntent这个抽象方法。

4.前台Service

Service默认都是在后台默默运行的,用户基本察觉不到有Service在运行。此时,Service的优先级是比较低的,当系统资源不足的时候,易被销毁。因此,如果我们想让用户知道有Service在后台运行,如音乐播放器,或者想让Service一直保持运行状态,不容易被系统回收,此时,就可以考虑使用前台Service。前台Service是被认为是用户已知的正在运行的服务,当系统需要释放内存时不会优先杀掉该进程。
用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onCreate() {
super.onCreate();
Log.e("TAG","onCreate");
Notification notification=new
Notification(R.mipmap.ic_launcher,"前台通知",System.currentTi meMillis());
Intent intent=new Intent(this,MainActivity.class);
PendingIntent pendingIntent=PendingIntent.getActivity(this,0,
intent,0);
notification.setLatestEventInfo(this, "通知标题",
"前台Service内容", pendingIntent);
//设置到前台运行,第一个参数为通知notification的唯一ID
startForeground(1,notification);
}

(关于Notification在SDK23以后和SDK22之前用法不一样,上面是SDK22以前的,下面是SDK23以后的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Notification.Builder builder=new      
Notification.Builder(getApplication());
builder.setContentInfo("补充内容");
builder.setContentText("主内容区");
builder.setContentTitle("通知标题");
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setTicker("新消息");
builder.setAutoCancel(true);
builder.setWhen(System.currentTimeMillis());
Intent intent = new Intent(getApplication(), MainActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(getApplication(), 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
builder.setContentIntent(pendingIntent);
Notification notification = builder.build();

如果我们要移除这个前台Service,只需要调用stopService()即可

1…34

Forwards

Keep coding

37 日志
GitHub 简书
© 2023 Forwards
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4