【问题标题】:Workaround for Nexus 9 SQLite file write operations on external dirs?Nexus 9 SQLite 文件在外部目录上写入操作的解决方法?
【发布时间】:2015-01-12 06:24:43
【问题描述】:

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

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

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


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

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

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

src/com/example/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/layout/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 不再需要读/写权限来访问这些文件夹,但我已经在打开和关闭这些权限的情况下对其进行了彻底的测试。

【问题讨论】:

  • 好问题。如果你在一天之内没有得到答复,我会为此付出我自己的赏金。
  • 我们在使用 xbmc/Kodi 时遇到了完全相同的问题。很高兴知道我们并不孤单。

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


【解决方案1】:

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

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

只要 Android 在 32 位内核上运行,这没有任何问题,因为所有实际的 inode 编号都是 32 位的,但是现在在 64 位内核上运行时,内核可以使用不'适合ino_t。这似乎是您在 sdcard 分区上的文件发生的情况。

sqlite 将原始 inode 值存储在 ino_t(被截断)中,然后比较 stat 返回的内容(参见 sqlite 中的 fileHasMoved 函数)——这是触发降级到只读模式的原因。

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

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

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

【讨论】:

  • 谢谢你!希望补丁快点出来。尽管这不是一种解决方法,但这可以澄清问题。
  • android-review.googlesource.com/115351 中的修复已被合并(更好地解释了为什么会遇到如此大的 inode 数,由 Narayan Kamath 调查),但我不知道它是否被精心挑选到 5.0 发布分支(目前还没有)。
  • 不幸的是,此修复似乎不包含在 5.0.1 版本中。我不确定是否有任何其他方法可以提醒他们,而不是尝试对问题报告进行投票/加注星标(唠叨它可能会有所帮助,或者可能只是一种烦恼)。
  • 此问题现已在 Nexus 9 的 5.1.1 中得到修复。 (在android-review.googlesource.com/123482. 源中修复问题时,链接的特定修复后来被恢复)
  • 你,我的朋友,真棒。有这个糟糕的错误 3 天,现在通过根据您的第一个 googlesource 修复修改 sqlite3 文件来修复。给我来一杯虚拟啤酒。
【解决方案2】:

无法打开数据库:

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 libs(32bit).

【讨论】:

  • 我已经验证这适用于我在上面发布的示例应用程序。它仍然发布“打开时重命名文件”错误,但它似乎确实写入了数据库(尽管它看起来必须从 WAL 日志文件中恢复条目)。但是,我们的主应用程序使用了更复杂的数据库系统。到目前为止,在我们所有的数据库调用中添加 WAL 标志还没有解决我们的问题。
  • 啊,它只是似乎在使用示例应用程序。但是将 tester-ext.db 文件拉出到您的计算机上,您会看到一个空白的 DB 文件。所以很遗憾,这不是一个真正的解决方案。
  • 啊,当然,使用 SQL 查看器应用程序,我们的 DB 文件似乎是空白的。但是,应用程序打开或读取了这个数据库文件,记录是存在的……很神秘……
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-22
  • 2016-07-06
  • 2016-03-17
  • 1970-01-01
相关资源
最近更新 更多