【源码分析】java sdk篇–Integer

【源码分析】java sdk篇–Integer

今天用到了Java中Integer.highestOneBit方法,之前还真没注意Integer中还有位运算相关的方法,这里简单列举一下,方便后续查:
a. highestOneBit(i)
如果一个数是0, 则返回0
如果是负数, 则返回 -2147483648 1000,0000,0000,0000,0000,0000,0000,0000(二进制表示的数)
如果是正数, 返回的则是跟它最靠近的比它小的2的N次方
正数、0好解释,至于负数,由于计算机中的数字都是采用补码表示,即反码+1,并且最高位是一个1、表示负数,因此这个符号位作为最终结果返回。
该方法内部实现很简单,具体分析可以参考这篇文章:Java Integer.highestOneBit(i)代码品读 (这文章里顺便复习了一下有关位运算的知识,需要的童鞋可以看看)
b. lowestOneBit(i)
API说明看着有点绕,但其实很简单,就是和上面的highestOneBit相反,返回一个数,这个数最多只包含i二进制表示中最低位的1
内部实现如下:

1
2
3
public static int lowestOneBit(int i) {
return i & -i;
}

仍然是利用了原码、补码、反码的知识:
i=0则返回0;
i不等于0,那么i和-i中一个是原码、一个是反码+1,假定-i是那个反码+1的数,i的二进制表示中最低位是从右往左的第m位,那么i和-i从第m+1位开始,由于分别是正码以及对应的反码,进行安慰与操作后直接变为0,第m位则由于反码加了1的缘故,保留下来,即得结果。
c.int numberOfLeadingZeros(int i)
在指定 int 值的二进制补码表示形式中最高位(最左边)的 1 位之前,零位的数量。(注:从JDK 1.5才有该接口)
d. int numberOfTrailingZeros(int i)
返回指定的 int 值的二进制补码表示形式中最低(“最右边”)的为 1 的位后面的零位个数。(注:从JDK 1.5才有该接口)
e.int bitCount(int i)
返回指定 int 值的二进制表示形式的 1 位的数量。(注:从JDK 1.5才有该接口)
该方法实现看上去有点莫名其妙,分析可以参考这篇文章:求二进制数中1的个数
f.String toBinaryString(int i)
返回二进制表示的字符串
g. String toHexString(int i)
返回十六进制表示的字符串
h.String toOctalString(int i)
返回八进制表示的字符串
i. String toUnsignedString(int i)
返回i对应的unsigned int数字的字符串表示(注:从JDK 1.8才有该接口)
j. String toUnsignedString(int i, int radix)
返回i对应的unsigned int数字的字符串表示,并且根据radix进制进行输出(注:从JDK 1.8才有该接口)。
注意,看源代码可以发现,目前JDK 1.8中的该方法参数radix实现仅支持2~36,radix大于36则按照radix=10进行计算,这一点需要注意。
 
列一下感觉平时没怎么用过、但可能比较有用的方法,其他方法常用就没必要再列举了,就酱紫。

移除UTF8文件的BOM头

移除UTF8文件的BOM头

开发时遇到过的UTF8文件有BOM头、导致文件不能正常解析这问题,BOM是什么这个问题请参考如下地址:
https://en.wikipedia.org/wiki/Byte_order_mark
UTF8 与 UTF8 +BOM 区别
其实就是在文件头部的3个字节:EF BB BF,而且是不可见的,可以用于标示字节编码顺序(Big-Endian/Little- Endian),UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。Windows就是使用BOM来标记文本文件的编码方式的。此处整理一下移除BOM头的方法:
【1】文本编辑器
UltarEdit/Sublime/notepad++都可以方便的转化(比如notepad++“另存为”)
【2】编码实现
平时开发一直用java,此处给出java实现(亲测,参考自:移除UTF-8文件头的bom):

1
2
3
4
5
6
7
8
9
10
11
public static byte[] removeUTF8BOM(byte[] bt) {
    if (bt != null && bt.length > 3) {
        // 前三个字节依次是 EF BB BF
        if (bt[0] == -17 && bt[1] == -69 && bt[2] == -65) {
                byte[] nbs = new byte[bt.length - 3];
                System.arraycopy(bt, 3, nbs, 0, nbs.length);
                return nbs;
        }
    }
    return bt;
}

