web-dev-qa-db-ja.com

Android電話が原因で、アプリがJava.lang.UnsatisfiedLinkErrorをスローするのはなぜですか?

市場でアプリを使用している一部のAndroidスマートフォンで_Java.lang.UnsatisfiedLinkError_が発生しています。

問題の説明:

_static
{
    System.loadLibrary("stlport_shared"); // C++ STL        
    System.loadLibrary("lib2"); 
    System.loadLibrary("lib3"); 
}
_

_Java.lang.UnsatisfiedLinkError_を使用してSystem.loadLibrary()行でアプリをクラッシュさせます。 _Java.lang.UnsatisfiedLinkError: Couldn't load stlport_shared from loader dalvik.system.PathClassLoader[dexPath=/data/app/app_id-2.apk,libraryPath=/data/app-lib/app_id-2]: findLibrary returned null_

ソリューションアプローチ

すべてのインストールでカスタム診断の実行を開始し、すべてのlibが_/data/data/app_id/lib_フォルダーに解凍されているかどうかを確認しました。

_PackageManager m = context.getPackageManager();
String s = context.getPackageName();
PackageInfo p;
p = m.getPackageInfo(s, 0);
s = p.applicationInfo.dataDir;

File appDir = new File(s);
long freeSpace = appDir.getFreeSpace();

File[] appDirList = appDir.listFiles();
int numberOfLibFiles = 0;
boolean subFilesLarger0 = true;
for (int i = 0; i < appDirList.length; i++) {

    if(appDirList[i].getName().startsWith("lib")) {
        File[] subFile = appDirList[i].listFiles(FileFilters.FilterDirs);   
        numberOfLibFiles = subFile.length;
        for (int j = 0; j < subFile.length; j++) {
            if(subFile[j].length() <= 0) {
                subFilesLarger0 = false;
                break;
            }
        }
    }
}
_

_numberOfLibFiles == 3_と_subFilesLarger0 == true_があるすべてのテスト電話。すべてのライブラリが適切に解凍され、0バイトより大きいかどうかをテストしたいと思いました。さらに、freeSpaceを調べて、使用可能なディスク容量を確認しています。 freeSpaceは、画面の下部にある[設定]-> [アプリケーション]にあるメモリ量と一致します。このアプローチの背後にある考えは、利用可能なディスクに十分なスペースがない場合、インストーラーがAPKの解凍に問題を起こす可能性があるということでした。

現実世界のシナリオ

