Android ListView无数据时显示其他View

大家都遇到过这样的情况,当 ListView 中的数据为空时,需要显示一个 “没有数据” 的 View 。想过一些办法,比如 FrameLayout 来 包含 ListView 和 数据为空时的 View,当有数据时隐藏空 View,没有数据时显示空 View。或者是给 ListView 添加一个 header 或 footer。但是今天看到了一种更简单的方案,下面来介绍一下。

先贴一下今天看到的代码

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<TextView
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No items." />

</FrameLayout>

Activity

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 MainActivity extends ListActivity {

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

ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, generateStrings());

setListAdapter(adapter);

}

private String[] generateStrings() {
String[] strings = new String[0];

for (int i = 0; i < strings.length; ++i) {
strings[i] = "String " + i;
}

return strings;
}

}

Activity 是继承自 ListActivity ,到这里代码都能看懂。奇怪的一点是布局文件中 android:id="@android:id/empty" id 是这样写的。在网上查了一下,这个属性的作用就是,当 ListView 关联的 Adapter 的数据为时,就显示这个 id 为 @android:id/empty 的 View。而当数据不为空时,这个 空 View 就不可见。

如果在不是继承自 ListActivity 的话,上面的那个属性没有作用,TextView 会是一直可见的状态。

所以如果是不继承 ListActivity 的情况下,需要在代码中调用 listView.setEmptyView(nullView) 来设置 listView 数据为空时 显示 nullView 。

样例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<ListView
android:id="@+id/myList"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<!-- Here is the view to show if the list is emtpy -->

<TextView
android:id="@+id/myText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No items." />

</FrameLayout>
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
public class MainActivity extends Activity {

private ListView mListView = null;

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

mListView = (ListView) findViewById(R.id.myList);
mListView.setEmptyView(findViewById(R.id.myText));

ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, generateStrings());

mListView.setAdapter(adapter);
}

private String[] generateStrings() {
String[] strings = new String[100];

for (int i = 0; i < strings.length; ++i) {
strings[i] = "String " + i;
}
return strings;
}
}

Android与JS交互

最近的项目里用到了混合开发,需要进行 Android 与 JS 的交互,就了解了一下相关知识。

Android 调用 JS 中的方法

布局就不放出来了。

html 代码如下。两个按钮,分别创建两个 script 方法,一个有参,一个无参。

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
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>jsandroid_test</title>

<script type="text/javascript" language="javascript">
function showFromHtml(){
document.getElementById("id_input").value = "Java call Html";
}

function showFromHtml2( param ){
document.getElementById("id_input2").value = "Java call Html : " + param;
}
</script>
</head>

<body>

<input id="id_input" style="width: 90%" type="text" value="null" />
<br>
<input type="button" value="JavacallHtml" onclick="window.jsObj.JavacallHtml()" />

<br>
<input id="id_input2" style="width: 90%" type="text" value="null" />
<br>
<input type="button" value="JavacallHtml2" onclick="window.jsObj.JavacallHtml2()" />

</body>
</html>

android 代码

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
private void showWebView(){     // webView与js交互代码  
try {
mWebView = new WebView(this);
setContentView(mWebView);

mWebView.requestFocus(); //触摸焦点起作用,如果不设置,则在点击网页文本输入框时,不能弹出软键盘及不响应其他的一些事件。

// 设置 WebChromeClient
mWebView.setWebChromeClient(new WebChromeClient(){
@Override
public void onProgressChanged(WebView view, int progress){
JSAndroidActivity.this.setTitle("Loading...");
JSAndroidActivity.this.setProgress(progress);

if(progress >= 80) {
JSAndroidActivity.this.setTitle("JsAndroid Test");
}
}
});

WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true); //支持 JavaScript
webSettings.setDefaultTextEncodingName("utf-8");

mWebView.addJavascriptInterface(getHtmlObject(), "jsObj"); //设置 JS 接口,第一个参数是事件接口实例,第二个参数是 实例 在 JS 中的别名
mWebView.loadUrl("http://192.168.1.121:8080/jsandroid/index.html");
} catch (Exception e) {
e.printStackTrace();
}
}

private Object getHtmlObject(){

Object insertObj = new Object(){
public void JavacallHtml(){
runOnUiThread(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript: showFromHtml()");
Toast.makeText(JSAndroidActivity.this, "clickBtn", Toast.LENGTH_SHORT).show();
}
});
}