其他处理方式:[Java处理文件BOM头的方式推荐](http://blog.csdn.net/littleatp2008/article/details/6943215)

【转发】[VirtualBox]如何复制一个虚拟机

【转发】[VirtualBox]如何复制一个虚拟机

原文地址:[VirtualBox]如何复制一个虚拟机

建立好一个虚拟机后,想要复制成两个虚拟机,需要如下操作:

1.复制vdi或者vmdx文件到一个新的目录。

2.在VirtualBox安装目录下有一个VBoxManage工具,可以改变磁盘文件的uuid。

VBoxManage internalcommands sethduuid “path of vdi or vmdx”

3.使用VirtualBox新建虚拟机,选择磁盘的时候,选择已有的磁盘,然后选中第一步中拷贝的文件。

4.建立好之后就可以正常使用了。

如果没有第二步,而直接新建,或者直接将虚拟机的所有文件都拷贝一份的话,会出现错误,UUID已经存在了。

【源码分析】java sdk篇–ConcurrentSkipListMap

【源码分析】java sdk篇–ConcurrentSkipListMap

最近开始看jdk源代码,先分享一个挺好的博客:http://blog.csdn.net/chenssy  chenssy 有关JDK源码的博文写的很细,我就参考他的博文写写其他的内容,已经被chenssy写过的内容我就不再重复啦,估计也没他写的细。另外,如果网上已有相关分析,我可能直接给出链接、做做补充,毕竟本系列属于读书笔记性质,留下个记录免得自己忘记,并不打算写的特别全或是全部原创。下面就开始【源码分析】系列的第一篇:ConcurrentSkipListMap
声明:本博客内容一般采集自互联网,若引用了其他文章或是博客的内容,我会尽量注明出处。如果有转载、未注明出处,或是有侵权的情况,请及时与我沟通(evasnowind@sina.com),我会及时删除。转载请注明本文出处,谢谢。
在java中,
如果我们需要快速存取键值对,可以用HashMap;
如果要求多线程并发存取键值对,可以用ConcurrentHashMap;
如果要求快速存取键值对,并要求键值对按照某种顺序排序,可以用TreeMap;
如果,我们需要快速存取键值对,支持多线程并发存取,还要求数据有序,是否有现成的数据结构?
答案是肯定,那就是ConcurrentSkipListMap。

一、概述

JDK 1.6中引入了ConcurrentSkipListMap这一数据结构,JDK文档描述如下:

A scalable concurrent ConcurrentNavigableMap implementation. The map is sorted according to the natural ordering of its keys, or by a Comparator provided at map creation time, depending on which constructor is used.
This class implements a concurrent variant of SkipLists providing expected average log(n) time cost for the containsKey, get, put and remove operations and their variants. Insertion, removal, update, and access operations safely execute concurrently by multiple threads. Iterators are weakly consistent, returning elements reflecting the state of the map at some point at or since the creation of the iterator. They do not throw ConcurrentModificationException, and may proceed concurrently with other operations. Ascending key ordered views and their iterators are faster than descending ones.
All Map.Entry pairs returned by methods in this class and its views represent snapshots of mappings at the time they were produced. They do not support the Entry.setValue method. (Note however that it is possible to change mappings in the associated map using put, putIfAbsent, or replace, depending on exactly which effect you need.)
Beware that, unlike in most collections, the size method is not a constant-time operation. Because of the asynchronous nature of these maps, determining the current number of elements requires a traversal of the elements, and so may report inaccurate results if this collection is modified during traversal. Additionally, the bulk operations putAll, equals, toArray, containsValue, and clear are not guaranteed to be performed atomically. For example, an iterator operating concurrently with a putAll operation might view only some of the added elements.
This class and its views and iterators implement all of the optional methods of the Map and Iterator interfaces. Like most other concurrent collections, this class does not permit the use of null keys or values because some null return values cannot be reliably distinguished from the absence of elements.

简单概括下关键点:

  • 常用操作,如containsKey/get/put/remove/..,平均时间复杂度为log(n)
  • CURD等操作是线程安全的,可以被多个线程并发执行
  • Iterator是弱一致的
  • Map.Entry反映的是Entry被创建时的数据(我个人理解:由于支持多线程,Entry被创建后Map的数据可能会改变),不支持Entry.setValue方法
  • size()方法不是一个常量时间操作,每次调用都会进行一次遍历来计算当前Map内元素的个数
  • putAll, equals, toArray, containsValue, and clear这些方法并不是原子性的操作
  • key和value都不能是null

二、使用建议

在非多线程的情况下,应当尽量使用TreeMap。此外对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度。
所以在多线程程序中,如果需要对Map的键值进行排序时,请尽量使用ConcurrentSkipListMap,可能得到更好的并发度。
注意,调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个O(log(n))的操作。
引自:Java多线程(四)之ConcurrentSkipListMap深入分析

三、代码分析

ConcurrentSkipListMap内部实现采用了跳跃表,有关跳跃表的分析参见:SkipList 跳表
有关更详细的分析参见:
跳表(SkipList)及ConcurrentSkipListMap源码解析
Java多线程系列–“JUC集合”05之 ConcurrentSkipListMap

【转发】Android中SQLite应用详解

【转发】Android中SQLite应用详解

打算整理一下最近两年做android所学到的东西,代码、文章配套整理一下,但是发现自己其实研究的还很肤浅,写出东西还是没人家写的明白,so,索性直接转发吧,主要是为了日后查起来方便,代码打出一份方便日后搬砖。
下面文章转自:Android中SQLite应用详解 相关代码参见:https://github.com/evasnowind/PrayerUtility/
————华丽的分割线————
上次我向大家介绍了SQLite的基本信息和使用过程,相信朋友们对SQLite已经有所了解了,那今天呢,我就和大家分享一下在Android中如何使用SQLite。
现在的主流移动设备像Android、iPhone等都使用SQLite作为复杂数据的存储引擎,在我们为移动设备开发应用程序时,也许就要使用到SQLite来存储我们大量的数据,所以我们就需要掌握移动设备上的SQLite开发技巧。对于Android平台来说,系统内置了丰富的API来供开发人员操作SQLite,我们可以轻松的完成对数据的存取。
下面就向大家介绍一下SQLite常用的操作方法,为了方便,我将代码写在了Activity的onCreate中:

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
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//打开或创建test.db数据库
SQLiteDatabase db = openOrCreateDatabase("test.db", Context.MODE_PRIVATE, null);
db.execSQL("DROP TABLE IF EXISTS person");
//创建person表
db.execSQL("CREATE TABLE person (_id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR, age SMALLINT)");
Person person = new Person();
person.name = "john";
person.age = 30;
//插入数据
db.execSQL("INSERT INTO person VALUES (NULL, ?, ?)", new Object[]{person.name, person.age});
person.name = "david";
person.age = 33;
//ContentValues以键值对的形式存放数据
ContentValues cv = new ContentValues();
cv.put("name", person.name);
cv.put("age", person.age);
//插入ContentValues中的数据
db.insert("person", null, cv);
cv = new ContentValues();
cv.put("age", 35);
//更新数据
db.update("person", cv, "name = ?", new String[]{"john"});
Cursor c = db.rawQuery("SELECT * FROM person WHERE age >= ?", new String[]{"33"});
while (c.moveToNext()) {
int _id = c.getInt(c.getColumnIndex("_id"));
String name = c.getString(c.getColumnIndex("name"));
int age = c.getInt(c.getColumnIndex("age"));
Log.i("db", "_id=>" + _id + ", name=>" + name + ", age=>" + age);
}
c.close();
//删除数据
db.delete("person", "age < ?", new String[]{"35"});
//关闭当前数据库
db.close();
//删除test.db数据库
// deleteDatabase("test.db");
}