診断結果を見ると、世の中にあるいくつかのデバイス[〜#〜](〜#〜]は、_/data/data/app_id/lib_フォルダですが、十分な空き容量があります。エラーメッセージが_/data/app-lib/app_id-2_を探しているのはなぜですか。すべての携帯電話は、ライブラリを_/data/data/app_id/lib_に保存します。また、System.loadLibrary()は、インストールとライブラリのロードで一貫したパスを使用する必要がありますか? OSがlibを探している場所を知るにはどうすればよいですか?

質問

ネイティブライブラリのインストールで問題が発生している人はいますか?どのような回避策が成功していますか?ネイティブライブラリが存在しないときにインターネット経由でダウンロードして手動で保存した経験はありますか?そもそも何が問題を引き起こしているのでしょうか?

[〜#〜]編集[〜#〜]

また、アプリケーションの更新後にこの問題が発生するユーザーもいます。以前のバージョンは彼の電話で正常に動作しましたが、更新後、ネイティブライブラリが欠落しているようです。 libsを手動でコピーすると、同様に問題が発生するようです。彼はAndroid 4.xで、カスタムROMのないルート権限を取得していない電話を使用しています。

EDIT 2-ソリューション

この問題に2年間費やした後。私達は今私達のためにうまく働く解決策を思いつきました。私たちはそれをオープンソース化しています: https://github.com/KeepSafe/ReLinker

39
philipp

私は同じ問題を抱えており、UnsatisfiedLinkErrorsはAndroidのすべてのバージョンで発生します-過去6か月間で、現在90000を超えるアクティブインストールがあるアプリの場合:

Android 4.2     36  57.1%
Android 4.1     11  17.5%
Android 4.3     8   12.7%
Android 2.3.x   6   9.5%
Android 4.4     1   1.6%
Android 4.0.x   1   1.6%

ユーザーからは、通常アプリのアップデート直後に発生すると報告されています。これは、1日あたり約200〜500人の新規ユーザーを獲得するアプリ用です。

私はもっと簡単な回避策を思いついたと思います。私はこの簡単な呼び出しで私のアプリの元のapkがどこにあるかを見つけることができます:

    String apkFileName = context.getApplicationInfo().sourceDir;

これにより、アプリのAPKファイルの正確なファイル名である「/data/app/com.example.pkgname-3.apk」のようなものが返されます。このファイルは通常のZipファイルであり、rootなしで読み取ることができます。したがって、Java.lang.UnsatisfiedLinkErrorをキャッチすると、.apk(Zip)lib/armeabi-v7aフォルダー(または私が使用しているアーキテクチャ)の内部から、任意のディレクトリにネイティブライブラリを抽出してコピーできます。読み取り/書き込み/実行が可能で、System.load(full_path)でロードできます。

編集:それは動作するようです

2014年7月1日の更新以下にリストされているようなコードで私の製品のバージョンを2014年6月23日にリリースして以来、不満はありませんでしたネイティブライブラリのエラーをリンクします。

これが私が使ったコードです:

public static void initNativeLib(Context context) {
    try {
        // Try loading our native lib, see if it works...
        System.loadLibrary("MyNativeLibName");
    } catch (UnsatisfiedLinkError er) {
        ApplicationInfo appInfo = context.getApplicationInfo();
        String libName = "libMyNativeLibName.so";
        String destPath = context.getFilesDir().toString();
        try {
            String soName = destPath + File.separator + libName;
            new File(soName).delete();
            UnzipUtil.extractFile(appInfo.sourceDir, "lib/" + Build.CPU_ABI + "/" + libName, destPath);
            System.load(soName);
        } catch (IOException e) {
            // extractFile to app files dir did not work. Not enough space? Try elsewhere...
            destPath = context.getExternalCacheDir().toString();
            // Note: location on external memory is not secure, everyone can read/write it...
            // However we extract from a "secure" place (our apk) and instantly load it,
            // on each start of the app, this should make it safer.
            String soName = destPath + File.separator + libName;
            new File(soName).delete(); // this copy could be old, or altered by an attack
            try {
                UnzipUtil.extractFile(appInfo.sourceDir, "lib/" + Build.CPU_ABI + "/" + libName, destPath);
                System.load(soName);
            } catch (IOException e2) {
                Log.e(TAG "Exception in InstallInfo.init(): " + e);
                e.printStackTrace();
            }
        }
    }
}

残念ながら、不適切なアプリの更新によってネイティブライブラリの古いバージョンが残されたり、System.loadLibrary( "MyNativeLibName")でロードしたコピーが何らかの形で損傷したりした場合、それをアンロードする方法はありません。標準のアプリのネイティブlibフォルダーに残っている、このような残存する機能しないライブラリについて調べたところ、ネイティブメソッドの1つを呼び出して、そこにないことを確認することで(UnsatisfiedLinkError)、標準のSystem.loadLibrary()を完全に呼び出さずに、次のアプリの起動時に独自の抽出およびロードコードに依存しないように設定を保存できます。

完全を期すために、ここにUnzipUtilクラスを示します。これは、これからコピーして変更したものです CodeJava UnzipUtilityの記事

import Java.io.*;
import Java.util.Zip.ZipEntry;
import Java.util.Zip.ZipInputStream;

public class UnzipUtil {
    /**
     * Size of the buffer to read/write data
     */

    private static final int BUFFER_SIZE = 4096;
    /**
     * Extracts a Zip file specified by the zipFilePath to a directory specified by
     * destDirectory (will be created if does not exists)
     * @param zipFilePath
     * @param destDirectory
     * @throws Java.io.IOException
     */
    public static void unzip(String zipFilePath, String destDirectory) throws IOException {
        File destDir = new File(destDirectory);
        if (!destDir.exists()) {
            destDir.mkdir();
        }
        ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
        ZipEntry entry = zipIn.getNextEntry();
        // iterates over entries in the Zip file
        while (entry != null) {
            String filePath = destDirectory + File.separator + entry.getName();
            if (!entry.isDirectory()) {
                // if the entry is a file, extracts it
                extractFile(zipIn, filePath);
            } else {
                // if the entry is a directory, make the directory
                File dir = new File(filePath);
                dir.mkdir();
            }
            zipIn.closeEntry();
            entry = zipIn.getNextEntry();
        }
        zipIn.close();
    }

    /**
     * Extracts a file from a Zip to specified destination directory.
     * The path of the file inside the Zip is discarded, the file is
     * copied directly to the destDirectory.
     * @param zipFilePath - path and file name of a Zip file
     * @param inZipFilePath - path and file name inside the Zip
     * @param destDirectory - directory to which the file from Zip should be extracted, the path part is discarded.
     * @throws Java.io.IOException
     */
    public static void extractFile(String zipFilePath, String inZipFilePath, String destDirectory) throws IOException  {
        ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
        ZipEntry entry = zipIn.getNextEntry();
        // iterates over entries in the Zip file
        while (entry != null) {
            if (!entry.isDirectory() && inZipFilePath.equals(entry.getName())) {
                String filePath = entry.getName();
                int separatorIndex = filePath.lastIndexOf(File.separator);
                if (separatorIndex > -1)
                    filePath = filePath.substring(separatorIndex + 1, filePath.length());
                filePath = destDirectory + File.separator + filePath;
                extractFile(zipIn, filePath);
                break;
            }
            zipIn.closeEntry();
            entry = zipIn.getNextEntry();
        }
        zipIn.close();
    }

    /**
     * Extracts a Zip entry (file entry)
     * @param zipIn
     * @param filePath
     * @throws Java.io.IOException
     */
    private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
        byte[] bytesIn = new byte[BUFFER_SIZE];
        int read = 0;
        while ((read = zipIn.read(bytesIn)) != -1) {
            bos.write(bytesIn, 0, read);
        }
        bos.close();
    }
}

グレッグ

13
gregko

編集:私のアプリの1つについて昨日別のクラッシュレポートを受け取ったので、私は問題をもう少し掘り下げて、その問題の3番目の非常にありそうな説明を見つけました:

Google PlayのAPKの部分的なアップデートが正しくありません

正直なところ、その機能については知りませんでした。 APKファイル名のサフィックス "-2.apk"が疑わしくなりました。これはこの質問のクラッシュメッセージで言及されており、その接尾辞は私の顧客のクラッシュレポートでも見つけることができました。

「-2.apk」は、おそらくより小さなデルタをAndroidデバイスに提供する部分的な更新を示唆しています。前のバージョンから変更されていない場合、そのデルタには明らかにネイティブライブラリが含まれていません。 。

何らかの理由で、System.loadLibrary関数は、部分的な更新(存在しない場合)からネイティブライブラリを検索しようとします。 AndroidとGoogle Playの両方に問題があります。

以下は、同様の観察に関する興味深いディスカッションを伴う非常に関連性の高いバグレポートです。 https://code.google.com/p/Android/issues/detail?id=35962

Jelly Beanには、ネイティブライブラリのインストールとロードの点で欠陥があるようです(両方のクラッシュレポートはJelly Beanデバイスでした)。

これが本当に当てはまる場合は、NDKライブラリコードを強制的に変更すると、各リリースで未使用のダミー変数を変更するなど、問題が解決されると思います。ただし、それは、コンパイラーがその変数を最適化せずに保存する方法で行う必要があります。

EDIT(12/19/13):残念ながら、各ビルドのネイティブライブラリコードを変更するという考えは機能しません。私は自分のアプリの1つでそれを試してみましたが、とにかく更新した顧客から「不満足なリンクエラー」のクラッシュレポートを受け取りました。

APKのインストールが不完全です

残念ながらそれは私の記憶の外にあり、私はもうリンクを見つけることができません。昨年、その不満のあるリンクの問題に関するブログ記事を読みました。これはAPKインストールルーチンのバグであると著者は述べています。

ネイティブライブラリをターゲットディレクトリにコピーすると、なんらかの理由で失敗します(デバイスのストレージスペースが足りなくなった、ディレクトリの書き込み権限がめちゃくちゃになっている可能性があります...)。アプリ。

この場合の唯一の回避策は、アプリに十分なストレージスペースがあることを確認しながらAPKを再インストールすることです。

しかし、Androidバグチケットもオリジナルのブログ記事ももう見つかりませんでした。かなり探しましたので、この説明は神話や伝説の領域にあるかもしれません。

「armeabi-v7a」ディレクトリは「armeabi」ディレクトリよりも優先されます

このバグチケットの説明 インストールされているネイティブライブラリの「同時実行の問題」のヒント、 Androidバグチケット#9089 を参照してください。

「armeabi-v7a」ディレクトリが1つのネイティブライブラリだけで存在する場合、そのアーキテクチャのディレクトリ全体が「armeabi」ディレクトリよりも優先されます。

「armeabi」にあるライブラリをロードしようとすると、UnsatisfiedLinkExceptionが発生します。ちなみに、このバグには「意図したとおりに機能する」というフラグが付けられています。

可能な回避策

どちらの場合でも: 興味深い答えが見つかりました ここで同様の質問にSOします。つまり、すべてのネイティブライブラリを生のリソースとしてAPKにパッケージ化し、最初のアプリで現在のプロセッサアーキテクチャ用の正しいライブラリを(アプリプライベート)ファイルシステムにコピーします。これらのライブラリをロードするには、完全なファイルパスでSystem.loadを使用します。

ただし、この回避策には欠陥があります。ネイティブライブラリがAPKのリソースとして存在するため、Google Playはそれらを見つけて必須のプロセッサアーキテクチャ用のデバイスフィルターを作成できなくなります。 「ダミー」のネイティブライブラリをすべてのターゲットアーキテクチャのlibフォルダーに配置することで回避できます。

全体的に、この問題はGoogleに適切に伝えられるべきだと思います。 Jelly BeanとGoogle Playの両方に欠陥があるようです。

通常、問題を抱えているお客様にアプリを再インストールするように伝えます。アプリのデータ損失が懸念される場合、これは残念ながら良い解決策ではありません。

19
tiguchi

Androidがライブラリを/ data/app-lib //にパッケージ化し始めたのは、アイスクリームサンドイッチとジェリービーンのどちらかでしたが、覚えていません。

「arm」と「armv7a」の両方をビルドして配布していますか?私の推測では、あなたは一方のアーキテクチャ用にのみビルドし、もう一方のアーキテクチャでテストしていると思います。

0
David