Android Single Sqlite Connection

最近在做数据同步的功能,其中在用户第一次同步时会下载数据,如果数据量比较大的话会比较耗时,十几秒的时间很是正常,但是下载数据的时候让用户在那干等不让他进行任何操作用户体验是很差的,那这个时候就只有新开一个线程,让下载数据的一系列动作在后台进行。

所谓下载数据就是把数据从网络取回来的时候把输入insert到本地sqlite数据库中,这时候问题就来了,如果在下载数据的同时用户也在进行数据库的操作(如记录等),这个时候就会有冲突并报出异常,解决的办法是始终让整个Application保持一个database连接,这样的话即使多线程同时访问sqlite,database对象使用java锁会保持访问的序列化。那么如果保持Application是一个database连接呢?

我们一般都是用SqliteOpenHelper来管理数据库,而一个Helper实例会产生一个database连接,所以我们只需要让整个Application产生一个SqliteOpenHelper的实例就ok了,没错,就是单例模式,废话不多说,看代码:

public class DBHelper extends SQLiteOpenHelper {
    private static final String DB_NAME = "food.db";
    private static final int DB_VERSION = 4;
    private static DBHelper helper;

    public static synchronized DBHelper getInstance(Context context) {
        if (helper == null) {
            helper = new DBHelper(context);
        }
        return helper;
    }

    private DBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }
}

接下来就是在你的Application类或者Activity类调用了。值得一说的是下面这个代码是不正确的:

public class MyApplication extends Application {
    private Context context;
    private DBHelper dbHelper;

    public void onCreate() {
        super.onCreate();
        context = getApplicationContext();
        dbHelper = DBHelper.getInstance(context);
    }

    public void onTerminate() {
        super.onTerminate();
        dbHelper.close();
    }
}

因为onTerminate()方法并不会一直执行,在由于异常退出的时候这个方法就不会执行。所以我的解决方案是在主ActivityGroup的onDestroy方法里也会执行一次数据库的关闭,确保万无一失。

参考资料: http://stackoverflow.com/questions/2493331/what-are-the-best-practices-for-sqlite-on-android


Android Sqlite Database Upgrade

本周着手开发数据同步的功能,但首先要解决的就是sqlite数据库升级的问题,关于数据库升级有蛮多方面涉及到,也许你是新增加了功能,所以新建了表,也许你为某些表增加了些字段,也许你是重构了数据模型与数据结构,不管如何升级,必须要满足用户正常升级的情况下原来的数据不会丢失。关于正确的数据库升级做法网上资料比较少,这次就来介绍下看到的国外一位大牛总结的数据库升级的正确做法。

Version 1 of your database

大多数我们都是用android提供的SQLiteOpenHelper来创建和管理数据库,如下代码:

public class DbHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "mysample.db";
    private static final int DATABASE_VERSION = 1;

    private static final String DATABASE_CREATE_SAMPLE_TABLE = "CREATE TABLE tblSample (_id integer primary key autoincrement, name varchar(32);";

    public DbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database) {
        database.execSQL(DATABASE_CREATE_SAMPLE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // Do nothing for now
    }
}

上述代码每次执行的时候都会检查当前版本的数据库是否存在,如果不存在则会执行onCreate方法来创建新的数据库,然后会把数据库的名字和版本号存储起来;如果已经存在,则会比较当前版本和DATABASE_VERSION的大小,然后就会去执行onUpgrade方法。

Version 2 of your database

问题是,最好的处理升级的方法是什么,这里认为最好的方法是循环处理每一个版本的数据库变化,看示例:

假设下一版本想为tblSample表新增一个“address”的字段,新的创建语句应该像这样:

CREATE TABLE tblSample
(
    _id integer primary key autoincrement,
    name varchar(32),
    address varchar(128)
);

那么看下新的代码会是什么样的:

public class DbHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "mysample.db";
    private static final int DATABASE_VERSION = 2;

    private static final String DATABASE_CREATE_SAMPLE_TABLE = "CREATE TABLE tblSample (_id integer primary key autoincrement, name varchar(32), address varchar(128);";

    public DbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database) {
        database.execSQL(DATABASE_CREATE_SAMPLE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        for (int i = oldVersion; i < newVersion; i++) {
            switch (i) {
                case 1:
            	    db.execSQL("ALTER TABLE tblSample ADD address varchar(128)");
            	    break;
            }
        }
    }
}

代码逻辑很简单,就是一个for循环加上switch…case…语句,然后上述代码却能处理所有的数据库升级,不管你是从版本1升级到版本9也好,还是从版本4升级到版本5也好,都可以从容的解决,确切的说,它将能解决从之前的所有版本升级到当前最新的版本。

须要说明的是,如果有些莫名其妙的用户从高版本升级到低版本(确切的说是降级),例如从版本3不小心降级到版本1了,这种情况下如果只是有了上述代码则就会抛出异常,造成系统崩溃。android中数据库降级则会执行onDowngrade方法,为防止有这种情况发生,同样须要重新这个方法防止程序的异常。


Android为自定义View添加属性

Android 自定义View 己经不是什么新鲜话题,Android Api提供了一大堆基础组件给我们,需要什么特定功能还需要我们继承它们然后定制更加丰富的功能。那么如何给自定义的View添加一些自定义xml属性呢,如one:textTitle=”“,不仅如此,我们知道xml中有一个android:onClick=”onClickMethod”,这样在Activity中就不需要给该View设置监听器了,那么有没有类似的自定义listener的属性呢?答案是肯定的。

先来看下我们最后想要定义的格式:

<com.boohee.view.Navbar
    android:id="@+id/navbar"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    one:textTitle="@string/tab_more_text"
    one:onAction="onAction" />

接着便会有如下的效果:

其中textTitle是定义navbar的标题,onAction是navbar上“保存”按钮的事件。好了,下面就来看下代码实现:

定义attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="Navbar">
        <attr name="textTitle" format="string|reference" />
        <attr name="onAction" format="string" />
    </declare-styleable>

</resources>

上面attrs.xml文件中定义了两个属性,关于自定义属性格式,见这篇blogAndroid中自定义属性格式详解

自定义View的初始化下添加代码

public class Navbar extends FrameLayout {
	private Context ctx;
	private Button left_btn, right_btn;
	private TextView title;

	public Navbar(Context context) {
		super(context);
		setUp();
	}

	public Navbar(Context context, AttributeSet attrs) {
		super(context, attrs);
		setUp();
		TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Navbar);
		final int N = a.getIndexCount();
		for (int i = 0; i < N; ++i) {
			int attr = a.getIndex(i);
			switch (attr) {
			case R.styleable.Navbar_textTitle:
				title.setText(a.getString(attr));
				break;
			case R.styleable.Navbar_onAction:
				...
				break;
			}
		}
		a.recycle();
	}

	void setUp() {
		ctx = getContext();
		addView(LayoutInflater.from(this.getContext()).inflate(R.layout.navbar, null));
		left_btn = (Button) findViewById(R.id.left_btn);
		title = (TextView) findViewById(R.id.title);
		right_btn = (Button) findViewById(R.id.right_btn);
	}
}

在XML布局文件中使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:one="http://schemas.android.com/apk/res/com.boohee.one"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/main_bg"
    android:orientation="vertical" >

    <com.boohee.myview.Navbar
        android:id="@+id/navbar"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        one:textTitle="@string/tab_more_text" />

</LinearLayout>

需要注意的是根布局要加上命名空间xmlns:one=”http://schemas.android.com/apk/res/com.boohee.one”

添加一个回调属性

上面说明了如何自定义一些基础属性,那么如何像android:onClick属性一样自定义一个方法回调属性呢,这个一开始实在不晓得如何下手,还好android是开源的,通过看源码后终于有了方法,废话不多说,看代码:

case R.styleable.Navbar_onAction:
	if (context.isRestricted()) {
		throw new IllegalStateException();
	}

	right_btn.setVisibility(View.VISIBLE);
	final String handlerName = a.getString(attr);
	if (handlerName != null) {
		right_btn.setOnClickListener(new OnClickListener() {
			private Method mHandler;

			public void onClick(View v) {
				if (mHandler == null) {
					try {
						mHandler = getContext().getClass().getMethod(handlerName,
								View.class);
					} catch (NoSuchMethodException e) {
						throw new IllegalStateException("NoSuchMethodException");
					}
				}

				try {
					mHandler.invoke(getContext(), right_btn);
				} catch (IllegalAccessException e) {
					throw new IllegalStateException();
				} catch (InvocationTargetException e) {
					throw new IllegalStateException();
				}
			}
		});
	}
	break;

代码倒是不难理解,只是上述代码用到了java的反射机制,这篇blog则讲述了java的反射机制


Android性能优化之使用SparseArray代替HashMap

最近在重构one的项目,其中用HashMap来缓存ActivityGroup加载过的View,Eclipse给出了一个警告,之前考虑项目进度没怎么在意,这次仔细看了下提示,如下:

Use new SparseArray<View> (...) instead for better performance

意思就是说用SparseArray来替代,以获取更好的性能。对SparseArray根本不熟悉,甚至都没听过,第一感觉应该是Android提供的类,于是F3进入SparseArray的源码,果不其然,确实是Android提供的一个工具类,部分源码如下:

* Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.util;

import com.android.internal.util.ArrayUtils;

/**
 * SparseArrays map integers to Objects.  Unlike a normal array of Objects,
 * there can be gaps in the indices.  It is intended to be more efficient
 * than using a HashMap to map Integers to Objects.
 */
public class SparseArray<E> implements Cloneable {
    private static final Object DELETED = new Object();
    private boolean mGarbage = false;

    private int[] mKeys;
    private Object[] mValues;
    private int mSize;

    /**
     * Creates a new SparseArray containing no mappings.
     */
    public SparseArray() {
        this(10);
    }

    /**
     * Creates a new SparseArray containing no mappings that will not
     * require any additional memory allocation to store the specified
     * number of mappings.
     */
    public SparseArray(int initialCapacity) {
        initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);

