Nexus 9 SQLite文件写入外部dirs操作的解决方法?

我的团队在Nexus 9上发现了一个bug,在那里我们的应用程序无法使用,因为它不能以外部文件目录的可写模式访问数据库。 这似乎只发生在应用程序使用JNI,只有当你没有在代码中包含arm64-v8a版本。

我们目前的理论是,如果没有包含arm64-v8a,Nexus 9包含了一些替代版本的本地库,以便与只有armeabi或armeabi-v7a库的应用程序向后兼容。 似乎这些替代SQLite库中的一些错误会阻止上面的操作。

有没有人发现这个问题的任何解决方法? 在arm64中重build我们所有的本地库是我们目前的轨道,也是最完整的解决scheme,但是这需要我们花费时间(我们的一些库是外部的),如果可能的话,我们更喜欢更快的周转时间来修复我们的Nexus 9用户。


你可以很容易地看到这个简单的示例项目(你需要最新的Android NDK )的问题。

  1. 将以下文件添加到项目中。
  2. 如果你没有安装最新的Android NDK 。
  3. 在项目目录中运行ndk-build
  4. 刷新,构build,安装和运行。
  5. 如果您更改了Android.mk或Application.mk,请在再次运行ndk-build之前通过删除libs和obj文件夹来清理项目。 你也需要在每个ndk-build之后手动刷新你的项目。

请注意,Nexus 9上的“破损”版本仍然适用于内部文件,但不适用于外部文件。