public void JavacallHtml2(){
runOnUiThread(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript: showFromHtml2('IT-homer blog')");
Toast.makeText(JSAndroidActivity.this, "clickBtn2", Toast.LENGTH_SHORT).show();
}
});
}
};

return insertObj;
}

如上所示,在 JS 中新建两个方法。按钮的点击事件是 window.jsObj.JavacallHtml() 其中 window 代表当前界面,jsObj 是 java 里设置的 JS 接口的别名,JavacallHtml() 是 java 里的方法名字。

JS 调用 Android 中的方法

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
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>jsandroid_test</title>

<script type="text/javascript" language="javascript">
function showHtmlcallJava(){
var str = window.jsObj.HtmlcallJava();
alert(str);
}

function showHtmlcallJava2(){
var str = window.jsObj.HtmlcallJava2("IT-homer blog");
alert(str);
}
</script>
</head>

<body>

<br>
<input type="button" value="HtmlcallJava" onclick="showHtmlcallJava()" />
<br>
<input type="button" value="HtmlcallJava2" onclick="showHtmlcallJava2()" />
<br>

</body>
</html>

android 代码

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
private void showWebView(){     // webView与js交互代码  
try {
mWebView = new WebView(this);
setContentView(mWebView);

mWebView.requestFocus(); //触摸焦点起作用,如果不设置,则在点击网页文本输入框时,不能弹出软键盘及不响应其他的一些事件。

// 设置 WebChromeClient
mWebView.setWebChromeClient(new WebChromeClient(){
@Override
public void onProgressChanged(WebView view, int progress){
JSAndroidActivity.this.setTitle("Loading...");
JSAndroidActivity.this.setProgress(progress);

if(progress >= 80) {
JSAndroidActivity.this.setTitle("JsAndroid Test");
}
}
});

WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true); //支持 JavaScript
webSettings.setDefaultTextEncodingName("utf-8");

mWebView.addJavascriptInterface(getHtmlObject(), "jsObj"); //设置 JS 接口,第一个参数是事件接口实例,第二个参数是 实例 在 JS 中的别名
mWebView.loadUrl("http://192.168.1.121:8080/jsandroid/index.html");
} catch (Exception e) {
e.printStackTrace();
}
}

private Object getHtmlObject(){

Object insertObj = new Object(){
public String HtmlcallJava(){
return "Html call Java";
}

public String HtmlcallJava2(final String param){
return "Html call Java : " + param;
}
};

return insertObj;
}

WebViewClient与WebChromeClient的区别

WebViewClient主要帮助WebView处理各种通知、请求事件的,比如:onLoadResource, onPageStart,onPageFinish, onReceiveError, onReceivedHttpAuthRequest, shouldOverrideUrlLoading, 可以进行 url 的跳转链接等处理。

WebChromeClient主要辅助WebView处理JavaScript的对话框、网站图标、网站title、加载进度等比如 onCloseWindow, onCreateWindow,onJsAlert,onProgressChanged, onReceivedTitle 等。

Android面试题集

在这里记录一下面试中遇到的问题,慢慢更新。

数据结构

  • 堆和栈的区别

    堆:先进先出。Java中,创建的对象存放在堆内存里。

    栈:先进后出。Java中,基本数据类型以及其他类的引用放在栈内存里,速度快。

Java基础

  • Java基本数据类型 (8种)

























    数据类型 int short long float double byte char boolean
    字节 4 2 8 4 8 1 2 1
  • String 和 StringBuilder 以及 StringBuffer 区别

Android基础

  • Handler机制

  • 事件分发

  • 线程间通信方式

项目经验

  • Glide源码

  • Android性能优化

面试经验

  • 面试前看下公司的一些要求以及公司给的薪资。
  • 最好是有一些自己深入了解过的东西,比如 Glide 源码之类的。然后主动跟面试官聊自己擅长的东西。
  • 不会的就直说不会。

Handler源码解析

因为 Android 只允许在主线程中更新 UI ,所以每个 Android 开发者都会使用到 Handler ,最近面试也一直在问这个问题。先说一下我自己的理解吧

每个 Activity 都会自动初始化一个 Looper 对象,这个 Looper 对象通过 loop() 方法,不断的遍历 MessageQueue,来查看消息队列里是否存在 Message,在代码里 在其他线程中可以通过 handler.sendMessage(message) 来把 Message 传到 MessageQueue 里,可以在 handler 重写的 handlerMessage(Message msg) 里获取到发送的 Message 来执行所要完成的动作。

