Android 远程网页访问本地资源

WebView 打开网页的时候,如果网页里面包含较多的 CSS, JS,图片等资源,可能需要非常长的时间。
为了提高加载速度,我们可以将各个网页通用的资源预先内置到 App 中,在网页从远程服务器加载资源之前,先检查本地是否已经有对应的预置或者预下载资源。
如果根据规则命中本地资源,则让 WebView 直接加载本地资源,当没有找到本地资源的时候,再将控制让渡给 WebView 默认的加载机制。

详见 https://github.com/xesam/WebLocalResource

Handler(三)WeakHandler

本文是对 https://github.com/badoo/android-weak-handler 的原理讲解。

Handler(一)内存泄漏 中已经清楚,
匿名内部类隐式持有外层对象的强引用是导致内存泄漏的一个主因。比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TActivity extends Activity {
private Handler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d("task", "task finished");
}
}, 10000);

}
}

其中 Runnable 就隐式持有外层 TActivity 的引用。除了前文讨论过的在 TActivity 销毁的时候清空消息队列,还有没有其他更自动化的方法呢?

我们先看一下上面代码中的引用关系是怎样的:

1

既然问题是由强引用导致的,那么,我们就看看是不是有办法将强引用替换为弱引用来解决这个问题。下面开始尝试:

1. 将隐式的强引用转换为隐式的弱引用

这个是语言机制,根本没办法修改,所以行不通。

2. 将对 Runnable 的强引用转换为对 Runnable 的弱引用

这里实际持有 Runnable 的是 Message,所以我们可以让 Message 持有对 Runnable 弱引用,而不是现在的强引用。
原始代码如下:

1
2
3
4
5
6
7
8
9
public final boolean postDelayed(Runnable r, long delayMillis){
return sendMessageDelayed(getPostMessage(r), delayMillis);
}

private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

按照我们改造 Runnable 弱引用的思路, 稍微修改一下:

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
class WeakHandler {
static class WeakRunnable implements Runnable {
private WeakReference<Runnable> mRunnable;

public WeakRunnable(Runnable runnable) {
this.mRunnable = new WeakReference<>(runnable);
}

@Override
public void run() {
Runnable runnable = mRunnable.get();
if (runnable != null) {
runnable.run();
}
}
}

private Handler mHandler;

public WeakHandler() {
mHandler = new Handler();
}

public final boolean postDelayed(Runnable r, long delayMillis) {
WeakRunnable weakRunnable = new WeakRunnable(r);
return mHandler.postDelayed(weakRunnable, delayMillis);
}
}

这样行不行呢,显然是不行的。因为在这种情况下,引用情况如下:

1

上图说明,就算在 Activity 还没有被销毁的时候,也只有一个弱引用持有 Runnable ,也就以意味着这个 Runnable 随时都会被回收,这显然不符合我们的意图。
因此,我们还需要想办法在 Activity 存活的时候,保证 Runnable 也一定存活。
因此,只需要让 Activity 持有 Runnable 的强引用即可,为了方便,这个任务交给 WeakHandler 来处理,因为 Activity 直接持有 WeakHandler,如果 WeakHandler 也直接持有 Runnable,
效果就相当于 Activity 直接持有 Runnable。

修改如下,在 WeakHandler 里面保持一个实际的 Runnable 集合,这样就可以避免 Runnable 被回收。
在实际的 Runnable 执行完毕后,将 Runnable 从集合中移除,释放不必要的资源。
当然,这一步的释放与 Activity 销毁并没有关系,因为如果 Activity 被销毁,这个 Runnable 集合一样会被回收。

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
42
43
44
45
46
47
48
49
50
51
public class WeakHandler {

private static class RunnableRefs {
private List<Runnable> mRunnables = new ArrayList<>();

public RunnableRefs() {
}

public void add(Runnable runnable) {
mRunnables.add(runnable);
}

public void remove(Runnable runnable) {
mRunnables.remove(runnable);
}
}

private static class WeakRunnable implements Runnable {
private RunnableRefs mRunnableRefs;
private WeakReference<Runnable> mRunnable;

public WeakRunnable(Runnable runnable, RunnableRefs runnableRefs) {
this.mRunnableRefs = runnableRefs;
this.mRunnable = new WeakReference<>(runnable);
mRunnableRefs.add(runnable);
}

@Override
public void run() {
Runnable runnable = mRunnable.get();
if (runnable != null) {
mRunnableRefs.remove(runnable);
runnable.run();
}
}
}

private RunnableRefs mRunnableRefs;
private Handler mHandler;

public WeakHandler() {
mHandler = new Handler();
mRunnableRefs = new RunnableRefs();
}

public final boolean postDelayed(Runnable r, long delayMillis) {
WeakRunnable weakRunnable = new WeakRunnable(r, mRunnableRefs);
return mHandler.postDelayed(weakRunnable, delayMillis);
}

}