在执行完上面的代码后,系统就会在/data/data/[PACKAGE_NAME]/databases目录下生成一个“test.db”的数据库文件,如图:
上面的代码中基本上囊括了大部分的数据库操作;对于添加、更新和删除来说,我们都可以使用

1
2
db.executeSQL(String sql);
db.executeSQL(String sql, Object[] bindArgs);//sql语句中使用占位符,然后第二个参数是实际的参数集

除了统一的形式之外,他们还有各自的操作方法:

1
2
3
db.insert(String table, String nullColumnHack, ContentValues values);
db.update(String table, Contentvalues values, String whereClause, String whereArgs);
db.delete(String table, String whereClause, String whereArgs);

以上三个方法的第一个参数都是表示要操作的表名;insert中的第二个参数表示如果插入的数据每一列都为空的话,需要指定此行中某一列的名称,系统将此列设置为NULL,不至于出现错误;insert中的第三个参数是ContentValues类型的变量,是键值对组成的Map,key代表列名,value代表该列要插入的值;update的第二个参数也很类似,只不过它是更新该字段key为最新的value值,第三个参数whereClause表示WHERE表达式,比如“age > ? and age < ?”等,最后的whereArgs参数是占位符的实际参数值;delete方法的参数也是一样。
下面来说说查询操作。查询操作相对于上面的几种操作要复杂些,因为我们经常要面对着各种各样的查询条件,所以系统也考虑到这种复杂性,为我们提供了较为丰富的查询形式:

1
2
3
4
db.rawQuery(String sql, String[] selectionArgs);
db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy);
db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);
db.query(String distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);

上面几种都是常用的查询方法,第一种最为简单,将所有的SQL语句都组织到一个字符串中,使用占位符代替实际参数,selectionArgs就是占位符实际参数集;下面的几种参数都很类似,columns表示要查询的列所有名称集,selection表示WHERE之后的条件语句,可以使用占位符,groupBy指定分组的列名,having指定分组条件,配合groupBy使用,orderBy指定排序的列名,limit指定分页参数,distinct可以指定“true”或“false”表示要不要过滤重复值。需要注意的是,selection、groupBy、having、orderBy、limit这几个参数中不包括“WHERE”、“GROUP BY”、“HAVING”、“ORDER BY”、“LIMIT”等SQL关键字。
最后,他们同时返回一个Cursor对象,代表数据集的游标,有点类似于JavaSE中的ResultSet。
下面是Cursor对象的常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c.move(int offset); //以当前位置为参考,移动到指定行
c.moveToFirst(); //移动到第一行
c.moveToLast(); //移动到最后一行
c.moveToPosition(int position); //移动到指定行
c.moveToPrevious(); //移动到前一行
c.moveToNext(); //移动到下一行
c.isFirst(); //是否指向第一条
c.isLast(); //是否指向最后一条
c.isBeforeFirst(); //是否指向第一条之前
c.isAfterLast(); //是否指向最后一条之后
c.isNull(int columnIndex); //指定列是否为空(列基数为0)
c.isClosed(); //游标是否已关闭
c.getCount(); //总数据项数
c.getPosition(); //返回当前游标所指向的行数
c.getColumnIndex(String columnName);//返回某列名对应的列索引值
c.getString(int columnIndex); //返回当前行指定列的值

在上面的代码示例中,已经用到了这几个常用方法中的一些,关于更多的信息,大家可以参考官方文档中的说明。
最后当我们完成了对数据库的操作后,记得调用SQLiteDatabase的close()方法释放数据库连接,否则容易出现SQLiteException。
上面就是SQLite的基本应用,但在实际开发中,为了能够更好的管理和维护数据库,我们会封装一个继承自SQLiteOpenHelper类的数据库操作类,然后以这个类为基础,再封装我们的业务逻辑方法。
下面,我们就以一个实例来讲解具体的用法,我们新建一个名为db的项目,结构如下:
其中DBHelper继承了SQLiteOpenHelper,作为维护和管理数据库的基类,DBManager是建立在DBHelper之上,封装了常用的业务方法,Person是我们的person表对应的JavaBean,MainActivity就是我们显示的界面。
下面我们先来看一下DBHelper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.scott.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "test.db";
private static final int DATABASE_VERSION = 1;
public DBHelper(Context context) {
//CursorFactory设置为null,使用默认值
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
//数据库第一次被创建时onCreate会被调用
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS person" +
"(_id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR, age INTEGER, info TEXT)");
}
//如果DATABASE_VERSION值被改为2,系统发现现有数据库版本不同,即会调用onUpgrade
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("ALTER TABLE person ADD COLUMN other STRING");
}
}

正如上面所述,数据库第一次创建时onCreate方法会被调用,我们可以执行创建表的语句,当系统发现版本变化之后,会调用onUpgrade方法,我们可以执行修改表结构等语句。
为了方便我们面向对象的使用数据,我们建一个Person类,对应person表中的字段,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.scott.db;
public class Person {
public int _id;
public String name;
public int age;
public String info;
public Person() {
}
public Person(String name, int age, String info) {
this.name = name;
this.age = age;
this.info = info;
}
}

