首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >在外部目录上执行Nexus9 SQLite文件写入操作的解决方法?

在外部目录上执行Nexus9 SQLite文件写入操作的解决方法?
EN

Stack Overflow用户
提问于 2014-11-15 03:08:13
回答 2查看 3.4K关注 0票数 19

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

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

有没有人找到解决这个问题的办法?在arm64中重新构建我们所有的本地库是我们目前的轨道,也是最完整的解决方案,但这需要我们花费时间(我们的一些库是外部的),如果可能的话,我们希望更快的周转来修复我们的Nexus9用户的应用程序。

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

  1. 如果没有,请将下面的文件添加到最新的Android NDK中。
  2. 在项目directory.
  3. Refresh,build,install,project.
  4. Install run中运行ndk-build
  5. 如果更改了Android.mk或Application.mk,请在再次运行ndk-build之前,通过删除libs和obj文件夹来清除项目。您还需要在每次执行ndk-build.

后手动刷新项目

请注意,Nexus 9上的“损坏”构建仍然可以处理内部文件,但不能处理外部文件。

src/com/example/dbtester/DBTesterActivity.java

代码语言:javascript
复制
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

代码语言:javascript
复制
<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

代码语言:javascript
复制
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 ()

代码语言:javascript
复制
APP_ABI := armeabi-v7a

jni (WORKING)

代码语言:javascript
复制
APP_ABI := armeabi-v7a arm64-v8a

jni/test.c

代码语言:javascript
复制
#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

代码语言:javascript
复制
<?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>

如果您在Nexus9上运行损坏的构建,您将在LogCat中看到如下所示的SQLiteLog错误消息:

代码语言:javascript
复制
     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(空)和Context.getExternalCacheDir()文件夹。在棒棒糖上不再需要读/写权限来访问这些文件夹,但我已经在打开和关闭这些权限的情况下对其进行了彻底的测试。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2014-11-19 17:51:33

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

在Android32位ABI上,数据类型ino_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位内核上运行时,内核可以使用不适合ino_t的inode编号。这似乎就是你的文件在sdcard分区上发生的情况。

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

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

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

希望任何一个补丁都能被合并,并返回到发布分支,并很快包含在(又一个)固件更新中。

票数 9
EN

Stack Overflow用户

发布于 2014-11-18 16:24:22

无法打开数据库:

代码语言:javascript
复制
SQLiteDatabase.openOrCreateDatabase(dbFile, null);
and
SQLiteDatabase.openDatabase(
    dbFile.getAbsolutePath(), 
    null, 
    SQLiteDatabase.CREATE_IF_NECESSARY);

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

代码语言:javascript
复制
Context.openOrCreateDatabase( 
            dbFile.getAbsolutePath(),
            Context.MODE_ENABLE_WRITE_AHEAD_LOGGING, null);

也许下面的代码可能会起作用。

代码语言:javascript
复制
SQLiteDatabase.openDatabase(
    dbFile.getAbsolutePath(), 
    null, 
    SQLiteDatabase.MODE_ENABLE_WRITE_AHEAD_LOGGING
    | SQLiteDatabase.CREATE_IF_NECESSARY);

我们还不明白当你使用这个标志时它为什么会起作用。*我们的应用有"armeabi-v7a库(32位)“。

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/26937152

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档