从 Handler 的无参构造方法开始看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Handler() {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = null;
}

可以看到在第十行调用了 Looper.myLooper()方法来获取一个 Looper 对象,如果这个对象为空则抛出一个异常。看一下 myLooper() 方法

1
2
3
public static final Looper myLooper() {
return (Looper)sThreadLocal.get();
}

这个方法非常简单,就是从sThreadLocal对象中取出Looper。如果sThreadLocal中有Looper存在就返回Looper,如果没有Looper存在自然就返回空了。因此你可以想象得到是在哪里给sThreadLocal设置Looper了吧,当然是Looper.prepare()方法!我们来看下它的源码:

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

可以看到,首先判断sThreadLocal中是否已经存在Looper了,如果还没有则创建一个新的Looper设置进去。这样也就完全解释了为什么我们要先调用Looper.prepare()方法,才能创建Handler对象。同时也可以看出每个线程中最多只会有一个Looper对象。

所以在非 UI 线程中创建 Handler 对象之前要先调用 Looper.prepare() 方法,否则会出现异常。而且在程序启动时,系统已经自动在主线程中调用了Looper.prepare() 方法。所以在主线程中我们可以直接新建 Handler 。

看完了如何创建Handler之后,接下来我们看一下如何发送消息,这个流程相信大家也已经非常熟悉了,new出一个Message对象,然后可以使用setData()方法或arg参数等方式为消息携带一些数据,再借助Handler将消息发送出去就可以了,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
new Thread(new Runnable() {
@Override
public void run() {
Message message = new Message();
message.arg1 = 1;
Bundle bundle = new Bundle();
bundle.putString("data", "data");
message.setData(bundle);
handler.sendMessage(message);
}
}).start();

可是这里Handler到底是把Message发送到哪里去了呢?为什么之后又可以在Handler的handleMessage()方法中重新得到这条Message呢?看来又需要通过阅读源码才能解除我们心中的疑惑了,Handler中提供了很多个发送消息的方法,其中除了sendMessageAtFrontOfQueue()方法之外,其它的发送消息方法最终都会辗转调用到sendMessageAtTime()方法中,这个方法的源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
{
boolean sent = false;
MessageQueue queue = mQueue;
if (queue != null) {
msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);
}
else {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
}
return sent;
}

sendMessageAtTime() 方法接收两个参数,其中 msg 参数就是我们发送的 Message 对象,而 uptimeMillis 参数则表示发送消息的时间,它的值等于自系统开机到当前时间的毫秒数再加上延迟时间,如果你调用的不是sendMessageDelayed()方法,延迟时间就为0,然后将这两个参数都传递到 MessageQueue 的 enqueueMessage() 方法中。这个 MessageQueue 又是什么东西呢?其实从名字上就可以看出了,它是一个消息队列,用于将所有收到的消息以队列的形式进行排列,并提供入队和出队的方法。这个类是在 Looper 的构造函数中创建的,因此一个 Looper 也就对应了一个 MessageQueue。

那么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
final boolean enqueueMessage(Message msg, long when) {
if (msg.when != 0) {
throw new AndroidRuntimeException(msg + " This message is already in use.");
}
if (msg.target == null && !mQuitAllowed) {
throw new RuntimeException("Main thread not allowed to quit");
}
synchronized (this) {
if (mQuiting) {
RuntimeException e = new RuntimeException(msg.target + " sending message to a Handler on a dead thread");
Log.w("MessageQueue", e.getMessage(), e);
return false;
} else if (msg.target == null) {
mQuiting = true;
}
msg.when = when;
Message p = mMessages;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
this.notify();
} else {
Message prev = null;
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
msg.next = prev.next;
prev.next = msg;
this.notify();
}
}
return true;
}

