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

Pau*_*Kim 19 sqlite android 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.

请注意,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();
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

jni/Application.mk(BROKEN)

APP_ABI := armeabi-v7a
Run Code Online (Sandbox Code Playgroud)

jni/Application.mk(工作)

APP_ABI := armeabi-v7a arm64-v8a
Run Code Online (Sandbox Code Playgroud)

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!");
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

如果您在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)
Run Code Online (Sandbox Code Playgroud)

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

*访问外部文件目录是{} SD卡和/Android/data/com.example.dbtester所有子文件夹,包括Context.getExternalFilesDir(空)和Context.getExternalCacheDir()文件夹.Lollipop上不再需要读/写权限来访问这些文件夹,但我已经使用这些权限进行了彻底的测试.

mst*_*sjo 9

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

在Android 32位ABI上,数据类型ino_t(用于返回/存储inode编号)是32位,而st_ino字段struct stat(返回文件的inode编号)是unsigned long long(64位).这意味着struct stat可以返回存储在a中时被截断的inode编号ino_t.在正常的linux上,当处于32位模式时,st_ino字段输入struct statino_t32位都是32位,因此两者都被截断.

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

sqlite将原始的inode值存储在一个ino_t(被截断)中,然后将它与stat返回的内容进行比较(参见fileHasMovedsqlite中的函数) - 这就是触发降级为只读模式的原因.

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

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

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