Android ARTランタイムのDalvik仮想マシンをシームレスに置き換えるプロセスの分析
Android 4.4では、これまで使われてきたDalvik仮想マシンに代わるARTランタイムをリリースし、批判されてきたパフォーマンスの問題を解決することを期待しています。ARTの実装を分析するつもりはありませんが、ARTがオリジナルのDalvik VMをどのようにシームレスに置き換えるのかに興味があるだけです。なにしろ、オリジナルのシステムでは、多くのコードがDalvik VMの内部で動いていたのですから。最初はこの置き換えがかなり複雑だと思っていたのですが、関連するコードを解析してみると、その考え方が非常に明確であることがわかりました。この記事では、このシームレスな置き換えプロセスを詳細に分析します。
<スパン
ラオ・ルオの新浪微博。
http://weibo.com/shengyangluo
とフォローを歓迎します
書籍「"Android System Source Code Scenario Analysis"」は、incomingprogrammer.comで公開中です(
http://0xcc0xcd.com
をクリックすると入場できます。
Dalvik VMは、クラスファイルではなくdexファイルを実行することを除けば、実際にはJava VMであることが分かっています。したがって、ARTランタイムは、理想的には、Java VMとして実装され、Dalvik VMを簡単に置き換えることができるようにすることです。Java VMとして実装するというのは、実際にはJava VMと完全に互換性のあるインターフェースのセットを提供するという意味であることに注意されたい。例えば、Dalvik VMは、そのインタフェースの点ではJava VMと一致していますが、その内部は全く異なるものになる可能性があります。
事実上、ARTランタイムはDalvik仮想マシンと同じであり、Java仮想マシンと完全に互換性のあるインターフェースのセットを実装しています。説明を簡単にするために、次にARTランタイムをART仮想マシンと呼び、Dalvik仮想マシンおよびJava仮想マシンとの関係を図1に示す。
図1 Java VM、Dalvik VM、ARTランタイムの関係
図1から、Dalvik VMとART VMの両方が、Java VMを抽象化するために使用される3つのインターフェースを実装していることがわかる。
1. JNI_GetDefaultJavaVMInitArgs -- 仮想マシンのデフォルトの初期化パラメータを取得する
2. JNI_CreateJavaVM -- プロセス内に仮想マシン・インスタンスを作成します。
3. JNI_GetCreatedJavaVMs -- プロセス内で作成された仮想マシンインスタンスを取得します。
Androidでは、Davik VMはlibdvm.soで、ART VMはlibart.soで実装されています。つまり、libdvm.soとlibart.soは、外部から呼び出せるように、JNI_GetDefaultJavaVMInitArgs、JNI_CreateJavaVM、JNI_GetCreatedJavaVMsというインターフェースをエクスポートしています。
また、Androidにはシステムプロパティとしてpersist.sys.dalvik.vm.libがあり、その値はlibdvm.soまたはlibart.soと等しくなっています。libdvm.soと等しい場合、現在Dalvik仮想マシンを使用していることを意味し、libart.soと等しい場合、現在ART仮想マシンを使用していることを意味します。
以上、Dalvik VMとART VMの共通点を説明しましたが、両者の最も大きな違いは、もちろん、Dalvik VMとART VMが異なるということです。その違いとは、Dalvik VMはdexバイトコードを実行し、ART VMはネイティブマシンコードを実行するということです。つまり、Dalvik VMにはdexバイトコードを実行するためのインタプリタが含まれており、そのインタプリタは Dalvik仮想マシンの簡単な紹介と学習計画 を本連載で紹介します。もちろん、2.2以降のAndroidにはJIT(Just-In-Time)も搭載されており、これを利用して実行度の高いdexバイトコードを実行時に動的にローカルマシンコードに変換し、実行することができる。JITを用いることで、Dalvik仮想マシンの実行効率を効果的に向上させることができる。しかし、dexバイトコードからローカルマシンコードへの変換はアプリケーションの実行時に行われるため、アプリケーションを再実行するたびに変換をやり直す必要がある。そのため、JITを使用しても、Dalvik VMの全体的な性能は、ネイティブのマシンコードを直接実行するART VMの性能には及びません。
AndroidのランタイムがDalvik VMからART VMに置き換わったことで、開発者はアプリをターゲットのマシンコードに直接再コンパイルする必要がありません。つまり、開発者が開発したアプリケーションは、コンパイルとパッケージング後もdexバイトコードを含むAPKファイルであることに変わりはありません。アプリケーションはまだdexバイトコードを含んでおり、ART仮想マシンはネイティブのマシンコードを必要とするため、翻訳プロセスが必要です。この変換処理は、もちろんアプリケーションの実行中には行えません。そうでなければ、Dalvik VMのJITと同じになってしまいます。コンピュータの世界では、JITの反対語はAOTで、Ahead-Of-Timeの略で、プログラムが実行される前に発生します。静的言語(C/C++など)でアプリケーションを開発する場合、コンパイラが直接ターゲットのマシンコードに翻訳する。このような静的言語のコンパイル方法もAOTの一種である。しかし、先に述べたように、ART仮想マシンは、開発者がアプリケーションをターゲットマシンコードに直接コンパイルする必要がない。したがって、アプリケーションのdexバイトコードをローカルのマシンコードに変換する最も適切なAOTのタイミングは、アプリケーションのインストール時に発生する。
ART仮想マシンの前に、アプリケーションはインストール時に実際に"翻訳"プロセスを実行することが分かっています。この翻訳処理だけが、dexバイトコードを最適化する、つまりdexファイルからodexファイルを生成します。この処理は、インストールサービスのPackageManagerServiceがデーモンinstalldに依頼することで行われる。このような観点から、アプリケーションのインストール時にdexバイトコードをローカルのマシンコードに変換しても、本来のアプリケーションのインストール処理には基本的に影響がない。
この背景知識をもとに、ART VMがDalvik VMをシームレスに置き換える方法について、2つの観点から説明しましょう。
1. ART VMのスタートアップ・プロセス。
2. Dexバイトコードからネイティブのマシンコードに変換する処理。
Androidは起動時にアプリプロセスのインキュベーターとして機能するZygoteプロセスを作成することが分かっています。Zygoteプロセスは、起動時にDalvik仮想マシンを作成します。これは、ザイゴートプロセスが自身のDalvik仮想マシンをアプリケーションプロセスにコピーすることを意味する。実際、Zygoteプロセスは自分自身をコピーすることによってアプリケーションプロセスを作成し、アプリケーションプロセスがDalvik仮想マシンを作成する必要がないだけでなく、さまざまなシステムライブラリやリソースがすでにZygoteプロセスで読み込まれ、Dalvik仮想マシンと共にアプリケーションプロセスにコピーされているため、アプリケーションプロセスがロードする処理を行う必要がありません。Zygoteプロセスおよびアプリケーションプロセスの起動に関する詳細については Androidプロセス「Zygote」起動処理のソースコード解析結果 と Androidアプリケーションプロセス起動処理のソースコード解析 この2つの記事
つまり、アプリプロセス内のDalvik VMはZygoteプロセスからコピーされたものなので、ZygoteプロセスがどのようにDalvik VMを作成するのかに話を移しましょう。アプリプロセスから Dalvik VM起動プロセスの解析 この記事では、Zygoteプロセス内のDalvik仮想マシンは、関数AndroidRuntime::startから作成されることが示されています。そこで、次にこの関数の実装を見てみましょう。
void AndroidRuntime::start(const char* className, const char* options)
{
......
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env) ! = 0) {
return;
}
......
/* Start VM.
* This thread becomes the main thread of the VM, and will
This thread becomes the main thread of the VM, and will * not return until the VM exits.
*/
char* slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
/* keep going */
} else {
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
/* keep going */
} else {
env->CallStaticVoidMethod(startClass, startMeth, strArray);
#if 0
if (env->ExceptionCheck())
threadExitUncaughtException(env);
#endif
}
}
......
}
この関数は、frameworks/base/core/jni/AndroidRuntime.cppに定義されています。
AndroidRuntimeクラスのメンバ関数startは、最も顕著に以下の3つのことを行います。
1. JniInvocationのインスタンスを生成し、そのメンバ関数initを呼び出してJNI環境を初期化する。
2. AndroidRuntimeクラスのメンバ関数startVmを呼び出して、仮想マシンとそれに対応するJNIインターフェースを作成する、つまり、JavaVMインターフェースとJNIEnvインターフェースを作成します。
3. 上記のJavaVMインターフェースとJNIEnvインターフェースがあれば、Zygoteプロセスで指定されたクラスをロードすることができます。
このうち、1番目と2番目のことが、やはり一番重要です。そこで、それらが対応する関数の実装の解析に移ろう。
JniInvocationクラスのメンバ関数initの実装は以下のとおりです。
#ifdef HAVE_ANDROID_OS
static const char* kLibrarySystemProperty = "persist.sys.dalvik.vm.lib";
#endif
static const char* kLibraryFallback = "libdvm.so";
bool JniInvocation::Init(const char* library) {
#ifdef HAVE_ANDROID_OS
char default_library[PROPERTY_VALUE_MAX];
property_get(kLibrarySystemProperty, default_library, kLibraryFallback);
#else
const char* default_library = kLibraryFallback;
#endif
if (library == NULL) {
library = default_library;
}
handle_ = dlopen(library, RTLD_NOW);
if (handle_ == NULL) {
if (strcmp(library, kLibraryFallback) == 0) {
// Nothing else to try.
ALOGE("Failed to dlopen %s: %s", library, dlerror());
return false;
}
// Note that this is enough to get something like the zygote
// running, we can't property_set here to fix this for the future
// because we are root and not the system user. see
// RuntimeInit.commonInit for where we fix up the property to
// avoid future fallbacks. http://b/11463182
ALOGW("Falling back from %s to %s after dlopen error: %s",
library, kLibraryFallback, dlerror());
library = kLibraryFallback;
handle_ = dlopen(library, RTLD_NOW);
if (handle_ == NULL) {
ALOGE("Failed to dlopen %s: %s", library, dlerror());
return false;
}
}
if (!FindSymbol(interpret_cast<void**>(&JNI_GetDefaultJavaVMInitArgs_),
"JNI_GetDefaultJavaVMInitArgs")) {
return false;
}
if (!FindSymbol(repeat_cast<void**>(&JNI_CreateJavaVM_),
"JNI_CreateJavaVM")) {
return false;
}
if (!FindSymbol(repeat_cast<void**>(&JNI_GetCreatedJavaVMs_),
"JNI_GetCreatedJavaVMs")) {
return false;
}
return true;
}
この関数は、ファイル libnativehelper/JniInvocation.cpp で定義されています。
JniInvocationクラスのメンバ関数initが行っていることは単純です。システムプロパティであるpersist.sys.dalvik.vm.libの値を読み込むところから始まります。先に述べたように、システムプロパティ persist.sys.dalvik.vm.lib の値は libdvm.so か libart.so のどちらかに等しくなっています。したがって、関数dlopenを介してプロセスに次にロードされるのは、libdvm.soかlibart.soのどちらかである。どちらのsoがロードされても、JNI_GetDefaultJavaVMInitArgs、JNI_CreateJavaVM、JNI_GetCreatedJavaVMsの3つのインターフェースをエクスポートし、JniInvocationクラスJNI_ GetDefaultJavaVMInitArgs_、JNI_CreateJavaVM_、JNI_GetCreatedJavaVMs_のそれぞれのメンバー変数に保存することが要求されます。これらは、先に述べた、Java仮想マシンを抽象化するための3つのインターフェースと同じです。
このように、JniInvocationクラスのメンバ関数initは、システムプロパティpersist.sys.dalvik.vm.libに基づいて、Dalvik VMまたはART VM環境を実際に初期化します。
次に、AndroidRuntime クラスの startVm メンバ関数の実装に移ります。
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv)
{
......
/*
* Initialize the VM.
*
* The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.
* If this call succeeds, the VM is ready, and we can start issuing
* JNI calls.
*/
if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
ALOGE("JNI_CreateJavaVM failed\n");
goto bail;
}
......
}
この関数は、frameworks/base/core/jni/AndroidRuntime.cppに定義されています。
AndroidRuntimeクラスのメンバ関数startVmは、特に関数JNI_CreateJavaVMを呼び出して、JavaVMインタフェースとそれに対応するJNIEnvインタフェースを生成しています。
extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
return JniInvocation::GetJniInvocation().JNI_CreateJavaVM(p_vm, p_env, vm_args);
}
この関数は、ファイル libnativehelper/JniInvocation.cpp で定義されています。
JniInvocation クラスの静的メンバ関数 GetJniInvocation は、先に作成した JniInvocation のインスタンスを返します。このJniInvocationインスタンスを取得したら、そのメンバ関数JNI_CreateJavaVMを呼び出して、JavaVMインタフェースとそれに対応するJNIEnvインタフェースを生成してください。
jint JniInvocation::JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
return JNI_CreateJavaVM_(p_vm, p_env, vm_args);
}
この関数は、ファイル libnativehelper/JniInvocation.cpp で定義されています。
JniInvocation クラスのメンバ変数 JNI_CreateJavaVM_ は、先にロードした libdvm.so または libart.so がエクスポートした関数 JNI_CreateJavaVM を指すので、JniInvocation クラスのメンバ関数 JNI_CreateJavaVM は Dalvik 仮想マシンまたは ART 仮想マシンのいずれかを指すことになるのです。
以上の分析から、Androidは、ARTランタイムをJava VMに抽象化し、システムプロパティpersist.sys.dalvik.vm.libと適応層JniInvocationを使って、Dalvik VMをARTランタイムにスムーズに置き換えることが容易に分かります。この置き換えプロセスは非常に巧妙に設計されており、関係するコードの変更はごくわずかです。
こうしてART仮想マシンが起動し、アプリケーションのインストール時にdexバイトコードをネイティブなマシンコードに変換するプロセスを分析します。
Androidアプリのインストール手順については Androidアプリケーションインストール処理のソースコード解析 この記事 要するに、AndroidシステムはPackageManagerServiceを通じてAPKをインストールし、インストール中にPackageManagerServiceは別クラスInstallerのメンバ関数dexopt:を通じてAPK内のdexバイトコードを最適化するということです。
public final class Installer {
......
public int dexopt(String apkPath, int uid, boolean isPublic) {
StringBuilder builder = new StringBuilder("dexopt");
builder.append(' ');
builder.append(apkPath);
builder.append(' ');
builder.append(uid);
builder.append(isPublic ? " 1" : " 0");
return execute(builder.toString());
}
......
}
この関数は、frameworks/base/services/java/com/android/server/pm/Installer.java に定義されています。
Installer はソケット経由でデーモン installd に dexopt リクエストを送信し、installld: 内の関数 dexopt によって処理されます。
int dexopt(const char *apk_path, uid_t uid, int is_public)
{
struct utimbuf ut;
struct stat apk_stat, dex_stat;
char out_path[PKG_PATH_MAX];
char dexopt_flags[PROPERTY_VALUE_MAX];
char persist_sys_dalvik_vm_lib[PROPERTY_VALUE_MAX];
char *end;
int res, zip_fd=-1, out_fd=-1;
......
/* The command to run depends ones the value of persist.sys.dalvik.vm.lib */
property_get("persist.sys.dalvik.vm.lib", persist_sys_dalvik_vm_lib, "libdvm.so");
/* Before anything else: is there a .odex file?
If so, we have * precompiled the apk and there is nothing to do here.
*/
sprintf(out_path, "%s%s", apk_path, ".odex");
if (stat(out_path, &dex_stat) == 0) {
return 0;
}
if (create_cache_path(out_path, apk_path)) {
return -1;
}
......
out_fd = open(out_path, O_RDWR | O_CREAT | O_EXCL, 0644);
......
pid_t pid;
pid = fork();
if (pid == 0) {
......
if (strncmp(persist_sys_dalvik_vm_lib, "libdvm", 6) == 0) {
run_dexopt(zip_fd, out_fd, apk_path, out_path, dexopt_flags);
} else if (strncmp(persist_sys_dalvik_vm_lib, "libart", 6) == 0) {
run_dex2oat(zip_fd, out_fd, apk_path, out_path, dexopt_flags);
} else {
exit(69); /* Unexpected persist.sys.dalvik.vm.lib value */
}
exit(68); /* only get here on exec failure */
}
......
}
この関数は、frameworks/native/cmds/installd/commands.c というファイルで定義されています。
関数 dexopt は、まずシステムプロパティ persist.sys.dalvik.vm.lib の値を読み取り、次に /data/dalvik-cache ディレクトリに odex ファイルを作成します。このodexファイルは、最適化されたdexファイルとして使用される出力ファイルである。次に、関数 dexopt はフォークして子プロセスを作成します。システムプロパティ persist.sys.dalvik.vm.lib の値が libdvm.so と等しい場合、そのサブプロセスは関数 run_dexopt を呼び出して dex ファイルを odex ファイルに最適化します。一方、システムプロパティ persist.sys.dalvik.vm.lib の値が libart.so と等しい場合、サブプロセスは関数 run_dex2oat を呼び出して dex ファイルを oat ファイルに変換し、これは実際に dex バイトコードをローカルのマシンコードに変換して oat ファイルに保存していることになるのです。
関数run_dexoptとrun_dex2oatの実装を以下に示す。
static void run_dexopt(int zip_fd, int odex_fd, const char* input_file_name,
const char* output_file_name, const char* dexopt_flags)
{
static const char* DEX_OPT_BIN = "/system/bin/dexopt";
static const int MAX_INT_LEN = 12; // '-'+10dig+'\0' -OR- 0x+8dig
char zip_num[MAX_INT_LEN];
char odex_num[MAX_INT_LEN];
sprintf(zip_num, "%d", zip_fd);
sprintf(odex_num, "%d", odex_fd);
ALOGV("Running %s in=%s out=%s\n", DEX_OPT_BIN, input_file_name, output_file_name);
execl(DEX_OPT_BIN, DEX_OPT_BIN, "--zip", zip_num, odex_num, input_file_name,
dexopt_flags, (char*) NULL);
ALOGE("execl(%s) failed: %s\n", DEX_OPT_BIN, strerror(errno));
}
static void run_dex2oat(int zip_fd, int oat_fd, const char* input_file_name,
const char* output_file_name, const char* dexopt_flags)
{
static const char* DEX2OAT_BIN = "/system/bin/dex2oat";
static const int MAX_INT_LEN = 12; // '-'+10dig+'\0' -OR- 0x+8dig
char zip_fd_arg[strlen("--zip-fd=") + MAX_INT_LEN];
char zip_location_arg[strlen("--zip-location=") + PKG_PATH_MAX];
char oat_fd_arg[strlen("--oat-fd=") + MAX_INT_LEN];
char oat_location_arg[strlen("--oat-name=") + PKG_PATH_MAX];
sprintf(zip_fd_arg, "--zip-fd=%d", zip_fd);
sprintf(zip_location_arg, "--zip-location=%s", input_file_name);
sprintf(oat_fd_arg, "--oat-fd=%d", oat_fd);
sprintf(oat_location_arg, "--oat-location=%s", output_file_name);
ALOGV("Running %s in=%s out=%s\n", DEX2OAT_BIN, input_file_name, output_file_name);
execl(DEX2OAT_BIN, DEX2OAT_BIN,
zip_fd_arg, zip_location_arg,
oat_fd_arg, oat_location_arg,
(char*) NULL);
ALOGE("execl(%s) failed: %s\n", DEX2OAT_BIN, strerror(errno));
}
この2つの関数は、frameworks/native/cmds/installd/commands.cというファイルに定義されています。
これは、run_dexopt関数が/system/bin/dexoptを呼び出してdexバイトコードを最適化し、run_dex2oat関数が/system/bin/dex2oatを呼び出してdexバイトコードをローカルマシンコードに変換していることを表しています。dexバイトコードの最適化とdexバイトコードのローカルマシンコードへの変換の両方が同じ名前のodexファイルに格納されていますが、前者はdeyファイル(最適化されたdexであることを示す)に対応し、後者はoatファイル(これは実際には(すべてのローカルマシン命令を含む)カスタムelfファイルである)に対応していることに注意してください。このように、絶対パスでodexファイルを参照するコードはすべて修正する必要がありません。
以上の分析により、アプリケーションのインストール時に、dexファイルの最適化処理を、dexファイルをネイティブのマシンコードに変換する処理に置き換えるだけで、Dalvik仮想マシンをARTランタイムにシームレスに置き換えることができることが容易に理解できるはずです。
最後にもう一つ、アプリケーションのインストールは、システム起動時と、システム起動終了後にユーザー自身がインストールする時の2つのタイミングで行われることに注意したい。最初のタイミングでは、/system/app および /data/app ディレクトリにあるすべての APK の dex バイトコード→ローカル機械語翻訳に加えて、/system/framework ディレクトリにある APK または JAR ファイルと、それらの APK が参照する外部 JAR の dex バイトコード→ローカル機械語翻訳も行います。 翻訳は、/system/framework ディレクトリにあるすべての APK の dex バイテク ド翻訳と、/system/app ディレクトリにある JAR ファイルおよびその APK が参照する JAR のデコード翻訳を行います。こうすることで、システム内のJavaで開発されたシステムサービスは、アプリケーションを除き、一律にdexバイトコードからローカルのマシンコードに翻訳されるようにすることができます。つまり、AndroidのDalvik仮想マシンをARTランタイムに置き換えた後は、システム内のすべてのコードがARTランタイムで実行され、現時点ではDalvik仮想マシンへの依存はない。
この時点で、Dalvik VMをARTランタイムにシームレスに置き換えるプロセスの分析が終了しました。今後は、ラオ・ルオの新浪微博のページで辛口のヒントを得ることにします。 http://weibo.com/shengyangluo
関連
-
Android 開発において、null オブジェクトの参照で仮想メソッドを呼び出そうとする。
-
Android フロントカメラのビデオ録画に失敗しました (MediaRecorder: start failed: -19)
-
AndroidでSPAN_EXCLUSIVE_EXCLUSIVEスパンが長さ0にできない場合、EditTextでコンテンツを削除する
-
エラーを解決する SSLピアが正しくシャットダウンされない
-
Android携帯で通常のhttpsのサイトにアクセスすると、最初のリクエストで認証パスのトラストアンカーが見つからないと報告され、その後正常にアクセスできるようになり、問題が解決しました。
-
Android Control - TabLayout Usage Introduction
-
Androidレイアウトにおけるmargin,padding,alignの使い分けと違いについて
-
Android動的ブロードキャストの追加許可
-
AndroidでデータをExcelファイルに書き出す方法
-
AndroidManifest.xml の use-sdk 警告メソッドを削除する。
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
Android Studio + Gradle またはコマンドラインを使用した Android apk の署名とパッケージング
-
ADBサーバーがACKしない問題を解決 (pro-test)
-
AndroidでFragmentを使用すると、Fragmentの内部コントロールを取得できず、findViewById()の結果がNullになる - 解決済み
-
Jniエラー:構造体でも組合でもないものにメンバー 'FindClass' を要求する、 解決方法
-
アプリケーションがメインスレで仕事をしすぎている可能性がある
-
ConstraintLayoutにおけるChainとGuidelineの利用について
-
Android ProgressBarの詳しい解説とカスタマイズ方法
-
Androidアプリ】【形状利用概要
-
Android AVDで "このターゲットにはシステムイメージがインストールされていません "と表示される
-
Android SDKです。sdkmanagerコマンドラインツールの使用(パッケージの表示、インストール、アップデート、アンインストール)