在外部目录上进行Nexus 9 SQLite文件写入操作的解决方法?

时间:2014-11-14 19:08:13

标签: android sqlite android-ndk android-5.0-lollipop

我的团队在Nexus 9上发现了一个错误,我们的应用程序无法使用,因为它无法在外部文件目录上以可写模式访问数据库。它似乎只有在应用程序使用JNI时才会发生,并且只有在代码中没有包含arm64-v8a版本时才会发生。

我们目前的理论是,如果不包含arm64-v8a,Nexus 9会包含一些替代版本的本机库,以便向后兼容只有armeabi或armeabi-v7a库的应用程序。似乎某些备用SQLite库中存在一个阻止上述操作的错误。

有没有人找到解决此问题的方法?在arm64中重新构建我们所有的本机库是我们当前的轨道和最完整的解决方案,但这需要我们的时间(我们的一些库是如果可能的话,我们希望更快地解决我们Nexus 9用户的应用程序。


您可以通过这个简单的示例项目轻松查看此问题(您需要最新的Android NDK)。

  1. 将以下文件添加到项目中。
  2. 如果您没有,请安装最新的Android NDK
  3. 在项目目录中运行ndk-build
  4. 刷新,构建,安装和运行。
  5. 如果更改Android.mk或Application.mk,请在再次运行ndk-build之前删除libs和obj文件夹以清理项目。您还需要在每个ndk-build之后手动刷新项目。
  6. 请注意,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(工作

    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上不再需要读/写权限来访问这些文件夹,但我已经使用这些权限进行了彻底的测试。

2 个答案:

答案 0 :(得分:9)

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

在Android 32位ABI上,数据类型ino_t(用于返回/存储inode编号)是32位,而st_ino中的struct stat字段(返回inode)文件的数字)是unsigned long long(64位)。这意味着struct stat可以返回存储在ino_t中时被截断的inode编号。在普通的Linux上,st_inostruct stat中的ino_t字段在32位模式下都是32位,因此两者都被截断相似。

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

sqlite将原始inode值存储在ino_t(被截断)中,然后将其与stat返回的内容进行比较(请参阅sqlite中的fileHasMoved函数) - 这是触发降级为只读的内容模式在这里。

我一般不熟悉sqlite;唯一的解决方法可能是找到一个不会尝试调用fileHasMoved的代码路径。

我为此问题提交了两个可能的解决方案,并将其报告为错误:

希望修复程序合并,然后向后移植到发布分支并很快包含在(又一个)固件更新中。

答案 1 :(得分:3)

无法打开数据库:

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

可以打开DB :(使用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 libs(32位)。