上面有一点需要特别强调:

Activity 直接持有 WeakHandler,同时 WeakHandler 也直接持有 Runnable

这句话的意思就是要求 WeakHandler 是 Activity 的一个实例变量。类似下面的写法是一个反例:

1
2
3
4
5
6
7
new WeakHandler().postDelayed(new Runnable() {

@Override
public void run() {
//xxxx
}
}, 50_000);

WeakHandler 对象是一个局部变量,等方法执行完毕,WeakHandler 就被回收了,等待执行的 Runnable 又成了一个孤儿,一样逃脱不了被回收的命运。

总结

java 的回收机制依赖于引用,而引用的根节点最后都会落到线程上面,如果想让 jre 自动回收某一个对象或者一组对象,
那就让这一组对象相互之间形成一个自封闭空间就行了。
也就是说,让根节点无法通过强引用搜索找到这组对象,就像现实中一样,封闭的小团体圈子,一旦上头没有人,自然就被打掉了。

当然,原理归原理, GC 的时机是没法控制的,所以,最保险的时机还是手动清空 Handler 的消息队列,个人认为,这样才靠谱。

如何写一个JsBridge

Android JsBridge 就是用来在 Android app的原生 java 代码与 javascript 代码中架设通信(调用)桥梁的辅助工具。

原文地址点这里

github点这里

使用方式戳这里

有问题请联系 xesam

或者 QQ 群 315658668

原理概述

Javascript 运行在 WebView 中,而 WebView 只是 Javascript 执行引擎与页面渲染引擎的一个包装而已。

由于这种天然的隔离效应,我们可以将这种情况与 IPC 进行类比,将 Java 与 Javascript 的每次互调都看做一次 IPC 调用。
如此一来,我们可以模仿各种已有的 IPC 方式来进行设计,比如 RPC。本文模仿 RPC 机制来实现一个 JsBridge。

首先回顾一一下基于 Binder 的经典 RPC 调用:

Javascript-bridge-rpc

当然,client 与 server 只是用来区分通信双方责任的叫法而已,并不是一成不变的。
对于 java 与 javascript 互调的情况,当 java 主动调用 javascript 的时候,java 充当 client 角色,javascript 则扮演 server 的角色,
javascript 中的函数执行完毕后回调 java 方法,这个时候,javascript 充当 client 角色,而 java 则承担 server 的责任。

Javascript-bridge-circle

剩下的问题就是怎么来实现这个机制了,大致有这么几个需要解决的问题:

  1. java 如何调用 Javascript
  2. Javascript 如何调用 java
  3. 方法参数以及回调如何处理
  4. 通信的数据格式是怎样的

下面逐个讨论这些问题:

1. java 如何调用 Javascript

要实现 Java 与 Javascript 的相互调用,有两条途径可以考虑:

  1. 集成一个定制化的 Javascript 与 Html 渲染引擎,java 通过引擎底层与 Javascript 交互。这样可以获得完全的控制权。
  2. 使用 Android Sdk 提供的交互方法。

对于第一种途径,代价比较大,而且技术方案比较复杂,一般只有基于 Javascript 的跨平台开发方案才会这么做。
所以,现在着重考查第二种途径。

Android 的默认 Sdk 中, Java 与 Javascript 的一切交互都是依托于 WebView 的,大致有以下几个可用方法:

第一:

1
webView.loadUrl("javascript:scriptString"); //其中 scriptString 为 Javascript 代码

第二,在 KITKAT 之后,又新增了一个方法:

1
2
3
4
5
6
webView.evaluateJavascript(scriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {

}
});//其中 scriptString 为 Javascript 代码,ValueCallback 的用来获取 Javascript 的执行结果。这是一个异步掉用。

这个调用看起比上面的正常,而且更像是一个方法调用。

