2014-11-14 8 views
19

Mój zespół wykrył błąd na Nexusie 9, w którym nasza aplikacja jest bezużyteczna, ponieważ nie może uzyskać dostępu do baz danych w trybie zapisu w zewnętrznych katalogach plików. Wydaje się, że dzieje się tak tylko wtedy, gdy aplikacja korzysta z JNI i tylko wtedy, gdy w kodzie nie ma wersji arm64-v8a.Obejście dla Nexusa 9 Operacje zapisu plików SQLite w zewnętrznych katalogach?

Nasza obecna teoria mówi, że Nexus 9 zawiera alternatywną wersję bibliotek natywnych, jeśli arm64-v8a nie jest dołączony, aby być kompatybilnym wstecz z aplikacjami, które mają tylko biblioteki armeabi lub armeabi-v7a. Wygląda na to, że w niektórych alternatywnych bibliotekach SQLite występuje błąd uniemożliwiający powyższą operację.

Czy ktoś znalazł jakieś obejścia tego problemu? Przebudowanie wszystkich naszych rodzimych bibliotek w arm64 to nasz obecny tor i najbardziej kompletne rozwiązanie, ale zajmie nam to trochę czasu (niektóre z naszych bibliotek są zewnętrzne), a my wolimy jak najszybciej naprawić aplikację dla naszego Nexusa 9 użytkowników.


Można łatwo zobaczyć tę kwestię z tego prostego projektu próbki (musisz mieć najnowszą Android NDK).

  1. Dodaj pliki poniżej do projektu.
  2. Zainstaluj najnowszą wersję Android NDK, jeśli jej nie masz.
  3. Wykonaj ndk-build w katalogu projektu.
  4. Odśwież, kompiluj, zainstaluj i uruchom.
  5. Jeśli zmienisz plik Android.mk lub Application.mk, oczyść projekt, usuwając foldery libs i obj przed ponownym uruchomieniem ndk-build. Musisz również ręcznie odświeżyć projekt po każdym ndk-build.

Zauważ, że "zepsuta" kompilacja na Nexusie 9 nadal działa z plikami wewnętrznymi, ale nie z plikami zewnętrznymi.

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/Aplikacja.mk (BROKEN)

APP_ABI := armeabi-v7a 

jni/Application.mk (PRACY)

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> 

Po uruchomieniu złamane budować na Nexusie 9, można zobaczyć komunikaty o błędach SQLiteLog w LogCat jak następuje:

 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) 

* Co ciekawe, jeśli przechowywać pliki bazy danych w Wewnętrzny katalog plików, bazy danych są dostępne w trybie zapisu. Mamy jednak duże bazy danych i niepożądane jest przenoszenie ich wszystkich do folderów wewnętrznych.

plik * Zewnętrzna katalogu dostępne są {sdcard} /Android/data/com.example.dbtester i wszystkich podfolderów, w tym Context.getExternalFilesDir (null) i Context.getExternalCacheDir() foldery. Uprawnienia do odczytu/zapisu nie są już wymagane na Lollipop, aby uzyskać dostęp do tych folderów, ale przetestowałem je dokładnie z tymi uprawnieniami.

+1

Świetne pytanie. Jeśli nie otrzymasz odpowiedzi w ciągu dnia, oddam swoją własną nagrodę. – Simon

+3

Mamy dokładnie ten sam problem z xbmc/Kodi. Dobrze wiedzieć, że nie jesteśmy sami. – Koying

Odpowiedz

9

Niestety nie mam żadnego obejścia, które sugerowałyby, ale udało mi się debugować problem i dowiedzieć się rzeczywiste przyczyny przynajmniej.

Na 32-bitowych ABI systemu Android typ danych ino_t (który jest przeznaczony do zwracania/zapisywania numerów i-węzłów) jest 32-bitowy, natomiast pole st_ino w struct stat (które zwraca numery i-węzłów dla plików) to unsigned long long (czyli 64-bitowe). oznacza, że ​​struct stat może zwracać liczby i-węzły, które są obcięte, gdy są przechowywane w ino_t. W normalnym systemie Linux oba pola st_ino w struct stat i ino_t są 32-bitowe. w trybie 32-bitowym, więc oba są przycinane w podobny sposób.