SRC / COM /示例/ dbtester / DBTesterActivity.java

 package com.example.dbtester; import java.io.File; import android.app.Activity; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; public class DBTesterActivity extends Activity { protected static final String TABLE_NAME = "table_timestamp"; static { System.loadLibrary("DB_TESTER"); } private File mDbFileExternal; private File mDbFileInternal; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.dbtester); mDbFileExternal = new File(getExternalFilesDir(null), "tester_ext.db"); mDbFileInternal = new File(getFilesDir(), "tester_int.db"); ((Button)findViewById(R.id.button_e_add)).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { addNewTimestamp(true); } }); ((Button)findViewById(R.id.button_e_del)).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { deleteDbFile(true); } }); ((Button)findViewById(R.id.button_i_add)).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { addNewTimestamp(false); } }); ((Button)findViewById(R.id.button_i_del)).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { deleteDbFile(false); } }); ((Button)findViewById(R.id.button_display)).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { setMessageView(getNativeMessage()); } }); } private void addNewTimestamp(boolean external) { long time = System.currentTimeMillis(); File file; if (external) { file = mDbFileExternal; } else { file = mDbFileInternal; } boolean createNewDb = !file.exists(); SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.NO_LOCALIZED_COLLATORS | SQLiteDatabase.OPEN_READWRITE); if (createNewDb) { db.execSQL("CREATE TABLE " + TABLE_NAME + "(TIMESTAMP INT PRIMARY KEY)"); } ContentValues values = new ContentValues(); values.put("TIMESTAMP", time); db.insert(TABLE_NAME, null, values); Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, null); setMessageView("Table now has " + cursor.getCount() + " entries." + "\n\n" + "Path: " + file.getAbsolutePath()); } private void deleteDbFile(boolean external) { // workaround for Android bug that sometimes doesn't delete a file // immediately, preventing recreation File file; if (external) { file = mDbFileExternal; } else { file = mDbFileInternal; } // practically guarantee unique filename by using timestamp File to = new File(file.getAbsolutePath() + "." + System.currentTimeMillis()); file.renameTo(to); to.delete(); setMessageView("Table deleted." + "\n\n" + "Path: " + file.getAbsolutePath()); } private void setMessageView(String msg) { ((TextView)findViewById(R.id.text_messages)).setText(msg); } private native String getNativeMessage(); } 

RES /布局/ dbtester.xml

 <GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:columnCount="1" > <Button android:id="@+id/button_e_add" android:text="Add Timestamp EXT" /> <Button android:id="@+id/button_e_del" android:text="Delete DB File EXT" /> <Button android:id="@+id/button_i_add" android:text="Add Timestamp INT" /> <Button android:id="@+id/button_i_del" android:text="Delete DB File INT" /> <Button android:id="@+id/button_display" android:text="Display Native Message" /> <TextView android:id="@+id/text_messages" android:text="Messages appear here." /> </GridLayout> 

JNI / Android.mk

 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_CFLAGS += -std=c99 LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog LOCAL_MODULE := DB_TESTER LOCAL_SRC_FILES := test.c include $(BUILD_SHARED_LIBRARY) 

jni / Application.mk( BROKEN

 APP_ABI := armeabi-v7a 

jni / Application.mk( WORKING

 APP_ABI := armeabi-v7a arm64-v8a 

JNI / test.c以

 #include <jni.h> JNIEXPORT jstring JNICALL Java_com_example_dbtester_DBTesterActivity_getNativeMessage (JNIEnv *env, jobject thisObj) { return (*env)->NewStringUTF(env, "Hello from native code!"); } 

AndroidManifest.xml中

 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.dbtester" android:versionCode="10" android:versionName="1.0" > <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" /> <application> <activity android:name="com.example.dbtester.DBTesterActivity" android:label="DB Tester" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 

如果在Nexus 9上运行损坏的版本,则会在LogCat中看到SQLiteLog错误消息,如下所示:

  SQLiteLog: (28) file renamed while open: /storage/emulated/0/Android/data/com.example.dbtester/files/tester.db SQLiteDatabase: android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database (code 1032) 

*有趣的是,如果您将数据库文件存储在内部文件目录中,则数据库可以以可写模式访问。 但是,我们有一些大型的数据库,并不希望将它们全部移到内部文件夹。

*访问的外部文件目录是{sdcard} /Android/data/com.example.dbtester和所有子文件夹,包括Context.getExternalFilesDir(null)和Context.getExternalCacheDir()文件夹。 Lollipop不再需要读/写权限来访问这些文件夹,但我已经通过这些权限进行了彻底的testing。

Solutions Collecting From Web of "Nexus 9 SQLite文件写入外部dirs操作的解决方法?"

不幸的是,我没有任何解决方法build议,但我设法debugging问题,并找出至less实际的根本原因。

在Android 32位ABI上,数据typesino_t (用于返回/存储inode数字)是32位,而struct stat (它返回文件的inode数字)中的st_ino字段是unsigned long long (64位) 。 这意味着struct stat可以返回存储在ino_t时截断的inode数字。 在普通的linux上, struct statino_t中的st_ino字段在32位模式下都是32位,所以两者都被截断。

只要Android已经在32位内核上运行,这一直没有问题,因为无论如何,所有实际的inode数字都是32位,但是现在在64位内核上运行时,内核可以使用不适合inode的数字ino_t 。 这似乎是你的SD卡分区上的文件发生了什么事情。

sqlite将原始的inode值存储在一个ino_t (被截断)中,然后比较它返回的是什么(请参阅sqlite中的fileHasMoved函数) – 这是触发降级到只读模式的原因。

一般来说,我并不熟悉sqlite; 唯一的解决方法可能是find一个不会尝试调用fileHasMoved

我提出了两个可能的解决scheme,并将其报告为一个错误:

希望这些修复都是合并的,然后将其移植到发布版本,并很快包含在另一个固件更新中。

数据库无法打开:

 SQLiteDatabase.openOrCreateDatabase(dbFile, null); and SQLiteDatabase.openDatabase( dbFile.getAbsolutePath(), null, SQLiteDatabase.CREATE_IF_NECESSARY); 

数据库可以打开:(使用MODE_ENABLE_WRITE_AHEAD_LOGGING标志)

 Context.openOrCreateDatabase( dbFile.getAbsolutePath(), Context.MODE_ENABLE_WRITE_AHEAD_LOGGING, null); 

只是也许下面的代码可能工作。

 SQLiteDatabase.openDatabase( dbFile.getAbsolutePath(), null, SQLiteDatabase.MODE_ENABLE_WRITE_AHEAD_LOGGING | SQLiteDatabase.CREATE_IF_NECESSARY); 

我们不明白为什么它使用这个标志。 *我们的应用程序有“armeabi-v7a库(32位)。