首先你要知道,MessageQueue 并没有使用一个集合把所有的消息都保存起来,它只使用了一个 mMessages 对象表示当前待处理的消息。然后观察上面的代码的 16~31 行我们就可以看出,所谓的入队其实就是将所有的消息按时间来进行排序,这个时间当然就是我们刚才介绍的 uptimeMilli 参数。具体的操作方法就根据时间的顺序调用 msg.next,从而为每一个消息指定它的下一个消息是什么。当然如果你是通过sendMessageAtFrontOfQueue()方法来发送消息的,它也会调用enqueueMessage()来让消息入队,只不过时间为0,这时会把 mMessages 赋值为新入队的这条消息,然后将这条消息的 next 指定为刚才的 mMessages,这样也就完成了添加消息到队列头部的操作。
现在入队操作我们就已经看明白了,那出队操作是在哪里进行的呢?这个就需要看一看Looper.loop()方法的源码了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static final void loop() {
Looper me = myLooper();
MessageQueue queue = me.mQueue;
while (true) {
Message msg = queue.next(); // might block
if (msg != null) {
if (msg.target == null) {
return;
}
if (me.mLogging!= null) me.mLogging.println(
">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what
);
msg.target.dispatchMessage(msg);
if (me.mLogging!= null) me.mLogging.println(
"<<<<< Finished to " + msg.target + " "
+ msg.callback);
msg.recycle();
}
}
}

可以看到,这个方法从第4行开始,进入了一个死循环,然后不断地调用的MessageQueue的next()方法,我想你已经猜到了,这个next()方法就是消息队列的出队方法。不过由于这个方法的代码稍微有点长,我就不贴出来了,它的简单逻辑就是如果当前MessageQueue中存在mMessages(即待处理消息),就将这个消息出队,然后让下一条消息成为mMessages,否则就进入一个阻塞状态,一直等到有新的消息入队。继续看loop()方法的第14行,每当有一个消息出队,就将它传递到msg.target的dispatchMessage()方法中,那这里msg.target又是什么呢?其实就是Handler啦,你观察一下上面sendMessageAtTime()方法的第6行就可以看出来了。接下来当然就要看一看Handler中dispatchMessage()方法的源码了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

在第5行进行判断,如果mCallback不为空,则调用mCallback的handleMessage()方法,否则直接调用Handler的handleMessage()方法,并将消息对象作为参数传递过去。这样我相信大家就都明白了为什么handleMessage()方法中可以获取到之前发送的消息了吧!

另外除了发送消息之外,我们还有以下几种方法可以在子线程中进行UI操作:

  1. Handler的post()方法

  2. View的post()方法

  3. Activity的runOnUiThread()方法

Handler中的post()方法,代码如下所示:

1
2
3
4
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}

原来这里还是调用了sendMessageDelayed()方法去发送一条消息啊,并且还使用了getPostMessage()方法将 Runnable 对象转换成了一条消息,我们来看下这个方法的源码:

1
2
3
4
5
private final Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

在这个方法中将消息的 callback 字段的值指定为传入的 Runnable 对象。咦?这个 callback 字段看起来有些眼熟啊,喔!在 Handler 的dispatchMessage()方法中原来有做一个检查,如果 Message 的 callback 等于 null 才会去调用handleMessage()方法,否则就调用handleCallback()方法。那我们快来看下handleCallback()方法中的代码吧:

1
2
3
private final void handleCallback(Message message) {
message.callback.run();
}

再来看一下View中的post()方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
public boolean post(Runnable action) {
Handler handler;
if (mAttachInfo != null) {
handler = mAttachInfo.mHandler;
} else {
ViewRoot.getRunQueue().post(action);
return true;
}
return handler.post(action);
}

原来就是调用了Handler中的post()方法,我相信已经没有什么必要再做解释了。
最后再来看一下Activity中的runOnUiThread()方法,代码如下所示:

1
2
3
4
5
6
7
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}

如果当前的线程不等于UI线程(主线程),就去调用Handler的post()方法,否则就直接调用Runnable对象的run()方法。

线程是怎么切换的?

当 A 线程创建 Handler 的时候,同时创建了 Looper 和 MessageQueue,Looper 随即在 A 线程中调用 loop() 方法,而有因为在 loop() 方法中从 MessageQueue 取出消息并进行下一步的操作,所以操作又回到了 A 线程中。

参考文章:Android Handler、Message完全解析,带你从源码的角度彻底理解

LruCache 解析

LRU 全称为 Least Recently Used 即最近最少使用,是一种缓存置换算法。淘汰最长时间未使用的对象。下面是 LruCache 的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final LinkedHashMap<K, V> map;

private int size; //当前缓存内容的大小。它不一定是元素的个数,比如如果缓存的是图片,一般用的是图片占用的内存大小
private int maxSize; // 最大可缓存的大小