需要注意的是,ValueCallback 并不是在 UI 线程里面执行的。
修正:evaluateJavascript 和 ValueCallback 都是在 UI 线程调用和触发的,
文档参见 [https://developer.android.google.cn/reference/android/webkit/WebView.html#evaluateJavascript](https://developer.android.google.cn/reference/android/webkit/WebView.html#evaluateJavascript(java.lang.String, android.webkit.ValueCallback<java.lang.String>))
感谢 “jordan大师兄” 的指正。

2. Javascript 如何调用 java

要实现 Javascript 调用 java 方法,需要先在 Javascript 环境中注入一个 Java 代理:

1
2
3
4
5
6
7
8
9

class JavaProxy{
@JavascriptInterface //注意这里的注解。出于安全的考虑,4.2 之后强制要求,不然无法从 Javascript 中发起调用
public void javaFn(){
//xxxxxx
};
}

webView.addJavascriptInterface(new JavaProxy();, "java_proxy");

然后在 Javascript 环境中直接调用 obj_proxy 代理上的方法即可。

1
2

java_proxy.javaFn();

这里有两个方面需要统一:

  1. Javascript 的执行方法比较怪异,所以,我们需要将概念统一化。
  2. 如果需要执行的方法比较多,那么,代理对象上也需要定义非常多的方法,我们需要将各种方法定义统一起来管理。

所以,我们先将 Javascript 的执行包装成类似 java 一样的代理对象,然后通过在各自的 stub 上注册回调来增加功能支持。
比如,如果 java 想增加 getPackageName 方法,那么,直接在 JavaProxy 上注册即可:

1
2
3
4
5
6
7

javaProxy.register("getPackageName", new JavaHandler(){
@Override
public void handle(Object value){
//xxxxx
}
})

如图:

Javascript-bridge-register

3. 方法参数以及回调如何处理

很显然,任何 IPC 通信都涉及到参数序列化的问题, 同理 java 与 Javascript 之间只能传递基础类型(注意,不单纯是基本类型),包括基本类型与字符串,不包括其他对象或者函数。
由于只涉及到简单的相互调用,这里就可以考虑采用 JSON 格式来传递各种数据,轻量而简洁。

Java 调用 Javascript 没有返回值(这里指 loadUrl 形式的调用),因此如果 java 端想从 Javascript 中获取返回值,只能使用回调的形式。
但是在执行完毕之后如何找到正确的回调方法信息,这是一个重要的问题。比如有下面的例子:

在 java 环境中,JavaProxy 对象有一个无参数的 getPackageName 方法用来获取当前应用的 PackageName。
获取到 packageName 之后,传递给 Javascript 调用者的对应回调中。

在 Javascript 环境中,获取当前应用的 PackageName 的大致调用如下:

1
2
3
4
bridge.invoke('getPackageName', null, function(packageName){
console.log(packageName);
});

显然

1
2
3
4
function(packageName){
console.log(packageName);
}

这个 Javascript 函数是无法传递到 java 环境中的,所以,可以采取的一个策略就是,
在 Javascript 环境中将所有回调统一管理起来,而只是将回调的 id 传递到 java 环境去,java 方法执行完毕之后,
将回调参数以及对应的回调 id 返回给 Javascript 环境,由 Javascript 来负责执行正确的回调。

这样,我们就可以实现一个简单的回调机制:

在 java 环境中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class JavaProxy{
public void onTransact(String jsonInvoke, String jsonParam){
json = new Json(jsonInvoke);
invokeName = json.getInvokeName(); // getPackageName
callbackId = json.getCallbackId(); // 12345678xx
invokeParam = new Param(jsonParam);// null

...
...

JsProxy.invoke(callbackId, callbackParam); //发起 Javascript 调用,让 Javascript 去执行对应的回调
}
}

在 javascript 环境中

1
2
3
4
5
6
7
8
9
10
11
12
13
14

bridge.invoke = function(name, param, callback){
var callbackId = new Date().getTime();
_callbacks[callbackId] = callback;
var invoke = {
"invokeName" : name,
"callbackId" : callbackId
};
JavaProxy.onTransact(JSON.stringify(invoke), JSON.stringify(param));
}

bridge.invoke('getPackageName', null, function(packageName){
console.log(packageName);
});

反之亦然。

4. 通信的数据格式是怎样的

问题都处理了,只需要设计对应的协议即可。
按照上面的讨论,

在 client 端,我们使用:

1
Proxy.transact(invoke, callback);

来调用 server 端注册的方法。

在 server 端,我们使用:

1
Stub.register(name, handler);

来注册新功能,使用

1
Stub.onTransact(invoke, handler);

来处理接收到的 client 端调用。

其中,invoke 包含所要执行的方法以及回调的信息,因此,invoke 的设计如下:

1
2
3
4
5
6
{
_invoke_id : 1234,
_invoke_name : "xxx",
_callback_id : 5678,
_callback_name : "xxx"
}

注意 _invoke_id 与 _invoke_name 的区别:

如果当前 invoke 是一个直接方法调用,那么 _invoke_id 应该是无效的。
如果当前 invoke 是一个回调,那么 _invoke_id + _invoke_name 共同决定回调的具体对象

需要注意的问题

1. 回调函数需要及时删除,不然会引起内存泄漏。

由于我们使用一 Hash 来保存各自环境中的回调函数。如果某个回调由于某种原因没有被触发,那么,这个引用的对象就永远不会被回收。
针对这种问题,处理方案如下:

在 Java 环境中:

如果 WebView 被销毁了,应该手动移除所有的回调,然后禁用 javascript 。
另外,一个 WebView 可能加载多个 Html 页面,如果页面的 URL 发生了改变,这个时候也应该清理所有的回调,因为 Html 页面是无状态的,也不会传递相互数据。
这里有一点需要注意的是,如果 javascript 端是一个单页面应用,应该忽略 url 中 fragment (也就是 # 后面的部分) 的变化,因为并没有发生传统意义上的页面跳转,
所有单应用的 Page 之间是可能有交互的。

在 javascript 环境中:

javascript 端情况好很多,因为 WebView 会自己管理每个页面的资源回收问题。

使用

必要配置

请在对应的 html 页面中引入

1
<script src="js-bridge.js"></script>

Java 环境

初始化 JsBridge:

1
jsBridge = new JsBridge(vWebView);

加入 url 监控:

1
2
3
4
5
6
7
8
9
vWebView.setWebViewClient(new WebViewClient() {

@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e("onPageFinished", url);
jsBridge.monitor(url);
}
});

Java 注册处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
jsBridge.register(new SimpleServerHandler("showPackageName") {
@Override
public void handle(String param, ServerCallback serverCallback) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
String packageName = getPackageName();
Tip.showTip(getApplicationContext(), "showPackageName:" + packageName);
}
});
}
});