Dopóki system Android działał na jądrze 32-bitowym, nie stanowiło to żadnego problemu, ponieważ wszystkie rzeczywiste numery i-węzłów i tak były 32-bitowe, ale teraz, gdy jest uruchomiony na jądrach 64-bitowych, jądro może wykorzystywać numery i-węzłów, które pasuje do ino_t. Wygląda na to, co dzieje się z twoimi plikami na partycji sdcard.

sqlite przechowuje oryginalna wartość i-węzeł w ino_t (co jest obcięta), a następnie porównuje je co stat powraca (patrz funkcję w SQLite fileHasMoved) - jest to, co wywołuje degradujące tylko do odczytu trybu tutaj.

Nie jestem jednak ogólnie zaznajomiony z sqlite; jedynym obejściem byłoby prawdopodobnie znalezienie ścieżki kodowej, która nie próbuje wywoływać fileHasMoved.

Złożyłem dwa możliwe rozwiązania tej kwestii, i zgłosił go jako błąd:

Mam nadzieję, że zarówno poprawka jest połączone, i przeniesione do gałąź wydania i uwzględniona w (jeszcze innej) aktualizacji oprogramowania układowego.

+0

Potwierdzam, że łatanie sqlite rozwiązuje problem. – Koying

+0

Dziękuję za to! Mając nadzieję, że wkrótce pojawi się łatka. Chociaż nie jest to dokładnie obejście problemu, wyjaśnia to problem. –

+0

Poprawka w https://android-review.googlesource.com/115351 została scalona (z jeszcze lepszym wyjaśnieniem, dlaczego tak duże liczby i-węzłów zostały napotkane, zbadane przez Narayana Kamatha), ale nie mam żadnego wglądu w to, czy dostaje wiśnie do gałęzi wydania 5.0 (jak dotąd nie ma). – mstorsjo

3

DB nie można otworzyć:

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

DB mogą być otwarte (Używanie flagi MODE_ENABLE_WRITE_AHEAD_LOGGING)

Context.openOrCreateDatabase( 
      dbFile.getAbsolutePath(), 
      Context.MODE_ENABLE_WRITE_AHEAD_LOGGING, null); 

tylko może poniższy kod może działać.

SQLiteDatabase.openDatabase(
    dbFile.getAbsolutePath(), 
    null, 
    SQLiteDatabase.MODE_ENABLE_WRITE_AHEAD_LOGGING 
    | SQLiteDatabase.CREATE_IF_NECESSARY); 

Nie zrozumieliśmy, dlaczego to działa, gdy używa się tej flagi. * The nasza aplikacja ma „armeabi-v7a libs (32bit).

+0

Sprawdziłem, że działa to z przykładową aplikacją, którą napisałem powyżej. Nadal publikuje błędy "zmienił nazwę pliku podczas otwarcia", ale wydaje się, że zapisuje dane w bazach danych (chociaż wygląda na to, że musi odzyskać wpisy z pliku dziennika WAL). Jednak nasza główna aplikacja używa znacznie bardziej skomplikowanego systemu baz danych. Jak dotąd dodanie flagi WAL do wszystkich połączeń z bazami danych nie rozwiązało jeszcze naszych problemów. –

+0

Ah, tylko * * działa przy przykładowej aplikacji. Ale wyciągnij plik tester-ext.db na komputer, a zobaczysz pusty plik DB. To niestety nie jest prawdziwe rozwiązanie. –

+0

Ach, z pewnością nasz plik DB wydaje się pusty za pomocą aplikacji przeglądarki SQL. Ale aplikacja otwiera lub czyta ten plik DB, zapisy istnieją ... To jest tajemnicze ... – Tarky

Powiązane problemy