然后,我们需要一个DBManager,来封装我们所有的业务方法,代码如下:

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
package com.scott.db;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
public class DBManager {
private DBHelper helper;
private SQLiteDatabase db;
public DBManager(Context context) {
helper = new DBHelper(context);
//因为getWritableDatabase内部调用了mContext.openOrCreateDatabase(mName, 0, mFactory);
//所以要确保context已初始化,我们可以把实例化DBManager的步骤放在Activity的onCreate里
db = helper.getWritableDatabase();
}
/**
* add persons
* @param persons
*/
public void add(List<Person> persons) {
db.beginTransaction(); //开始事务
try {
for (Person person : persons) {
db.execSQL("INSERT INTO person VALUES(null, ?, ?, ?)", new Object[]{person.name, person.age, person.info});
}
db.setTransactionSuccessful(); //设置事务成功完成
} finally {
db.endTransaction(); //结束事务
}
}
/**
* update person's age
* @param person
*/
public void updateAge(Person person) {
ContentValues cv = new ContentValues();
cv.put("age", person.age);
db.update("person", cv, "name = ?", new String[]{person.name});
}
/**
* delete old person
* @param person
*/
public void deleteOldPerson(Person person) {
db.delete("person", "age >= ?", new String[]{String.valueOf(person.age)});
}
/**
* query all persons, return list
* @return List<Person>
*/
public List<Person> query() {
ArrayList<Person> persons = new ArrayList<Person>();
Cursor c = queryTheCursor();
while (c.moveToNext()) {
Person person = new Person();
person._id = c.getInt(c.getColumnIndex("_id"));
person.name = c.getString(c.getColumnIndex("name"));
person.age = c.getInt(c.getColumnIndex("age"));
person.info = c.getString(c.getColumnIndex("info"));
persons.add(person);
}
c.close();
return persons;
}
/**
* query all persons, return cursor
* @return Cursor
*/
public Cursor queryTheCursor() {
Cursor c = db.rawQuery("SELECT * FROM person", null);
return c;
}
/**
* close database
*/
public void closeDB() {
db.close();
}
}

我们在DBManager构造方法中实例化DBHelper并获取一个SQLiteDatabase对象,作为整个应用的数据库实例;在添加多个Person信息时,我们采用了事务处理,确保数据完整性;最后我们提供了一个closeDB方法,释放数据库资源,这一个步骤在我们整个应用关闭时执行,这个环节容易被忘记,所以朋友们要注意。
我们获取数据库实例时使用了getWritableDatabase()方法,也许朋友们会有疑问,在getWritableDatabase()和getReadableDatabase()中,你为什么选择前者作为整个应用的数据库实例呢?在这里我想和大家着重分析一下这一点。
我们来看一下SQLiteOpenHelper中的getReadableDatabase()方法:

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
public synchronized SQLiteDatabase getReadableDatabase() {
if (mDatabase != null && mDatabase.isOpen()) {
// 如果发现mDatabase不为空并且已经打开则直接返回
return mDatabase;
}
if (mIsInitializing) {
// 如果正在初始化则抛出异常
throw new IllegalStateException("getReadableDatabase called recursively");
}
// 开始实例化数据库mDatabase
try {
// 注意这里是调用了getWritableDatabase()方法
return getWritableDatabase();
} catch (SQLiteException e) {
if (mName == null)
throw e; // Can't open a temp database read-only!
Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
}
// 如果无法以可读写模式打开数据库 则以只读方式打开
SQLiteDatabase db = null;
try {
mIsInitializing = true;
String path = mContext.getDatabasePath(mName).getPath();// 获取数据库路径
// 以只读方式打开数据库
db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
if (db.getVersion() != mNewVersion) {
throw new SQLiteException("Can't upgrade read-only database from version " + db.getVersion() + " to "
+ mNewVersion + ": " + path);
}
onOpen(db);
Log.w(TAG, "Opened " + mName + " in read-only mode");
mDatabase = db;// 为mDatabase指定新打开的数据库
return mDatabase;// 返回打开的数据库
} finally {
mIsInitializing = false;
if (db != null && db != mDatabase)
db.close();
}
}

在getReadableDatabase()方法中,首先判断是否已存在数据库实例并且是打开状态,如果是,则直接返回该实例,否则试图获取一个可读写模式的数据库实例,如果遇到磁盘空间已满等情况获取失败的话,再以只读模式打开数据库,获取数据库实例并返回,然后为mDatabase赋值为最新打开的数据库实例。既然有可能调用到getWritableDatabase()方法,我们就要看一下了:

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
public synchronized SQLiteDatabase getWritableDatabase() {
if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
// 如果mDatabase不为空已打开并且不是只读模式 则返回该实例
return mDatabase;
}
if (mIsInitializing) {
throw new IllegalStateException("getWritableDatabase called recursively");
}
// If we have a read-only database open, someone could be using it
// (though they shouldn't), which would cause a lock to be held on
// the file, and our attempts to open the database read-write would
// fail waiting for the file lock. To prevent that, we acquire the
// lock on the read-only database, which shuts out other users.
boolean success = false;
SQLiteDatabase db = null;
// 如果mDatabase不为空则加锁 阻止其他的操作
if (mDatabase != null)
mDatabase.lock();
try {
mIsInitializing = true;
if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
// 打开或创建数据库
db = mContext.openOrCreateDatabase(mName, 0, mFactory);
}
// 获取数据库版本(如果刚创建的数据库,版本为0)
int version = db.getVersion();
// 比较版本(我们代码中的版本mNewVersion为1)
if (version != mNewVersion) {
db.beginTransaction();// 开始事务
try {
if (version == 0) {
// 执行我们的onCreate方法
onCreate(db);
} else {
// 如果我们应用升级了mNewVersion为2,而原版本为1则执行onUpgrade方法
onUpgrade(db, version, mNewVersion);
}
db.setVersion(mNewVersion);// 设置最新版本
db.setTransactionSuccessful();// 设置事务成功
} finally {
db.endTransaction();// 结束事务
}
}
onOpen(db);
success = true;
return db;// 返回可读写模式的数据库实例
} finally {
mIsInitializing = false;
if (success) {
// 打开成功
if (mDatabase != null) {
// 如果mDatabase有值则先关闭
try {
mDatabase.close();
} catch (Exception e) {
}
mDatabase.unlock();// 解锁
}
mDatabase = db;// 赋值给mDatabase
} else {
// 打开失败的情况:解锁、关闭
if (mDatabase != null)
mDatabase.unlock();
if (db != null)
db.close();
}
}
}