Java 在处理方法中回调 Javascript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

@Override
public void handle(final String param, final ServerCallback serverCallback) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
User user = getUser();
Map<String, String> map = new Gson().fromJson(param, Map.class);
String prefix = map.get("name_prefix");
Tip.showTip(mContext, "user.getName():" + prefix + "/" + user.getName());
if ("standard_error".equals(prefix)) {
Map<String, String> map1 = new HashMap<>();
map1.put("msg", "get user failed");
String userMarshalling = new Gson().toJson(map1);
serverCallback.invoke("fail", new MarshallableObject(userMarshalling));
} else {
String userMarshalling = new Gson().toJson(user);
serverCallback.invoke("success", new MarshallableObject(userMarshalling));
}
}
});
}

Java 执行 Js 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

jsBridge.invoke("jsFn4", new MarshallableString("yellow"), new ClientCallback<String>() {
@Override
public void onReceiveResult(String invokeName, final String invokeParam) {
if ("success".equals(invokeName)) {

new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Tip.showTip(getApplicationContext(), invokeParam);
}
});
}
}

@Override
public String getResult(String param) {
return param;
}
});

销毁 JsBridge

1
2
3
4
5
@Override
protected void onDestroy() {
super.onDestroy();
jsBridge.destroy();
}

Javascript 环境

Javascript 的灵活性比较高,所以要简单一些:

Javascript 注册处理函数:

1
2
3
4
5
6
window.JavaBridge.serverRegister('jsFn4', function (transactInfo, color) {
log("jsFn4:" + color);
title.style.background = color;
log("jsFn4:callback");
transactInfo.triggerCallback('success', 'background change to ' + color);
});

Javascript 执行 Java 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

var sdk = {
getUser: function (params) {
var _invokeName = 'getUser';
var _invokeParam = params;
var _clientCallback = params;
window.JavaBridge.invoke(_invokeName, _invokeParam, _clientCallback);
}
};

sdk.getUser({
"name_prefix": "standard_error",
"success": function (user) {
log('sdk.getUser,success:' + user.name);
},
"fail": function (error) {
log('sdk.getUser,fail:' + error.msg);
}
})

详细 Demo 请参见 js-bridge-demo 工程

Handler(二)Message面面观

Message 有几种?

Message 有两种:Data Message(数据消息) 与 Task Message(任务消息)

Data Message 是指有携带多个数据参数的 Message。比如:

1
public static Message obtain(Handler h, int what, int arg1, int arg2, Object obj)

其中 arg1, arg2,以及 obj 都是携带的数据。