private int putCount; // put 方法被调用的次数
private int createCount; // create(Object) 被调用的次数
private int evictionCount; // 被置换出来的元素的个数
private int hitCount; // get 方法命中缓存中的元素的次数
private int missCount; // get 方法未命中缓存中元素的次数

public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}

LinkedHashMap

可以看出,LruCache 是使用 LinkedHashMap 来存储内容的。LinkedHashMap 继承自 HashMap ,不同的是,它是一个双向循环链表,它的每一个数据结点都有两个指针,分别指向直接前驱和直接后继。LinkedHashMap 的 put(key ,value) 方法,我觉得是当 map 中已经插入过 key 的话,把newValue插入到map中,返回之前存储的 oldValue ,否则返回null。

新建一个节点时的插入过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry<K, V> header = this.header;

// Remove eldest entry if instructed to do so.
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}

// Create new entry, link it on to list, and put it into table
LinkedEntry<K, V> oldTail = header.prv;
LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
key, value, hash, table[index], header, oldTail);
table[index] = oldTail.nxt = header.prv = newTail;
}

可以看到,当加入一个新结点时,结构如下:

当accessOrder为true时,更新或者访问一个结点时,它会把这个结点移到尾部,对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;

// Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}

以上代码分为两步,第一步是先把该节点取出来(Unlink e),如下图:

第二步是把这个这个结点移到尾部(Relink e as tail),也就是把旧的尾部的nxt以及头部的prv指向它,并让它的nxt指向头部,把它的prv指向旧的尾部。如下图:

LruCache

safeSizeOf(K key, V value) 是返回 value 对应的 size 大小

通过重写 sizeOf( ) 方法来确定每个 size 的大小

来读一下它的get方法:

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
public final V get(K key) {

if (key == null) {
throw new NullPointerException("key == null");
}

V mapValue;
synchronized (this) { //获取到值时,就返回该值
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}

/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/

//尝试创建一个值,这个方法的默认实现是直接返回null。但是在它的设计中,这个方法可能执行完成之后map已经有了变化。
V createdValue = create(key);
if (createdValue == null) {
return null; // 如果不为没有命名的key创建新值,则直接返回 null
}

synchronized (this) {
createCount++;
//将创建的值放入map中,如果map在前面的过程中正好放入了这对key-value,那么会返回放入的value
mapValue = map.put(key, createdValue);

if (mapValue != null) {
//如果不为空,说明不需要我们所创建的值,所以又把返回的值放进去
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
//为空,说明我们更新了这个key的值,需要重新计算大小
size += safeSizeOf(key, createdValue);
}
}

//上面放入的值有冲突
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);// 通知之前创建的值已经被移除,而改为mapValue
return mapValue;
} else {
trimToSize(maxSize);//没有冲突时,因为放入了新创建的值,大小已经有变化,所以需要修整大小
return createdValue;
}
}

LruCache 是可能被多个线程同时访问的,所以在读写 map 时进行加锁。当获取不到对应的 key 的值时,它会调用其 create(K key) 方法,这个方法用于当缓存没有命名时计算一个 key 所对应的值,它的默认实现是直接返回 null。这个方法并没有加上同步锁,也就是在它进行创建时,map 可能已经有了变化。
所以在 get 方法中,如果 create(key) 返回的 V 不为 null,会再把它给放到 map 中,并检查是否在它创建的期间已经有其他对象也进行创建并放到 map 中了,如果有,则会放弃这个创建的对象,而把之前的对象留下,否则因为我们放入了新创建的值,所以要计算现在的大小并进行 trimToSize。
trimToSize 方法是根据传进来的 maxSize,如果当前大小超过了这个 maxSize,则会移除最老的结点,直到不超过。代码如下:

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 void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}

if (size <= maxSize || map.isEmpty()) {
break;
}

Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}

entryRemoved(true, key, value, null);
}
}

再来看put方法,它的代码也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}

V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, value);
}

trimToSize(maxSize);
return previous;
}

主要逻辑是,计算新增加的大小,加入 size ,然后把 key-value 放入 map 中,如果是更新旧的数据(map.put(key, value) 会返回之前的 value),则减去旧数据的大小,并调用 entryRemoved(false, key, previous, value) 方法通知旧数据被更新为新的值,最后也是调用 trimToSize(maxSize) 修整缓存的大小。

本文参考: Android源码解析——LruCache

Android图片三级缓存