大家可以看到,几个关键步骤是,首先判断mDatabase如果不为空已打开并不是只读模式则直接返回,否则如果mDatabase不为空则加锁,然后开始打开或创建数据库,比较版本,根据版本号来调用相应的方法,为数据库设置新版本号,最后释放旧的不为空的mDatabase并解锁,把新打开的数据库实例赋予mDatabase,并返回最新实例。
看完上面的过程之后,大家或许就清楚了许多,如果不是在遇到磁盘空间已满等情况,getReadableDatabase()一般都会返回和getWritableDatabase()一样的数据库实例,所以我们在DBManager构造方法中使用getWritableDatabase()获取整个应用所使用的数据库实例是可行的。当然如果你真的担心这种情况会发生,那么你可以先用getWritableDatabase()获取数据实例,如果遇到异常,再试图用getReadableDatabase()获取实例,当然这个时候你获取的实例只能读不能写了。
最后,让我们看一下如何使用这些数据操作方法来显示数据,下面是MainActivity.Java的布局文件和代码:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent">
<Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="add" android:onClick="add"/>
<Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="update" android:onClick="update"/>
<Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="delete" android:onClick="delete"/>
<Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="query" android:onClick="query"/>
<Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="queryTheCursor" android:onClick="queryTheCursor"/>
<ListView android:id="@+id/listView" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
</LinearLayout>
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
package com.scott.db;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.SimpleCursorAdapter;
public class MainActivity extends Activity {
private DBManager mgr;
private ListView listView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
listView = (ListView) findViewById(R.id.listView);
//初始化DBManager
mgr = new DBManager(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
//应用的最后一个Activity关闭时应释放DB
mgr.closeDB();
}
public void add(View view) {
ArrayList<Person> persons = new ArrayList<Person>();
Person person1 = new Person("Ella", 22, "lively girl");
Person person2 = new Person("Jenny", 22, "beautiful girl");
Person person3 = new Person("Jessica", 23, "sexy girl");
Person person4 = new Person("Kelly", 23, "hot baby");
Person person5 = new Person("Jane", 25, "a pretty woman");
persons.add(person1);
persons.add(person2);
persons.add(person3);
persons.add(person4);
persons.add(person5);
mgr.add(persons);
}
public void update(View view) {
Person person = new Person();
person.name = "Jane";
person.age = 30;
mgr.updateAge(person);
}
public void delete(View view) {
Person person = new Person();
person.age = 30;
mgr.deleteOldPerson(person);
}
public void query(View view) {
List<Person> persons = mgr.query();
ArrayList<Map<String, String>> list = new ArrayList<Map<String, String>>();
for (Person person : persons) {
HashMap<String, String> map = new HashMap<String, String>();
map.put("name", person.name);
map.put("info", person.age + " years old, " + person.info);
list.add(map);
}
SimpleAdapter adapter = new SimpleAdapter(this, list, android.R.layout.simple_list_item_2,
new String[]{"name", "info"}, new int[]{android.R.id.text1, android.R.id.text2});
listView.setAdapter(adapter);
}
public void queryTheCursor(View view) {
Cursor c = mgr.queryTheCursor();
startManagingCursor(c); //托付给activity根据自己的生命周期去管理Cursor的生命周期
CursorWrapper cursorWrapper = new CursorWrapper(c) {
@Override
public String getString(int columnIndex) {
//将简介前加上年龄
if (getColumnName(columnIndex).equals("info")) {
int age = getInt(getColumnIndex("age"));
return age + " years old, " + super.getString(columnIndex);
}
return super.getString(columnIndex);
}
};
//确保查询结果中有"_id"列
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2,
cursorWrapper, new String[]{"name", "info"}, new int[]{android.R.id.text1, android.R.id.text2});
ListView listView = (ListView) findViewById(R.id.listView);
listView.setAdapter(adapter);
}
}