Task Message 是指发送一个 Runnable 的 Message。比如:

1
public static Message obtain(Handler h, Runnable callback)

其中 callback 就是消息携带的任务。

注意,一个 Message 不可能同时为 Data Message 和 Task Message,而只能是其中之一。
当然,你可以强制给一个 Message 设置所有的属性,但是只会有一种类型起作用。

Message 的特征属性是什么?即,如何区分 Message?

一个 Message 包含四个方面:

  1. Handler # 必须——消息的接收者,同时也是消息的管理者。

  2. Object # 非必须——消息的 token

  3. Integer # 非必须——数据消息独有

  4. Runnable # 非必须——任务消息独有

因此,对于所有的 Message 来说,只要其中任意一个属性不相同,就是不同的 Message。因此,像下面的情况,是不会发生冲突的 。

1
2
Message.obtain(handler1, 1).sendToTarget();
Message.obtain(handler2, 1).sendToTarget();

如何创建 Message

第一种方式,可以直接 new 一个 Message 对象:

1
2
3
4
Message message = new Message();
message.what = 1;
message.setTarget(handler1);
message.sendToTarget();

但是这种方式有两个缺点:

  1. 只能创建 Data Message, 无法创建 Task Message
  2. 每次都创建新的 Message 会增加系统资源。

第二种方式,从消息池里面重用取出。

系统内部维护了一个消息池,推荐的做法是每次需要的时候就从消息池里获取一个,然后系统会自动回收重用。
因此,上面的调用可以修改为:

1
Message.obtain(handler1, 1).sendToTarget();

而且这种方式也可以创建 Task Message:

1
2
3
4
5
6
Message.obtain(handler1, new Runnable() {
@Override
public void run() {

}
});

第三种方式,从另一个消息拷贝而来:

1
Message.obtain(message).sendToTarget();

如何发送 Message

消息最终的接受者肯定是一个 Handler,因此,Message 的四特征里面,Handler必不可少。
发送消息有两种方式,从 Message 发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    Message.obtain(handler1, 1).sendToTarget();
```

从 Handler 发送:

```java
new Handler().post(new Runnable() {
@Override
public void run() {

}
});

//或者

new Handler().sendEmptyMessage()

有一个简单的区分:

send 前缀的方法用来发送 Data Message

post 前缀的方法用来发送 Task Message

本质上,以上两种方式是一样的,因为 Message.sendToTarget() 方法最终还是委托给内部的 Handler 来处理,这么做只是使用起来更方便而已。

而返回值(boolean)都是用来表明消息是否成功发送。

Message 的处理

Task Message 会在指定的时间直接在 Handler 线程被执行,Handler 本身无法接收到任何回调。

而对于 Data Message 来说,处理方法就比较丰富一些了。Handler 通过

1
Handler.handleMessage(Message msg)

来处理。

Handler 处理的方式也有两种:

第一种,通过继承的方式:

1
2
3
4
5
6
7
public static class CbkHandler extends Handler {

@Override
public void handleMessage(Message msg) {
//handle message
}
}

第二种,通过回调的方式:

1
2
3
4
5
6
7
new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//handle message
return true;
}
});

对于第二种方式的返回值,true 表示已经处理了消息, false 表示没有处理消息。如果没有处理消息,那么消息将会被投递到 Handler 本身的 handleMessage(Message msg) 方法,也就是第一种方式。

如何移除消息

移除 Message 同样要参考 Message 的四个特征。只要相应的特征符合,就可以移除对应的消息。

移除 Data Message 使用 removeMessages 方法:

1
2
Handler.removeMessages(int what) //移除所有 what 值匹配的 Message
Handler.removeMessages(int what, Object token) //移除所有 what 值匹配并且 token 也相同的 Message

移除 Task Message 使用 removeCallbacks 方法:

1
2
Handler.removeCallbacks(Runnable r) //移除所有 r 相同的 Message
Handler.removeCallbacks(Runnable r, Object token) //移除所有 r 相同并且 token 也相同的 Message

由于 token 是两类消息共有的,因此,可以通过 token 来同时移除两类消息:

1
removeCallbacksAndMessages(Object token) //移除所有 token 相同的 Message

如果 token 为 null,效果就是清空所有的消息队列。

Message 的生命周期

1

注意,在正式的程序开发中,是没有手段来检测 Message 的生命周期状态的。而且,也不应该持续持有一个 Message,更不应该在一个 Message 发送之后,再对其进行修改。
因为,既无法确认 Message 的状态,而且 Message 有可能被重用,会直接影响到下一个处理逻辑。

