
AndroidアプリケーションでClassLoader.getResourceAsStream()メソッドを呼び出すのに1432msかかり、一部のライブラリがどれほど危険であるかに興味がある場合は、catを使用してください。
問題の説明
Androidアプリケーションのパフォーマンスの問題を調査すると、この方法に気付きました。 問題は最初の呼び出しでのみ現れ、その後の呼び出しはすべて数ミリ秒かかりました。 興味深い機能は、問題が非常に多数のアプリケーションにあり、1億ダウンロード以上の
AmazonのKindleから、数百ダウンロードの小さなものまでに及ぶことです。
別の機能は、さまざまなアプリケーションでこの方法がまったく異なる時間を要したことです。 ここでは、たとえば、最も人気のある
セルフィーアプリの時間です:
B612-心からの自分撮り
ご覧のとおり、ここではメソッドの所要時間は771ミリ秒で、これも小さくありませんが、1432ミリ秒よりはるかに短くなっています。
問題の大きさを強調する非常に人気のあるアプリケーション。
(アプリケーションのプロファイルを作成するために、サービス
https://nimbledroid.comが使用されました )
その他多数。
いくつかのアプリケーションの呼び出しスタックを詳しく見てみましょう。
LINE:無料通話とメッセージ :

ご覧のとおり、getResourceAsStream呼び出しはメインスレッドにはありません。 これは、Line開発者がそれがどれほど遅いかを認識していることを意味します。
Yahooファンタジースポーツ :

呼び出しはアプリケーションコードではなく、
JodaTimeライブラリ内で
発生します
Audibleのオーディオブック
呼び出しは
Logbackライブラリで発生します
。この問題のあるアプリケーションを分析した後、この呼び出しが主にJarファイルとして配布されるさまざまなライブラリとSDKで使用されていることが明らかになりました。 この方法でリソースにアクセスできるため、これは論理的です。さらに、このコードはAndroidと大きなJavaの両方の世界で機能します。 多くの人がJavaの経験でAndroid開発に来て、もちろん、アプリケーションがどれほど遅いか知らずに、使い慣れたライブラリを使い始めます。 なぜこれほど多くのアプリケーションが影響を受けるのかが明らかになったと思います。
実際、彼らは問題について知っています。たとえば、stackoverflowには次のような質問がありました
。AndroidJava-Joda Date is slow2013年にこの投稿が書かれました:
http :
//blog.danlew.net/2013/08/20/joda_time_s_memory_issue_in_android/とそのようなライブラリが登場しました
https://github.com/dlew/joda-time-androidしかし、- JodaTimeの通常バージョンを使用するアプリケーションはまだ多くあります。
- 同じ問題を持つ他の多くのライブラリとSDKがあります。
問題が明確になったので、その原因が何であるかを理解してみましょう。
研究課題
これを行うには、Androidのソースコードを理解します。
ソースの場所、AOSPの作成方法などについては詳しく説明しませんが、それでも自分で理解して自分の道を繰り返したい人のために、ここから始めましょう
。https:// source.android.com/android-6.0.1_r11ブランチを使用します
ファイル
libcore / libart / src / main / java / java / lang / ClassLoader.javaを開いて、getResourceAsStreamコードを見てみましょう。
public InputStream getResourceAsStream(String resName) { try { URL url = getResource(resName); if (url != null) { return url.openStream(); } } catch (IOException ex) {
すべてが非常に単純に見えます。最初にリソースへのパスを見つけ、それがnullでない場合は、java.net.URLにあるopenStream()メソッドを使用して開きます。
getResource()の実装を見てみましょう:
public URL getResource(String resName) { URL resource = parent.getResource(resName); if (resource == null) { resource = findResource(resName); } return resource; }
それでも面白くない、findResource():
protected URL findResource(String resName) { return null; }
OK、つまりfindResource()は実装されていません。 ClassLoaderは抽象クラスであるため、実際のアプリケーションで使用されているクラスを見つける必要があります。 ドキュメント
http://developer.android.com/reference/java/lang/ClassLoader.htmlを開くと、
Androidがクラスの具体的な実装をいくつか提供しますが、PathClassLoaderが通常使用されます。 。
これを確認したいので、以前に同様の方法でgetResourceAsStream()を変更して、AOSPを構築しました。
public InputStream getResourceAsStream(String resName) { try { Logger.getLogger("RESEARCH").info("this: " + this); URL url = getResource(resName); if (url != null) { return url.openStream(); } ...
dalvik.system.PathClassLoaderと予想されるものを入手しましたが
、PathClassLoaderのソース
パスをチェックすると、
findResource()の実装が見つかりません。 実際、
findResource()は親クラスである
BaseDexClassLoaderに実装されています。
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java :
@Override protected URL findResource(String name) { return pathList.findResource(name); }
pathListを見つけましょう(具体的には開発者のコメントを削除しないので、内容を理解しやすくなります)。
public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }
この
DexPathListに移りましょう。
libcore / dalvik / src / main / java / dalvik / system / DexPathList.java :
final class DexPathList {
リソース検索が実際に実行される場所を見つけたようです。
public URL findResource(String name) { for (Element element : dexElements) { URL url = element.findResource(name); if (url != null) { return url; } } return null; }
要素は、
DexPathListの単なる静的な内部クラス
です 。 そして、その中にはもっと面白いコードがあります:
public URL findResource(String name) { maybeInit();
これについてもう少し詳しく見ていきましょう。 知っているように、APKは単なるzipファイルです。 見ての通り:
if (zipFile == null || zipFile.getEntry(name) == null) {
ZipEntryを名前で検索し、見つかった場合はjava.net.URLを返します。 これはかなり遅い操作かもしれませんが、
getEntryの実装を確認すると、これはLinkedHashMapの単なる反復であることがわかります。
/libcore/luni/src/main/java/java/util/zip/ZipFile.java :
... private final LinkedHashMap<String, ZipEntry> entries = new LinkedHashMap<String, ZipEntry>(); ... public ZipEntry getEntry(String entryName) { checkNotClosed(); if (entryName == null) { throw new NullPointerException("entryName == null"); } ZipEntry ze = entries.get(entryName); if (ze == null) { ze = entries.get(entryName + "/"); } return ze; }
これは超高速操作ではありませんが、非常に長い時間はかかりません。
ひとつ忘れてしまったのは、Zipファイルを操作する前に、それらを開く必要があるということです。
DexPathList.Element.findResource()メソッドの実装をもう一度見ると、
maybeInit()が表示され
ます。 。
それをチェックしてみましょう:
public synchronized void maybeInit() { if (initialized) { return; } initialized = true; if (isDirectory || zip == null) { return; } try { zipFile = new ZipFile(zip); } catch (IOException ioe) { System.logE("Unable to open zip file: " + zip, ioe); zipFile = null; } }
ここにある! この行:
zipFile = new ZipFile(zip);
zipファイルを読み取り用に開きます。
public ZipFile(File file) throws ZipException, IOException { this(file, OPEN_READ); }
これは非常に遅い操作です。ここでは、
エントリを LinkedHashMapに初期化し
ます 。 明らかに、zipファイルが大きいほど、開くのに時間がかかります。
初期化されたフラグのため、zipファイルを1回だけ開きます。これにより、後続の呼び出しがすぐに発生する理由がわかります。
Zipファイルの内部構造をさらに理解するには、ソースを参照してください。
https://android.googlesource.com/platform/libcore/+/android-6.0.1_r21/luni/src/main/java/java/util/zip/ZipFile.javaおもしろかったです! それは始まりにすぎないからです。
実際、これまでは呼び出しのみを扱ってきました。
URL url = getResource(resName);
ただし、getResourceAsStreamコードを次のように変更した場合:
public InputStream getResourceAsStream(String resName) { try { long start; long end; start = System.currentTimeMillis(); URL url = getResource(resName); end = System.currentTimeMillis(); Logger.getLogger("RESEARCH").info("getResource: " + (end - start)); if (url != null) { start = System.currentTimeMillis(); InputStream inputStream = url.openStream(); end = System.currentTimeMillis(); Logger.getLogger("RESEARCH").info("url.openStream: " + (end - start)); return inputStream; } ...
AOSPを収集し、いくつかのアプリケーションをテストするため、
url.openStream()はgetResource()よりもはるかに時間がかかることが
わかります。
url.openStream()
この部分では、あまり興味深い点をいくつか省略します。
url.openStream()からの呼び出しのチェーンに沿って移動すると、
/libcore /
luni /
src /
main /
java /
libcore /
net /
url /
JarURLConnectionImpl.javaに到達し
ます 。
@Override public InputStream getInputStream() throws IOException { if (closed) { throw new IllegalStateException("JarURLConnection InputStream has been closed"); } connect(); if (jarInput != null) { return jarInput; } if (jarEntry == null) { throw new IOException("Jar entry not specified"); } return jarInput = new JarURLConnectionInputStream(jarFile .getInputStream(jarEntry), jarFile); }
connect()メソッドを確認しましょう:
@Override public void connect() throws IOException { if (!connected) { findJarFile();
面白いことは何もありません。もっと深くする必要があります:)
private void findJarFile() throws IOException { if (getUseCaches()) { synchronized (jarCache) { jarFile = jarCache.get(jarFileURL); } if (jarFile == null) { JarFile jar = openJarFile(); synchronized (jarCache) { jarFile = jarCache.get(jarFileURL); if (jarFile == null) { jarCache.put(jarFileURL, jar); jarFile = jar; } else { jar.close(); } } } } else { jarFile = openJarFile(); } if (jarFile == null) { throw new IOException(); } }
これは興味深い方法であり、ここで停止します。 一連の呼び出しを通じて
getUseCaches()を使用すると、
public abstract class URLConnection { ... private static boolean defaultUseCaches = true; ...
この値はオーバーライドされないため、キャッシュの使用が表示されます。
private static final HashMap<URL, JarFile> jarCache = new HashMap<URL, JarFile>();
findJarFile()メソッドには、2番目のパフォーマンスの問題があります! zipファイルが再び開きます! 当然、これは、特にサイズが40MBのアプリケーションでは、すぐには機能しません:)
もう1つの興味深い点があります
。openJarFile()メソッドを確認しましょう。
private JarFile openJarFile() throws IOException { if (jarFileURL.getProtocol().equals("file")) { String decodedFile = UriCodec.decode(jarFileURL.getFile()); return new JarFile(new File(decodedFile), true, ZipFile.OPEN_READ); } else { ...
ご覧の
とおり 、
ZipFileではなく
JarFileを作成してい
ます 。 JarFileはZipFileの子孫です。追加することを確認しましょう。
public JarFile(File file, boolean verify, int mode) throws IOException { super(file, mode);
ええ、それが違いです! 知っているように、APKファイルは署名されている必要があり、
JarFileクラスはこれを検証します。
検証の仕組みを理解したい場合は、これ以上先に進みません
。https :
//android.googlesource.com/platform/libcore/+/android-6.0.1_r21/luni/src/main/java/java/utilをご覧ください/ jar / 。
しかし、言わなければならないことは、それが非常に非常に遅いプロセスであることです。おわりに
ClassLoader.getResourceAsStream()が初めて呼び出されると、APKファイルはリソースを見つけるためのzipファイルとして開きます。 その後、2回目に開きますが、InputStreamを開くための検証が行われます。 また、さまざまなアプリケーションで通話速度に大きな違いがある理由も説明しますが、それはすべてAPKファイルのサイズと内部にあるファイルの数に依存します。
もう一つ
nimbledroidの右上隅の[
フルスタックトレース]セクションで
[Androidフレームワークの展開 ]をクリックすると、実行したすべてのメソッドが表示されます。

Q&A
Q:DalvikとARTランタイム、およびgetResourceAsStream()呼び出しの操作に違いはありますかA:実はいいえ、
android-6.0.1_r11のいくつかのAOSPブランチをARTで、
android-4.4.4_r2の Dalvikを
チェックしました。 両方に問題があります!
それらの違いはわずかに異なりますが、これについては多くのことが書かれています:)
Q:ClassLoader.findClass()を呼び出すときに、このような問題がないのはなぜですかA:既にわかっている
DexPathListクラスに移動すると、
次のように表示されます。
public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
一連の呼び出しに続いて、メソッドに到達します。
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie) throws ClassNotFoundException, NoClassDefFoundError;
さらに、これがどのように機能するかはランタイム(ARTまたはDalvik)に依存しますが、ご覧の
とおり 、
ZipFileでは機能しません。
Q:Resources.get ...(resId)の呼び出しにこの問題がないのはなぜですかA:ClassLoader.findClass()と同じ理由で。
これらの呼び出しはすべて、
/ frameworks / base / core / java / android / content / res / AssetManager.javaにつながります。 private native final int loadResourceValue(int ident, short density, TypedValue outValue, boolean resolve);
ご清聴ありがとうございました!
ハッピーコーディング!