这里需要注意的是SimpleCursorAdapter的应用,当我们使用这个适配器时,我们必须先得到一个Cursor对象,这里面有几个问题:如何管理Cursor的生命周期,如果包装Cursor,Cursor结果集都需要注意什么。
如果手动去管理Cursor的话会非常的麻烦,还有一定的风险,处理不当的话运行期间就会出现异常,幸好Activity为我们提供了startManagingCursor(Cursor cursor)方法,它会根据Activity的生命周期去管理当前的Cursor对象,下面是该方法的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* This method allows the activity to take care of managing the given
* {@link Cursor}'s lifecycle for you based on the activity's lifecycle.
* That is, when the activity is stopped it will automatically call
* {@link Cursor#deactivate} on the given Cursor, and when it is later restarted
* it will call {@link Cursor#requery} for you. When the activity is
* destroyed, all managed Cursors will be closed automatically.
*
* @param c The Cursor to be managed.
*
* @see #managedQuery(android.net.Uri , String[], String, String[], String)
* @see #stopManagingCursor
*/

文中提到,startManagingCursor方法会根据Activity的生命周期去管理当前的Cursor对象的生命周期,就是说当Activity停止时他会自动调用Cursor的deactivate方法,禁用游标,当Activity重新回到屏幕时它会调用Cursor的requery方法再次查询,当Activity摧毁时,被管理的Cursor都会自动关闭释放。
如何包装Cursor:我们会使用到CursorWrapper对象去包装我们的Cursor对象,实现我们需要的数据转换工作,这个CursorWrapper实际上是实现了Cursor接口。我们查询获取到的Cursor其实是Cursor的引用,而系统实际返回给我们的必然是Cursor接口的一个实现类的对象实例,我们用CursorWrapper包装这个实例,然后再使用SimpleCursorAdapter将结果显示到列表上。
Cursor结果集需要注意些什么:一个最需要注意的是,在我们的结果集中必须要包含一个“_id”的列,否则SimpleCursorAdapter就会翻脸不认人,为什么一定要这样呢?因为这源于SQLite的规范,主键以“_id”为标准。解决办法有三:第一,建表时根据规范去做;第二,查询时用别名,例如:SELECT id AS _id FROM person;第三,在CursorWrapper里做文章:

1
2
3
4
5
6
7
8
9
CursorWrapper cursorWrapper = new CursorWrapper(c) {
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
if (columnName.equals("_id")) {
return super.getColumnIndex("id");
}
return super.getColumnIndexOrThrow(columnName);
}
};

如果试图从CursorWrapper里获取“_id”对应的列索引,我们就返回查询结果里“id”对应的列索引即可。
最后我们来看一下结果如何:

【转发】Android 系统是不是MIUI、Flyme、EMUI

【转发】Android 系统是不是MIUI、Flyme、EMUI

工作中遇到“判断android手机的rom是哪一个版本”这样的问题,搜到如下解决方案,具体出自哪里不确定(网上转载太多,也没标注出处……),我是在这里看到的:http://mojijs.com/2015/10/211671/index.html 代码复制下来即可用,我已经验证过,O(∩_∩)O~
\

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
//读取系统配置信息build.prop类
import android.os.Environment;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
public class BuildProperties {
private final Properties properties;
private BuildProperties() throws IOException {
properties = new Properties();
properties.load(new FileInputStream(new File(Environment.getRootDirectory(), "build.prop")));
}
public boolean containsKey(final Object key) {
return properties.containsKey(key);
}
public boolean containsValue(final Object value) {
return properties.containsValue(value);
}
public Set<Entry<Object, Object>> entrySet() {
return properties.entrySet();
}
public String getProperty(final String name) {
return properties.getProperty(name);
}
public String getProperty(final String name, final String defaultValue) {
return properties.getProperty(name, defaultValue);
}
public boolean isEmpty() {
return properties.isEmpty();
}
public Enumeration<Object> keys() {
return properties.keys();
}
public Set<Object> keySet() {
return properties.keySet();
}
public int size() {
return properties.size();
}
public Collection<Object> values() {
return properties.values();
}
public static BuildProperties newInstance() throws IOException {
return new BuildProperties();
}
}
import android.os.Build;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* 判断系统是不是miui,flyme,emui
*/
public class OSUtils {
private static final String KEY_EMUI_VERSION_CODE = "ro.build.version.emui";
private static final String KEY_MIUI_VERSION_CODE = "ro.miui.ui.version.code";
private static final String KEY_MIUI_VERSION_NAME = "ro.miui.ui.version.name";
private static final String KEY_MIUI_INTERNAL_STORAGE = "ro.miui.internal.storage";
private static boolean isPropertiesExist(String... keys) {
try {
BuildProperties prop = BuildProperties.newInstance();
for (String key : keys) {
String str = prop.getProperty(key);
if (str == null)
return false;
}
return true;
} catch (IOException e) {
return false;
}
}
public static boolean isEMUI() {
return isPropertiesExist(KEY_EMUI_VERSION_CODE);
}
public static boolean isMIUI() {
return isPropertiesExist(KEY_MIUI_VERSION_CODE, KEY_MIUI_VERSION_NAME, KEY_MIUI_INTERNAL_STORAGE);
}
public static boolean isFlyme() {
try {
final Method method = Build.class.getMethod("hasSmartBar");
return method != null;
} catch (final Exception e) {
return false;
}
}
}

\

android gridview不使用viewholder直接添加view导致乱序、重复响应的解决方法

android gridview不使用viewholder直接添加view导致乱序、重复响应的解决方法

要实现可拖动的九宫格,网上搜了下,搜到比较靠谱的是这个版本,原理、实现都写的很清楚,但有个问题,用到GridView时,写adapter时一般都会使用ViewHolder的方式,即adapter使用BaseAdapter,adapter中保存数据,而调用getView方法时,若是第一次调用则将可复用的数据保存到一个ViewHolder类中,后续就不用再重新创建View;或者,adapter中每次根据数据创建view。
由于公司所用的客户端框架限制,我在实现可拖动的九宫格时,在getView方法中,拿到的数据就是一个个已经创建好的View对象,但按照上面提到的xiaanming的拖动实现方式,我将这些已有的View对象在getView中返回、加入到GridView后,发现显示没问题,但是拖动之后,View的显示顺序会乱掉,而且会出现第一次点击某个View时对应回调方法没有响应,但第二次点击时则会响应第一次点击、第二次点击两个回调方法。
为解决这问题,在github上发现如下可拖动九宫格的另一种实现,尝试重写adapter类后解决了该问题,发现这个跟stableid有关。我一开始写的adapter大概样子如下;

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
public class GridAdapter extends BaseAdapter{
List<View> viewList;
...//init opertation
@Override
public int getCount() {
// TODO Auto-generated method stub
return viewList.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return viewList.get(position);
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
return viewList.get(position);
}
public void reorder(){
//reorder view list
}

这种写法对于ViewHolder方式或是每次创建新View放的方式实现adapter来说,没有任何问题,但由于我直接使用现有的View对象,而GridView本身又有缓存机制,导致GridView重用View对象时View的顺序和我已经创建的viewList不一致,为解决这一问题,就得说下getItemId(int pos)方法和hasStableIds()方法,可以参考这个帖子,引用一下人家的话:

getItemId是干嘛用的?在调用 invalidateView()时,ListView会刷新显示内容。如果内容的id是有效的,系统会跟据id来确定当前显示哪条内容,也就是firstVisibleChild的位置。id是否有效通过hasStableIds()确定。

我的理解是,在BaseAdapter中,hasStableIds()方法默认返回false,意思是每个item元素的id不稳定,会根据item位置来确定id;如果返回true,表示每个item元素的id是稳定的,即相同的id引用相同的对象。这是因为默认id不稳定,导致我上面写的adapter在进行拖拽操作后,item元素(view对象)位置变了,但其id并没有与我在重排列viewList时一致,导致很古怪的乱序或是重复响应问题。参考可拖动九宫格的另一种实现 后,修改adapter实现如下:

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
100
101
102
103
104
105
private HashMap<Object, Integer> mIdMap = new HashMap<Object, Integer>();
private ArrayList<Object> mItems = new ArrayList<Object>();
public ArrayList<Object> getItemList(){
return mItems;
}
@Override
public final boolean hasStableIds() {
return true;
}
/**
* creates stable id for object
* @param item
*/
protected void addStableId(Object item) {
mIdMap.put(item, nextStableId++);
}
/**
* create stable ids for list
* @param items
*/
protected void addAllStableId(List<?> items) {
for (Object item : items) {
addStableId(item);
}
}
/**
* clear stable id map.
* should called when clear adapter data.
*/
protected void clearStableIdMap() {
mIdMap.clear();
}
/**
* remove stable id for item.
* should called on remove data item from adapter.
* @param item
*/
protected void removeStableID(Object item) {
mIdMap.remove(item);
}
private void init(List<?> items) {
addAllStableId(items);
this.mItems.addAll(items);
}
public void set(List<?> items) {
clear();
init(items);
notifyDataSetChanged();
}
public void clear() {
clearStableIdMap();
mItems.clear();
notifyDataSetChanged();
}
public void add(Object item) {
addStableId(item);
mItems.add(item);
notifyDataSetChanged();
}
public void add(int position, Object item) {
addStableId(item);
mItems.add(position, item);
notifyDataSetChanged();
}
public void add(List<?> items) {
addAllStableId(items);
this.mItems.addAll(items);
notifyDataSetChanged();
}
public void remove(Object item) {
mItems.remove(item);
removeStableID(item);
notifyDataSetChanged();
}
public GridViewAdapter(List<?> elements) {
init(elements);
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public Object getItem(int position) {
return mItems.get(position);
}
/**
* get id for position
*
* @param position
* @return
*/
@Override
public final long getItemId(int position) {
if (position < 0 || position >= mIdMap.size()) {
return -1;
}
Object item = getItem(position);
return mIdMap.get(item);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view =(View) getItem(position);
return view;
}
}

说实话,这问题很诡异,和同事一起找,找到GitHub上的九宫格不同实现,看人家代码、做尝试才解决这问题,GridView这控件太复杂,之前还遇到过GridView第一个item不响应的问题(stackoverflow上一堆相关问题),有时间再细说。上面有关BaseAdapter的分析仅仅是我参考网上帖子做的一些推断,如有错误或是疏漏欢迎大家指正。

android开启辅助功能后GridView控件导致app闪退

android开启辅助功能后GridView控件导致app闪退

app上线前遇到一个很诡异的问题,同一款型号的手机,一台手机没问题,另一台手机则在显示首页后假死、然后直接闪退。经过排查,确定是自定义GridView惹的祸,异常信息如下:

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
java.lang.IllegalArgumentException: parameter must be a descendant of this view
at android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:5353)
at android.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:5282)
at android.view.ViewGroup$ViewLocationHolder.init(ViewGroup.java:7755)
at android.view.ViewGroup$ViewLocationHolder.obtain(ViewGroup.java:7689)
at android.view.ViewGroup$ChildListForAccessibility.init(ViewGroup.java:7624)
at android.view.ViewGroup$ChildListForAccessibility.obtain(ViewGroup.java:7592)
at android.view.ViewGroup.addChildrenForAccessibility(ViewGroup.java:1927)
at android.view.ViewGroup.onInitializeAccessibilityNodeInfoInternal(ViewGroup.java:2982)
at android.widget.AdapterView.onInitializeAccessibilityNodeInfoInternal(AdapterView.java:983)
at android.widget.AbsListView.onInitializeAccessibilityNodeInfoInternal(AbsListView.java:1509)
at android.widget.GridView.onInitializeAccessibilityNodeInfoInternal(GridView.java:2361)
at android.view.View.onInitializeAccessibilityNodeInfo(View.java:6151)
at android.view.View.createAccessibilityNodeInfoInternal(View.java:6110)
at android.view.View.createAccessibilityNodeInfo(View.java:6095)
at android.view.accessibility.AccessibilityRecord.setSource(AccessibilityRecord.java:145)
at android.view.accessibility.AccessibilityRecord.setSource(AccessibilityRecord.java:119)
at android.view.View.onInitializeAccessibilityEventInternal(View.java:6047)
at android.widget.AdapterView.onInitializeAccessibilityEventInternal(AdapterView.java:994)
at android.view.View.onInitializeAccessibilityEvent(View.java:6035)
at android.view.View.sendAccessibilityEventUncheckedInternal(View.java:5896)
at android.view.View.sendAccessibilityEventUnchecked(View.java:5881)
at android.view.View$SendViewStateChangedAccessibilityEvent.run(View.java:22468)
at android.os.Handler.handleCallback(Handler.java:743)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5546)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684)