观察 Message Queue

系统提供了两种方式来观察当前消息队列的情况:

第一种,在 Handler 上调用 Handler.dump(Printer pw, String prefix) 来输出当前的消息队列情况:

1
handler.dump(new LogPrinter(Log.DEBUG, "HandlerDemo"), "xesam");

示例结果如下;

xesam  Looper (main, tid 1) {44bd1af0}
xesam    Message 0: { when=-9ms barrier=62 }
xesam    Message 1: { when=+10s0ms callback=dev.xesam.android.demo.system.handler.HandlerDemo$2 target=android.os.Handler callback=dev.xesam.android.demo.system.handler.HandlerDemo$2@44d5ae18 target=Handler (android.os.Handler) {44bd7758} }
xesam    (Total messages: 2, idling=false, quitting=false)

第二种,可以使用 Looper#setMessageLogging(Printer printer) 来监控消息的处理过程。

1
Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "xesam"));

示例结果如下,展示了每一个 Message 的处理情况:

D/xesam: >>>>> Dispatching to Handler (android.os.Handler) {44d27750} dev.xesam.android.demo.system.handler.HandlerDemo$2@44d27a20: 0
D/xesam: <<<<< Finished to Handler (android.os.Handler) {44d27750} dev.xesam.android.demo.system.handler.HandlerDemo$2@44d27a20

这是主要的调试手段,而且比较实用。

推荐书籍

《Efficient Android Threading》

Handler(一)内存泄漏

内存泄漏的最终原因无非就是逻辑上应该被回收的资源实际上无法被回收。

如果我们将 Activity 看做一个封闭环境,那么导致内存泄漏的原因通常就是:实际上有外部环境中的其他存活对象持有这个逻辑上应该被回收的 Activity 的强引用。

回到 Handler,Handler 内部肯定不存在内存泄露的问题,那问题肯定处在我们的写法上,而主要的问题就是在延时 Message 中持有强引用。
因为虽然 Handler 跟随 Activity 的生命周期,但是 Message 却是跟随着执行线程的生命周期。

1. 显式引用

在延时回调中直接持有 Activity:

1
2
3
4
5
6
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
activity.setVisibility(View.VISIBLE);
}
}, 10000);

虽然使用了静态类,但是内部持有了 View, view 又持有外层 Activity 引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static class CbkHandler extends Handler {

private View mView;

public CbkHandler(View view) {
mView = view;
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mView.setVisibility(View.VISIBLE);
}
}

new CbkHandler(view).sendEmptyMessage(0);

2. 隐式引用

匿名内部隐式持有外层类的引用,比如 Runnable 持有外层类的引用,如果外层类是一个 Activity,那么这个 Activity 就只会在 10 秒之后才可能被回收。

1
2
3
4
5
6
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//op
}
}, 10000);

如何解决

  1. 及时清理延时 Message,如果没有延时 Message,那么顶多可能会存在短时间的内存泄漏,而不会产生大问题。此时的风险是,在 Message 中可能引用了已经被销毁的 View 或者其他,导致空指针。
  2. 避免使用匿名内部类,在静态类中使用弱引用来引用外层的 View 或者 Activity。
  3. 与第 2 条类似,但是是对整个 Runnable 对象持有弱引用,这样无论 Runnable 里面是怎样的,都不会影响到外层的内存回收。参见android-weak-handler
    weak-handler 的问题就是必须要求外层类持有 weak-handler 的强引用,不然 weak-handler 自身也会被回收掉。

补充

更多内存泄漏请参考:

  1. https://techblog.badoo.com/blog/2014/08/28/android-handler-memory-leaks
  2. 内存泄露从入门到精通三部曲之基础知识篇
  3. 内存泄露从入门到精通三部曲之排查方法篇
  4. 内存泄露从入门到精通三部曲之常见原因与用户实践

Android 如何快速构建Android Demo

在 Android 学习的过程中,经常需要针对某些项目来写一些测试的例子,或者在做一些 demo 的时候,都需要先写 Activity 然后注册。
如果里面有太多的跳转的话,还需要每个跳转都增加一个事件。这些都是非常繁琐的步骤。那么如何省略这些步骤呢?

有一种办法就是使用 Fragment,然后按照“约定大于配置”的原则,遍历安装包下符合条件的 Fragment 然后自动构建目录索引与跳转动作。
通俗来讲,就是把 APK 里面的包结构文件当做树形结构的文件夹来处理,然后构建一个文件浏览器。当然,我们构建的“类浏览器”。