三级缓存来减少不必要的网络请求,节省流量。

  • 内存缓存,优先加载,速度最快
  • 本地缓存,次要加载,速度快
  • 网络缓存,最后加载,速度慢

内存缓存实现

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
public class MemoryCacheUtils {

private LruCache<String, Bitmap> mMemoryCache;

public MemoryCacheUtils() {
long maxMemory = Runtime.getRuntime().maxMemory() / 8;//得到手机最大允许内存的1/8,即超过指定内存,则开始回收
//需要传入允许的内存最大值,虚拟机默认内存16M,真机不一定相同
mMemoryCache = new LruCache<String, Bitmap>((int) maxMemory) {
//用于计算每个条目的大小
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};

}

/**
* 从内存中读图片
*
* @param url
*/
public Bitmap getBitmapFromMemory(String url) {
Bitmap bitmap = mMemoryCache.get(url);
return bitmap;
}

/**
* 往内存中写图片
*
* @param url
* @param bitmap
*/
public void setBitmapToMemory(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
}
}

本地缓存实现

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
public class LocalCacheUtils {

private static final String CACHE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/WerbNews";

/**
* 从本地读取图片
* @param url
*/
public Bitmap getBitmapFromLocal(String url) {
String fileName = null;//把图片的url当做文件名,并进行MD5加密
try {
fileName = EncryptUtil.mapUrl(url);
File file = new File(CACHE_PATH, fileName);
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
return bitmap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 从网络获取图片后,保存至本地缓存
* @param url
* @param bitmap
*/
public void setBitmapToLocal(String url, Bitmap bitmap) {
try {
String fileName = EncryptUtil.mapUrl(url);//把图片的url当做文件名,并进行MD5加密
File file = new File(CACHE_PATH, fileName);

//通过得到文件的父文件,判断父文件是否存在
File parentFile = file.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
//把图片保存至本地
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(file));
} catch (Exception e) {
e.printStackTrace();
}
}
}

网络缓存实现

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class NetCacheUtils {

private LocalCacheUtils mLocalCacheUtils;
private MemoryCacheUtils mMemoryCacheUtils;

public NetCacheUtils(LocalCacheUtils localCacheUtils, MemoryCacheUtils memoryCacheUtils) {
mLocalCacheUtils = localCacheUtils;
mMemoryCacheUtils = memoryCacheUtils;
}

/**
* 从网络下载图片
* @param ivPic 显示图片的imageview
* @param url 下载图片的网络地址
*/
public void getBitmapFromNet(ImageView ivPic, String url) {
new BitmapTask().execute(ivPic, url);//启动AsyncTask
}

/**
* AsyncTask就是对handler和线程池的封装
* 第一个泛型:参数类型
* 第二个泛型:更新进度的泛型
* 第三个泛型:onPostExecute的返回结果
*/
class BitmapTask extends AsyncTask<Object, Void, Bitmap> {

private ImageView ivPic;
private String url;

/**
* 后台耗时操作,存在于子线程中
* @param params
* @return
*/
@Override
protected Bitmap doInBackground(Object[] params) {
ivPic = (ImageView) params[0];
url = (String) params[1];
return downLoadBitmap(url);
}

/**
* 更新进度,在主线程中
* @param values
*/
@Override
protected void onProgressUpdate(Void[] values) {
super.onProgressUpdate(values);
}

/**
* 耗时方法结束后执行该方法,主线程中
* @param result
*/
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
ivPic.setImageBitmap(result);
System.out.println("从网络缓存图片啦.....");

//从网络获取图片后,保存至本地缓存
mLocalCacheUtils.setBitmapToLocal(url, result);
//保存至内存中
mMemoryCacheUtils.setBitmapToMemory(url, result);
}
}
}

/**
* 网络下载图片
* @param url
* @return
*/
private Bitmap downLoadBitmap(String url) {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");

int responseCode = conn.getResponseCode();
if (responseCode == 200) {
//图片压缩
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize=2;//宽高压缩为原来的1/2
options.inPreferredConfig=Bitmap.Config.ARGB_4444;
Bitmap bitmap = BitmapFactory.decodeStream(conn.getInputStream(),null,options);
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
conn.disconnect();
}
return null;
}
}