        mKeys = new int[initialCapacity];
        mValues = new Object[initialCapacity];
        mSize = 0;
    }

    @Override
    @SuppressWarnings("unchecked")
    public SparseArray<E> clone() {
        SparseArray<E> clone = null;
        try {
            clone = (SparseArray<E>) super.clone();
            clone.mKeys = mKeys.clone();
            clone.mValues = mValues.clone();
        } catch (CloneNotSupportedException cnse) {
            /* ignore */
        }
        return clone;
    }

    /**
     * Gets the Object mapped from the specified key, or <code>null</code>
     * if no such mapping has been made.
     */
    public E get(int key) {
        return get(key, null);
    }

    /**
     * Gets the Object mapped from the specified key, or the specified Object
     * if no such mapping has been made.
     */
    @SuppressWarnings("unchecked")
    public E get(int key, E valueIfKeyNotFound) {
        int i = binarySearch(mKeys, 0, mSize, key);

        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }

    /**
     * Removes the mapping from the specified key, if there was any.
     */
    public void delete(int key) {
        int i = binarySearch(mKeys, 0, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

    /**
     * Alias for {@link #delete(int)}.
     */
    public void remove(int key) {
        delete(key);
    }

    /**
     * Removes the mapping at the specified index.
     */
    public void removeAt(int index) {
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }
    ...
mSize++;单纯从字面上来理解,SparseArray指的是稀疏数组(Sparse array),所谓稀疏数组就是数组中大部分的内容值都未被使用(或都为零),在数组中仅有少部分的空间使用。因此造成内存空间的浪费,为了节省内存空间,并且不影响数组中原有的内容值,我们可以采用一种压缩的方式来表示稀疏数组的内容。

继续阅读SparseArray的源码,从构造方法我们可以看出,它和一般的List一样,可以预先设置容器大小,默认的大小是10:

public SparseArray() {
    this(10);
}

public SparseArray(int initialCapacity) {
    initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);

    mKeys = new int[initialCapacity];
    mValues = new Object[initialCapacity];
    mSize = 0;
}

再来看看它对数据的“增删改查”。

public void put(int key, E value) {}
public void append(int key, E value){}

修改数据起初以为只有setValueAt(int index, E value)可以修改数据,但后来发现put(int key, E value)也可以修改数据,我们查看put(int key, E value)的源码可知,在put数据之前,会先查找要put的数据是否已经存在,如果存在就是修改,不存在就添加。

public void put(int key, E value) {
    int i = binarySearch(mKeys, 0, mSize, key);

    if (i >= 0) {
        mValues[i] = value;
    } else {
        i = ~i;

        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        if (mGarbage && mSize >= mKeys.length) {
            gc();

            // Search again because indices may have changed.
            i = ~binarySearch(mKeys, 0, mSize, key);
        }

        if (mSize >= mKeys.length) {
            int n = ArrayUtils.idealIntArraySize(mSize + 1);

            int[] nkeys = new int[n];
            Object[] nvalues = new Object[n];

            // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
            System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
            System.arraycopy(mValues, 0, nvalues, 0, mValues.length);

            mKeys = nkeys;
            mValues = nvalues;
        }

        if (mSize - i != 0) {
            // Log.e("SparseArray", "move " + (mSize - i));
            System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
            System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
        }

        mKeys[i] = key;
        mValues[i] = value;
    
    }
}

所以,修改数据实际也有两种方法:

public void put(int key, E value)
public void setValueAt(int index, E value)

最后再来看看如何查找数据。有两个方法可以查询取值:

public E get(int key)
public E get(int key, E valueIfKeyNotFound)

其中get(int key)也只是调用了 get(int key,E valueIfKeyNotFound),最后一个从传参的变量名就能看出,传入的是找不到的时候返回的值.get(int key)当找不到的时候,默认返回null。

查看第几个位置的键:

public int keyAt(int index)

有一点需要注意的是,查看键所在位置,由于是采用二分法查找键的位置,所以找不到时返回小于0的数值,而不是返回-1。返回的负值是表示它在找不到时所在的位置。

查看第几个位置的值:

public E valueAt(int index)

查看值所在位置,没有的话返回-1:

public int indexOfValue(E value)

最后,发现其核心就是折半查找函数(binarySearch),算法设计的很不错。

private static int binarySearch(int[] a, int start, int len, int key) {
    int high = start + len, low = start - 1, guess;

    while (high - low &gt; 1) {
        guess = (high + low) / 2;

        if (a[guess] &lt; key)
            low = guess;
        else
            high = guess;
    }

    if (high == start + len)
        return ~(start + len);
    else if (a[high] == key)
        return high;
    else
        return ~high;
}

相应的也有SparseBooleanArray,用来取代HashMap<Integer, Boolean>SparseIntArray用来取代HashMap<Integer, Integer>,大家有兴趣的可以研究。

总结

SparseArrayandroid里为<Interger,Object>这样的Hashmap而专门写的类,目的是提高效率,其核心是折半查找函数(binarySearch)。在Android中,当我们需要定义
    HashMap<Integer, E> hashMap = new HashMap<Integer, E>();
时,我们可以使用如下的方式来取得更好的性能.
    SparseArray<E> sparseArray = new SparseArray<E>();

最后,看看重构后的ActivityGroupManager:

public class ActivityGroupManager {
    static final String TAG = ActivityGroupManager.class.getName();

    private SparseArray<View> array;
    private ViewGroup container;

    public ActivityGroupManager() {
    	array = new SparseArray<View>();
    }

    public void setContainer(ViewGroup container) {
    	this.container = container;
    }

    public void showContainer(int num, View view) {
    	if (array.get(num) == null) {
    		array.put(num, view);
    		container.addView(view);
    	}
    	for (int i = 0; i < array.size(); i++) {
    		View v = array.valueAt(i);
    		v.setVisibility(View.GONE);
    	}
    	view.setVisibility(View.VISIBLE);
    }
}

Android中自定义属性格式详解

在Android项目的实际开发中,免不了要自定义一些控件或者view,更高深一点的自定义view也应该可以直接在xml自定义属性,今天就来分享下自定义属性的格式。

1. reference:参考某一资源ID

属性定义:

<declare-styleable name="名称">
    <attr name="background" format="reference" />
</declare-styleable>

属性使用:

<ImageView
    android:layout_width="42dip"
    android:layout_height="42dip"
    android:background="@drawable/图片ID" />

2. color:颜色值

属性定义:

<declare-styleable name="名称">
    <attr name="textColor" format="color" />
</declare-styleable>

属性使用:

<TextView
    android:layout_width="42dip"
    android:layout_height="42dip"
    android:textColor="#00FF00" />

3. boolean:布尔值

属性定义:

<declare-styleable name="名称">
    <attr name="focusable" format="boolean" />
</declare-styleable>

属性使用:

<Button
    android:layout_width="42dip"
    android:layout_height="42dip"
    android:focusable="true" />

4. dimension:尺寸值

属性定义:

<declare-styleable name="名称">
    <attr name="layout_width" format="dimension" />
</declare-styleable>

属性使用:

<Button
    android:layout_width="42dip"
    android:layout_height="42dip" />

5. float:浮点值

属性定义:

<declare-styleable name="AlphaAnimation">
    <attr name="fromAlpha" format="float" />
    <attr name="toAlpha" format="float" />
</declare-styleable>

属性使用:

<alpha
    android:fromAlpha="1.0"
    android:toAlpha="0.7" />

6. integer:整型值

属性定义:

<declare-styleable name="AnimatedRotateDrawable">
    <attr name="visible" />
    <attr name="frameDuration" format="integer" />
    <attr name="framesCount" format="integer" />
    <attr name="pivotX" />
    <attr name="pivotY" />
    <attr name="drawable" />
</declare-styleable>

属性使用:

<animated-rotate
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/图片ID"
    android:frameDuration="100"
    android:framesCount="12"
    android:pivotX="50%"
    android:pivotY="50%" />

7. string:字符串

属性定义:

<declare-styleable name="MapView">
    <attr name="apiKey" format="string" />
</declare-styleable>

属性使用:

<com.google.android.maps.MapView
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:apiKey="0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g" />

8. fraction:百分数

属性定义:

<declare-styleable name="RotateDrawable">
    <attr name="visible" />
    <attr name="fromDegrees" format="float" />
    <attr name="toDegrees" format="float" />
    <attr name="pivotX" format="fraction" />
    <attr name="pivotY" format="fraction" />
    <attr name="drawable" />
</declare-styleable>

属性使用:

<rotate
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="5000"
    android:fromDegrees="0"
    android:interpolator="@anim/动画ID"
    android:pivotX="200%"
    android:pivotY="300%"
    android:repeatCount="infinite"
    android:repeatMode="restart"
    android:toDegrees="360" />

9. enum:枚举值

属性定义:

<declare-styleable name="名称">
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
</declare-styleable>

属性使用:

<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" />

10. flag:位或运算

属性定义:

<declare-styleable name="名称">
    <attr name="windowSoftInputMode">
        <flag name="stateUnspecified" value="0" />
        <flag name="stateUnchanged" value="1" />
        <flag name="stateHidden" value="2" />
        <flag name="stateAlwaysHidden" value="3" />
        <flag name="stateVisible" value="4" />
        <flag name="stateAlwaysVisible" value="5" />
        <flag name="adjustUnspecified" value="0x00" />
        <flag name="adjustResize" value="0x10" />
        <flag name="adjustPan" value="0x20" />
        <flag name="adjustNothing" value="0x30" />
    </attr>
</declare-styleable>

属性使用:

<activity
    android:name=".StyleAndThemeActivity"
    android:label="@string/app_name"
    android:windowSoftInputMode="stateUnspecified | stateUnchanged | stateHidden" >

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

注意

属性定义时可以指定多种类型值,如:

属性定义:

<declare-styleable name="名称">
    <attr name="background" format="reference|color" />
</declare-styleable>

属性使用:

<ImageView
    android:layout_width="42dip"
    android:layout_height="42dip"
    android:background="@drawable/图片ID|#00FF00" />