一个完整的实现请参考 https://github.com/xesam/AndroidQuickDemo

PS:AndroidQuickDemo 同时增加了 Activity 的支持,但是由于 Android 系统的限制,Activity必须被注册,这一点是无法绕过去的。
当然,也可以使用插件的原理,达到自动索引 Activity 的目的,不过,在我的实际使用中,当一定要使用 Activity 的时候,肯定是为了使用或者探究 Activity 的直接效果,而不应该进行代理或者拦截。

使用方式

compile 'dev.xesam.android:quick-demo-creator:0.2.0'

使用方式

现在支持两种模式:

1. 列出所有已经注册的 Activity,点击即可打开

这个方式只是使用一个列表简单列出所有的已注册 Activity,然后点击即可打开。

用法:

QuickDemo.inflateActivity(activity, R.id.listview);

2. 像文件管理器一样,列出 app 的目录索引

个人比较推荐这种用法,直接构建完整的“类浏览器”。

lib工程里面已经内置了一个 QuickDemoActivity,你只需要在你的 Android 项目中的 AndroidManifest.xml 中注册这个 Activity,并设置为 LAUNCHER Activity 即可

1
2
3
4
5
6
7
8
9
<activity
android:name="dev.xesam.android.quickdemo.QuickDemoActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

如果你不想使用内置的 QuickDemoActivity,那么也可以在自己的 Activity 中来显示目录,只需要提供一个 container view id 就可以了,示例如下:

1
2
3
4
5
6
7
8
9
10
public class MyManActivity extends FragmentActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
QuickDemo.inflateActivity(this, R.id.lv);
}
}

默认的目录索引只会显示当前 PackageName 下名称中包含 “demo 或者 sample 或者 example”的 Activity 或者 Fragment,
如果想按照自己的规则来定义过滤,可以自定义 QuickDemoFilter,一个示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomFilter implements QuickDemoFilter {

Pattern target = Pattern.compile("demo|sample|example", Pattern.CASE_INSENSITIVE);
String pkgName;

public CustomFilter(Context context) {
pkgName = context.getPackageName();
}

@Override
public boolean filter(String className) {
String[] comps = className.split("\\.");
String simpleClassName = comps[comps.length - 1];
return className.startsWith(pkgName) && target.matcher(simpleClassName).find() && simpleClassName.indexOf("$") == -1;
}
}

然后在对应的 Activity 中:

1
2
3
4
5
6
7
8
9
10
public class MyManActivity extends FragmentActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
QuickDemo.inflateDemo(this, R.id.quick_demo_root, new CustomFilter(this));
}
}

效果预览

Screenshot_2015-08-12-23-36-42.png

Screenshot_2015-08-12-23-36-47.png

Screenshot_2015-08-12-23-36-53.png

Volley线程池的问题

Volley 中有一个专门负责处理网络请求的线程池。
虽说是线程池,但是实际上是一个默认长度为 4 的线程数组,然后使用一个 BlockingQueue 来保存请求列表,是一个典型的生产-消费者模式应用。

1
2
3
4
5
6
7
8
9
10
11

DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
NetworkDispatcher[] mDispatchers = new NetworkDispatcher[DEFAULT_NETWORK_THREAD_POOL_SIZE];

// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}

我阅读源码的一个问题是:为什么不直接使用 java 并发包里面的 Executor 来处理线程池的问题?
这个问题并没有官方的正式回复,所以,也没有一个具体的答案。所以我只能对比一下两种实现方式的优缺点,来谈一下自己的看法。

首先需要明确的就是:Volley 的设计意图是适用于“小而频繁”的网络请求场景。

使用线程数组的优点

  1. 这样实现的代码比较简单,可以有完全的控制,可以比较方便的对任务线程进行定制。
  2. 由于一开始就保证会有 4 个(默认数量)线程贯穿整个生命周期,因此免除了动态创建或者回收线程的开销。
    如果对应用的网络请求有一个比较好的评估,那么具体的线程数量还可以再斟酌。

其他,我找不到比较好的理由,因为,相同的功能使用 Executor 也完全可以实现。

使用线程数组的缺点

