Nexus 9 SQLite文件编写外部DID操作的解决方法是什么?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (85)

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

我们目前的理论是,Nexus 9在不包括arm64-v8a的情况下,包含了一些本地库的替代版本,以便向后兼容只有armeabi或armeabi-v7a库的应用程序。似乎在一些替代SQLite库中存在一个bug,它阻止了上面的操作。

有人找到解决这个问题的办法了吗?重新构建ARM 64中的所有本地库是我们目前的方向,也是最完整的解决方案,但这将花费我们的时间(我们的一些库是外部的),如果可能的话,我们更愿意更快地为Nexus 9用户修复这个应用程序。

在这个简单的示例项目中,可以很容易地看到这个问题(需要最新的AndroidNDK)。

  1. 将下面的文件添加到项目中。
  2. 安装最新AndroidNDK如果你没有的话。
  3. 运行ndk-build在项目目录中。
  4. 刷新、生成、安装和运行。
  5. 如果更改了Android.mk或Application.mk,请在运行之前删除lib和obj文件夹来清除项目ndk-build再来一次。还需要在每个项ndk-build目之后手动刷新项目。

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

src/com/examel/dbtest/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(有问题)

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和所有子文件夹,包括Context.getExternalFilesDir(NULL)和Context.getExternalCacheDir()文件夹。在Lolliop上不再需要读/写权限来访问这些文件夹,但是我已经用这些权限对其进行了彻底的测试。

提问于
用户回答回答于

我设法调试了这个问题,找出了实际的根本原因。

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

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

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

不过,我对sqite一般并不熟悉;唯一的解决办法可能是找到一个不尝试调用fileHasMoved的代码页.

我为这个问题提交了两个可能的解决方案,并将其报告为一个bug:

希望这两个补丁都能合并,并支持发布分支,并在不久的将来包含在一个(或者另一个一个)固件更新中。

用户回答回答于

数据库不能打开:

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(32位)”。

扫码关注云+社区

领取腾讯云代金券