1、SD卡数据库
数据库默认位置是在/data/data2目录下,但是出于某些原因(例如数据库文件可能会扩展得比较大,并且对读写速度要求不是很高,不介意sd卡可能比手机内置存储的速度慢),我们可能希望定制位置将数据库文件存放到sd卡,定制位置的方式也很多,其中对现有代码影响最小的方法之一是重写SQLiteOpenHelper的构造函数,新构造出来的SQLiteOpenHelper中存有的就是对SD卡数据库文件的引用。该函数的原型是:
1 2 3 4 5 6 7 8 9 10 11 12 | /** * 构造函数1 */ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version); /** * 构造函数2 */ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, DatabaseErrorHandler errorHandler); |
我们可以写一个SdcardDatabaseOpenHelper继承SQLiteOpenHelper,并重写构造函数,传入新的Context对象,以使调用方还是按照以前的模式进行调用,但是读写的数据库却保存于SD卡中:
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 | /** * 构造函数 */ public SdcardDatabaseOpenHelper(final Context context, String dbPath, String dbName) { super(new SDCardDatabaseContext(context, dbPath), dbName, null, DATABASE_VERSION); } /** * <p> * 用于支持对存储在SD卡上的数据库的访问 * </p> * * @author <a href="mailto:kimiazhu@gmail.com">Kimia Zhu</a> * @version $Id: SDCardDatabaseContext.java, v0.1 2012-7-1 下午2:42:25, Kimia Zhu Exp $ */ public class SDCardDatabaseContext extends ContextWrapper { private String mPath; /** * 构造函数 * * @param context 上下文 * @param path 数据库路径,目录名,不包括文件名。例如/sdcard */ public SDCardDatabaseContext(Context context, String path) { super(context); this.mPath = path; } /** * 重载这方法,是用于指定数据库的存储路径的, */ public File getDatabasePath(String name) { String dbfile = mPath + File.separator + name; if (!dbfile.endsWith(".db")) { dbfile += ".db"; } File result = new File(dbfile); if (!result.getParentFile().exists()) { result.getParentFile().mkdirs(); } return result; } //其他方法... } |
注意仅仅这样并不够,另外需要重写:public SQLiteDatabase openOrCreateDatabase()方法。该方法有两个重载形式,都必须一一实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * 重载这个方法,是用来打开SD卡上的数据库的,android 2.3及以下会调用这个方法。 */ @Override public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory) { SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null); return result; } /** * Android 4.0会调用此方法获取数据库。 * * @see android.content.ContextWrapper#openOrCreateDatabase(java.lang.String, int, * android.database.sqlite.SQLiteDatabase.CursorFactory, * android.database.DatabaseErrorHandler) */ @Override public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) { SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null); return result; } |
重载一个不够的原因是(当然可能你也不会只重载其中一个,不过如果漏掉或者是早期开发的应用则是有可能的),Android 2.3和4.0所调用的函数不一样。前者是API LEVEL 1就已经有了的API,而后者是API Level 11才新增的。所以老应用升级如果没有覆盖后面这个方法,会发现数据库仍然被创建到了/data/data路径下了。(所幸应用是不会出现异常的。)
2、联系人头像设置
网络上现在有很多联系人头像设置的解决方案,都是操作数据库。先说下我对这个过程的理解:
- 在用户希望设置为联系人头像的照片页面弹出选择框,让用户选择联系人;
- 拿到返回的联系人URI,你能从中取得联系人ID(URI最后一个字段),先临时保存起来;
- 弹出框让用户选择裁剪图片,(用一张完整的照片可能不是一个好的方案)
- 拿到裁剪完的照片,以及我们刚刚保存起来的URI或者联系人ID,去修改联系人表的PHOTO(data15)字段
过程是没有问题的,相应的代码也很简单,主要逻辑如下:
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 | /** * 选择联系人,会返回联系人URI,最后一段是联系人ID,保存起来 * * @param activity * @param requestCode */ public static void selectContact(Activity activity, int requestCode){ Intent i = new Intent(); i.setAction(Intent.ACTION_PICK); i.setData(Contacts.CONTENT_URI); activity.startActivityForResult(i, requestCode); } /** * 为设置联系人头像进行截图,数据较小,这里固定为100X100,所以直接返回, * 调用方在onActivityResult()中可以通过getParcelableExtra("data")获得截取好的Bitmap * * @param activity * @param inputUri * @param outputUri * @param requestCode */ public static void cropFroContactAvatar(Activity activity, Uri inputUri, int requestCode){ Intent i = new Intent("com.android.camera.action.CROP"); i.setDataAndType(inputUri, "image/*"); i.putExtra("crop", "true"); i.putExtra("aspectX", 1); i.putExtra("aspectY", 1); i.putExtra("outputX", 100); i.putExtra("outputY", 100); i.putExtra("scale", true); i.putExtra("noFaceDetection", false); i.putExtra("return-data", true); activity.startActivityForResult(Intent.createChooser(i, activity.getString(R.string.crop_image)), requestCode); } /** * 设置联系人图片 * * @param c * @param bytes * @param personId * @param sync */ public static void setContactPhoto(ContentResolver c, byte[] bytes, long personId, boolean sync) { Uri u = Uri.parse("content://com.android.contacts/data"); ContentValues values = new ContentValues(); values.put(Photo.PHOTO, bytes); int r = c.update(ContactsContract.Data.CONTENT_URI, values, ContactsContract.Data.RAW_CONTACT_ID + " = " + personId, null); Log.d(TAG, "更新" + r + "位联系人信息成功"); } |
然而这样的代码走下来我们可能发现在某些机器上成功,某些机器上不能成功。甚至是某些机器的某些联系人成功,另外一些联系人不能成功。
这问题本质上还是和系统分化有关(或者哪位仁兄如果有通用的方法,还请劳烦告知 :) )。Android 2.0以后的联系人数据库十分复杂,在某些定制的系统中,可能有接近50个表。为了避免在各个系统之间出现差池,还是建议调用系统提供的接口进行设置。
系统提供的Intent.ACTION_ATTACH_DATA可以设置联系人,当然它还会带来其他“副作用”,例如它弹出的框也允许用户将图片设做壁纸。不过这个调用当真会比前面的方式简单得多,更重要的是安全。还有一个额外的好处,就是你的应用不需要申请读写Contacts的权限,对于比较敏感的用户,应用不过分申请权限是个明智的选择。
1 2 3 4 5 6 7 8 9 10 11 | /** * “设置为”按钮按下。 */ public void setAs(String filePath){ Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); //ImageUtils是自己写的方法,返回mimetype,例如image/jpeg或者image/png String mimetype = ImageUtils.getImageMimeType(filePath); intent.setDataAndType(Uri.parse("file://" + filePath), mimetype); intent.putExtra("mimeType", mimetype); startActivity(Intent.createChooser(intent, getText(R.string.set_as))); } |
3、在Assets和Drawable中放置图片
这个其实没什么好说,但是困扰了我两天,我曾经在assets放了一些用作前景的jpg图片,在drawable中放了一份用作背景的1px X 1px的像素点jpg。发现将他们用在一起的时候,背景在某些机器上和前景不一样。但实际上前景的边缘和背景这1像素的颜色值绝对是一样的。在小米 + MIUI V4上正常,没有色差;在HTC、MOTO等2.3的机器上不正常,背景显得比前景颜色更深一些。
偶然的,发现如果将他们都放到drawable中,那么不管在4.0还是2.3,小米还是HTC上都是正常无色差的。暂时未能有时间做更多的测试,怀疑是Android系统版本导致的问题。
为了避免此类不是问题的问题,建议将资源统一防止到assets或者drawable目录中。
4、9 Patch 图片
9 Patch图片只支持png,并且是png24,png32未测试,但是png8大部分情况是不行的。虽然png8也能带透明度,但是系统对png8的支持在可与不可之间。所以编辑9 Patch图片最好使用Android SDK自带的工具。甚至避免用Photoshop去制作周围那一条黑边。
我感觉Google是否也应该提供jpg格式的9 Patch? :D
5、NDK数据库操作
Android使用SQLite3数据库,顶层对数据库做了定制化非常大的封装,使用Provider来完成数据库操作,当然你也可以使用一般的SQL语句进行增删改查。所有的Java层数据库调用实际都会执行到本地C代码,再由它们通过libsqlite.so执行到DB层面。libsqlite.so位于/system/lib/中。
我们有一个需求希望能在C代码中执行文件系统扫描操作,这是耗时操作,通过NDK实现能够使得效率上有相当大的提升。这个问题在《从Android本地代码扫描SD卡说开去》中做过些探讨。
但是使用过程中频频出现奇怪的问题,困扰了我很久。包括数据访问过程中不停地出现不同的错误:
- The database disk image is malformed database disk image is malformed
- database is locked
- database image I/O Error
排除了锁的错误之后,I/O错误以及数据库镜像损坏是一个比较头疼的问题。特别是数据库损坏,每次都在不同的地方出现这个错误,十分诡异。
尝试过很多方式去解决问题,包括使用统一的SQLiteOpenHelper,减少访问数据库的频率,拆分数据库,更改事务级别,甚至去掉了事务,等等等等。均无果。
最终看到一篇讲NDK编译SQLite的文章,其实过程和我原来使用SQLite C API的方式差不多,但却让我突然想到是不是可能并不是自己的问题,是不是可能是Android NDK本身对SQLite的支持问题。原因是NDK使用SQLite的方式都是支持从官方下载一份sqlite3.h和sqlite3.c放到jni目录下来进行编译生成.so文件。而不是像其他操作那样(例如NDK下打日志用的log.c)使用NDK本身提供的代码。所以这块是否可能产生一些兼容性问题?
如何用上系统本身的libsqlite.so,可能是一个可以尝试的解决问题的方向。
- 先adb pull /system/lib/libsqlite.so得到 libsqlite.so文件
- 把libsqlite.so文件放到$NDK/platforms/android-14/arch-arm/usr/lib目录下。 (我用的是android 4.0的sdk,所以是android-14)
- 把 sqlite3.h放到$NDK/platforms/android-14/arch-arm/usr/include/android目录下。 (我用的是android 4.0的sdk,所以是android-14)
- 在Android.mk 文件中加入语句:LOCAL_LDLIBS := -lsqlite
- 和平常一样使NDK进行编译。
编译执行,发现问题解决了。我是在Android 2.3提取出来的libsqlite.so,目前测试在Android 2.3和Android 4.0上均没有问题。然而这始终是个悬而未决的问题,无法保证这种方式在任何ROM上都不出问题。
还有一种比较稳妥,但是效率不那么高的方式,就是在数据库操作的地方回调Java代码(其实如前所述,Java代码也会再次调用C进行数据库访问,绕了个圈子)。这种方式相对于是在效率和稳妥之间选择了折中。
不过至少到现在为止,我没有发现2.3导出的so在其他地方出现不兼容的现象。