直接在网上搜这个异常信息,会出来类似的问题,比如这个帖子,这些帖子给出的异常信息相似,最后给出的解决方案也类似,都是说焦点问题,只要调用clearFocus就可以了(这个清楚焦点的操作可以在多个地方进行),但我都试过,发现这些方法都没法解决我遇到的问题。后来通过同事试用app反馈,发现只要打开android系统的辅助功能(android accessibility 比如开关控制、放大手势之类的,不同手机菜单名可能不一样,比如MIUI中是在“无障碍”中的那些功能),就会出现这个问题。然后重新搜,发现是“检测窗口内容(检查您正与其他互动的窗口的内容)”这个功能导致的,在React项目也存在该问题,参考如下链接 传送门  以我的使用场景为例,在我修改GridView后,添加view似乎并不是像其他控件那样,把新添加的View加到整个ViewTree中,而使用辅助功能时,需要从ViewTree的根遍历到所有叶子View,到GridView这一层没法往下走,才导致了上述问题。实际上,看异常也可以看到是在涉及accessibility相关方法时出错,所以只要在GridView中添加如下代码即可解决:

1
2
3
4
5
@Override
public void addChildrenForAccessibility(ArrayList<View> outChildren) {
// Explicitly override this to prevent accessibility events being passed down to children
// Those will be handled by the mHostView which lives in the dialog
}