我们在使用过程中,通常是在 app 启动的时候进行 Volley 请求队列初始化,在应用结束的时候销毁 Volley 请求队列。那么,在使用的生命周期过程中,可能出现下面几个问题:

  1. 当请求数量持续超负荷的时候,无法自动调整线程数量。由于使用的数组形式,并且也没有提供动态容量控制机制,无法根据实际情况伸缩应对。
    这个时候就有可能导致请求的超预期延迟,特别是在网络状况不是很好(会触发 retry 机制,占用线程资源)而用户刷新比较频繁的场景下,表现会比较突出。
  2. 如果某个线程由于某种异常被终止,将无法再次恢复,这个问题在 Volley 中并没有处理过。
    一个极端情况是,如果默认的线程全部死掉,那么网络请求将崩溃。

另外,Volley 内置了对网络图片的请求处理,但是需要注意的是,
如果图片比较大或者比较多的情况下,需要在主要的 Volley 业务请求队列之外,重新开一个新的图片请求队列来处理,不然,极有可能阻塞了正常的业务请求。
其实个人觉得,Volley 没必要增加对图片的处理,既没有处理好,又不够强大。还是使用专业的 Glide 或者 Fresco 来处理。

可能这些情况比较极端,但是都是可能出现问题的地方。所以,个人觉得,Volley 还是有很大的改进空间的。

最大公因数与最小公倍数

1.求正整数 a, b 的最大公因数

a,b 是两个正整数,其中 a > b,则 a 可以写成如下形式:

1
a = kb + r

其中 0 <= r < b

  1. 如果 r 等于 0,则 a = kb,那么最大公因数即为 b
  2. 如果 r 不等于 0,设 a, b 的一个因数为正整数 q,则有:
1
2
a = tq
b = sq

其中 t s 为正整数,得到

1
2
=> tq = ksq + r
=> r = (t - ks)q

因此 q 也是 r 的一个因数,所以,a,b 的公因数同时也是 b,r 的公因数。
同理可证 b,r 的公因数同时也是 a,b 的公因数,因此:

1
a,b 与 b,r 的公因数相同

如果用 gcd(a, b) 表示 a,b 的最大公因素,则

1
gcd(a, b) == gcd(b, r)

程序实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
function gcd(a, b) {
if(a < b){
var tmp = a;
a = b;
b = tmp;
}
var mod = a % b;
if(mod === 0){
return b;
}else{
return gcd(b, mod);
}
}

2.求正整数 a, b 的最小公倍数

a,b 是两个正整数,其中 a > b,则:

1
a = kb + r

其中 0 <= r < b

  1. 如果 r 等于 0,则 a = kb,那么最小公倍数即为 a
  2. 如果 r 不等于 0,设 a, b 的最小公倍数为正整数 q,则有:
1
2
q = sa
q = tb

其中 t s 为正整数。

已知 a,b 的最大公因数为 g,则

1
2
3
4
5
6
q = sa = sug
q = tb = tvg

=> sa = sug = tb = tvg
=> su = tv
=> s/t = v/u

其中 u v 为正整数,且 u v 互质(因为如果 u v 不互质,那么 g 就不是 a,b 的最大公因数)。

同理可知:s t 也一定互质,因为假设 s t 不互质,且有公因数 z,那么存在如下关系:

1
2
3
4
5
s = mz
t = nz

=> q = sa = mza
q = tb = nzb

因此,存在一个正整数 w = ma = nb,w也是 a b 的公倍数,显然 w < q,与 “q 是最小公倍数”相矛盾。
此时可以得出:

1
s/t = v/u

其中 u v 互质,s t 互质。可知:

1
s = vt/u

因此 t 必定可以整数 u,假设 t = xu,其中 x!=1,可以得到 s = xv。与 “s t 互质”相矛盾。因此 x 必定是 1,所以

1
2
s = v
t = u

得到

1
q = sa = va = ab/q

程序实现:

1
2
3
4
function lcm(a, b){
var _gcd = gcd(a, b);
return a * b / _gcd;
}

Android 5.0注意事项

有些时候我们使用Service的时需要采用隐式意图启动的方式。
但是Android 5.0一出来后,其中有个特性就是 Service Intent must be explicit,也就是说从Lollipop开始,service服务必须采用显式意图方式启动.

1
2
3
Intent intent = new Intent();  
intent.setAction("b.aidl.DownLoadService");
bindService(intent, conn, BIND_AUTO_CREATE);

报错:

java.lang.IllegalArgumentException: Service Intent must be explicit 

解决办法:

1
2
3
4
Intent intent = new Intent();  
intent.setAction("b.aidl.DownLoadService");
intent.setPackage("lq.cn.twoapp"); //指定启动的是那个应用(lq.cn.twoapp)中的Action(b.aidl.DownLoadService)指向的服务组件
bindService(intent, conn, BIND_AUTO_CREATE);