MD5加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EncryptUtil {
public EncryptUtil() {
}

public static String mapUrl(String url) {
StringBuilder builder = new StringBuilder();

try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(url.getBytes());
for (byte b : bytes) {
builder.append(String.format("%02x", b));
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return builder.toString();
}
}

图片加载工具类

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
public class MyBitmapUtils {

private NetCacheUtils mNetCacheUtils;
private LocalCacheUtils mLocalCacheUtils;
private MemoryCacheUtils mMemoryCacheUtils;

public MyBitmapUtils() {
mMemoryCacheUtils = new MemoryCacheUtils();
mLocalCacheUtils = new LocalCacheUtils();
mNetCacheUtils = new NetCacheUtils(mLocalCacheUtils, mMemoryCacheUtils);
}

public void disPlay(ImageView ivPic, String url) {
ivPic.setImageResource(R.mipmap.ic_launcher);
Bitmap bitmap;
//内存缓存
bitmap = mMemoryCacheUtils.getBitmapFromMemory(url);
if (bitmap != null) {
ivPic.setImageBitmap(bitmap);
System.out.println("从内存获取图片啦.....");
return;
}

//本地缓存
bitmap = mLocalCacheUtils.getBitmapFromLocal(url);
if (bitmap != null) {
ivPic.setImageBitmap(bitmap);
System.out.println("从本地获取图片啦.....");
//从本地获取图片后,保存至内存中
mMemoryCacheUtils.setBitmapToMemory(url, bitmap);
return;
}
//网络缓存
mNetCacheUtils.getBitmapFromNet(ivPic, url);
}
}

修改Activity进入和退出动画

新建 Activity 进入和退出的动画

在anim文件夹里面创建两个xml动画文件

activity_fade_in.xml

1
2
3
4
5
6
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="200"
android:fromAlpha="0.0"
android:toAlpha="1.0"/>
</set>

activity_fade_out.xml

1
2
3
4
5
6
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="200"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
</set>

创建进入和退出 style

1
2
3
4
5
6
<style name="activity_fade_in_out" parent="android:Animation.Activity">
<item name="android:activityCloseEnterAnimation">@anim/activity_alpha_out</item>
<item name="android:activityCloseExitAnimation">@anim/activity_alpha_out</item>
<item name="android:activityOpenEnterAnimation">@anim/activity_alpha_in</item>
<item name="android:activityOpenExitAnimation">@anim/activity_alpha_in</item>
</style>

创建应用动画的 style

1
2
3
<style name="fade_in_out_theme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowAnimationStyle">@style/activity_fade_in_out</item>
</style>

在清单文件中设置 Activity 的Theme

1
2
3
<activity
android:name=".Main2Activity"
android:theme="@style/fade_in_out_theme" />

AS导入jni项目

最近遇到了一个问题,老板让看关于android直播推流方面的资料,因为觉得像百度云,腾讯云等的封装的太厉害,所以打算自己做推流的部分。
在网上看到了雷霄骅大神的博客,就把他做的demo的源码下下来了,但是他之前是用 eclipse 写得项目,所以在用AS导入的时候出现了一些问题。

解决
打开 app 的 build.gradle 在 buildTypes 里添加如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sourceSets {
main {
jni.srcDirs = []
}
}
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
commandLine "D:/java/android-ndk-r13b/ndk-build.cmd",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}

其中 D:/java/android-ndk-r13b/ndk-build.cmd 是你电脑的ndk路径下的ndk-build.cmd路径

AS导出so文件

1.新建java类Test.java,在类中声明一个native方法

1
2
3
static {
System.loadLibrary("jnitest");
}

2.进入到类所在文件夹javac Test.java 生成Test.class

3.返回到java目录 javah -jni com.example.jnitest.Test 生成com_example_jnitest_Test.h

4.新建com_example_jnitest_Test.c,把.h里的内容拷过去,写里面的方法

5.在build.gradle添加

1
2
3
4
5
ndk {
moduleName "sffstreamer"
ldLibs "log", "z", "m"
abiFilters "armeabi", "armeabi-v7a", "x86"
}

还有

1
2
3
4
5
sourceSets {
main {
jni.srcDirs = []
}
}

Build.gradle文件如下:

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
android {
compileSdkVersion 24
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.ishow3d.ffmpegjni"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

ndk {
moduleName "sffstreamer"
ldLibs "log", "z", "m"
abiFilters "armeabi", "armeabi-v7a", "x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
sourceSets {
main {
jni.srcDirs = []
}
}
}
}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×

keyboard_arrow_up 回到顶端