android webview控件中javascript与java的相互调用

android webview控件中javascript与java的相互调用

首先说明下,本帖主要是为了给出思路,不会给出特别细的教学代码,详细的API使用网上搜就好了。
先给出一个对于webview的帖子:有关webview中js与java互调的帖子
javascript调用java
也就是说通过webview所访问的web站点,希望通过js调用android本身的接口实现某些功能。这种情况下可以用webview控件的addJavascriptInterface接口注册一个js接口,然后web站点中调用这个接口即可,可以参考这个例子:传送门
必须说明的是,在android 4.2(API<17)版本之前的WebView,addJavascriptInterface存在漏洞,所以使用一定要慎重。参考如下帖子:传送门。也因为这个漏洞,貌似addJavascriptInterface这个接口实际用的貌似并不多。
java调用javascript
这个相对简单,有两种方式,对于android 4.4之前,在JS中定义好即将提供给Native的方法javaCallJS(),然后用webview.loadUrl(“javascript:javaCallJS()”)即可。当然,也可以直接拼好一个字符串(如“javascript:function test(){alert(‘test’);} test();”),然后直接传给loadUrl。但需要说明,使用loadUrl方式执行js是在UI线程中进行的,因此如果需要运行较长时间的js,最好不要用loadUrl。
android 4.4之后,webview新提供了一个evaluateJavascript接口用于执行js,此接口异步执行js。内部实现机制见此帖:传送门。使用上与loadUrl类似,直接将“javascript:function test(){alert(‘test’);} test();”这样的字符串传给该接口即可。
那么,对于android 4.4之前的webview,如果真出现了不能用loadUrl调用js的情况,应该如何呢?笔者就遇到这么个问题:为了在web站点的js中获取软键盘高度,需要在键盘弹出时由webview调用一个js将高度传给web页面,可是某款手机(其他几款测试手机没问题,唉……)在loadUrl执行js函数时,居然发现软键盘弹出后立马闪退了,原因就是在弹出键盘、调用js后,看上去js的执行重新刷了页面,导致键盘又隐藏了。为解决这问题,我和同事一起钻研了好长时间,发现还有JSBridge这么个好东西,具体原理与使用参考这个帖子:JSBridge的原理与实现。具体使用此处不再给出。GitHub上可以参考这个项目:JSBridge
 \

Android 4.4 WebView控件提示ERR_CACHE_MISS错误

Android 4.4 WebView控件提示ERR_CACHE_MISS错误

我们在页面(该页面仅仅用于重定向操作)发起某个请求、请求正常,但按下物理返回键返回上个页面时遇到这个问题,搜了很多帖子,有的情况说这是因为web站点的页面设置的缓存策略是无缓存,即http header中有如下字段:Cache-Control: no-store,如下:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Encoding: gzip


此时由于页面没缓存、webview去找缓存却没找到,此种情况下,需要看web站点中“no-store”字段是否有必要,没啥作用的话可以尝试去掉“no-cache”试试看,参见这个帖子。另外还有一种改法,是直接修改webview控件,如下。

1
2
3
if (Build.VERSION.SDK_INT &amp;gt;= 19) {
mWebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}

但修改webview控件的方式其实有很大的风险,因为webview本身有多种缓存策略,如下:

1
2
3
4
5
LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
LOAD_DEFAULT: 根据cache-control决定是否从网络上取数据。
LOAD_CACHE_NORMAL: API level 17中已经废弃, 从API level 11开始作用同LOAD_DEFAULT模式
LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。

默认的策略是根据页面中的cache-control字段来判断是读取缓存还是去网上取最新页面。如果改成LOAD_CACHE_ELSE_NETWORK,而业务上需要每次都从web站点取最新数据,那将发生错误(因为webview将优先从本地缓存中取)。
我遇到这个问题时,一开始以为是页面缓存设置问题,但跟服务端同事调试后发现,并不是由Cache-Control: no-store 引起的,又继续查,才发现这样一个问题:我们所采用的框架中,webview在load一个页面时,会先判断是否为post请求,若是则调用postUrl方法,否则就调用loadUrl方法(loadUrl是用GET方式发请求哦!!)。出问题的页面发起的请求是post请求,而按下android物理返回键时webview会调用loadUrl重新发请求,这就导致本来应该发POST请求去load上个页面、结果却发了GET请求,这才导致报错。stackoverflow上也有提到类似的情况,见此链接
为解决这个问题,我们采用了在web站点上用js控制页面重定向,具体方式跟我们的实现有关,贴出来意义不大,在此略去。顺带说句,webview中注册js函数很费劲,我们调了好几天,最终选定了jsbridge,有兴趣的童鞋自己查吧。