Android 5.0 (Lollipop) のJavaアプリケーションは32bitと64bitのどちらで動作するか?

Android 5.0 (Lollipop) では64bitプロセッサへの対応が行われており、
Linux Kernelはもちろん多くのプロセスが64bitプロセスとして動作します。
また、DalvikVMに変わって採用されたARTランタイムにおいても64bitプロセッサへの対応は行われています。
さて、それでは64bitプロセッサで動作するAndroid上では全てのJavaアプリケーションが64bitとして動作するのでしょうか?
その答えは「場合によっては32bitプロセスとして動作することもある」となります。

AndroidにおけるJavaアプリの起動

さて、ここからは実際にどういったロジックで32bit/64bitプロセスとして分かれていくのかを、順をおって見ていきたいと思います。

起動までの全体像

まずはおさらい的な内容になりますが、Javaアプリがどのように起動されるのか、
その大まかな流れを見てみたいと思います。

f:id:crimsonwoods:20141106234544p:plain

ざっくりとは上図のような流れになっており、箇条書きにすると下記のようになります。

  1. Intent発行
  2. ActivityManagerからZygoteへUnix domain socketを通じて通信
  3. Zygoteからfork(app_process)
  4. ZygoteからforkされたプロセスがJVMを起動してJavaの動作を開始

さてこの流れを頭にいれた上で、Lollipopでは何が違うのかを見て行きたいと思います。

Zygoteとapp_process

Androidの仕組みとして、全てのJavaアプリケーションプロセスがZygoteからforkされるというのは既にご理解頂けていると思います。
では、このZygoteの正体とは何でしょうか?それはapp_processというプログラムになります。
このapp_processはinit(一番最初に起動されるユーザーランドのプロセスで、初期化のためのいろいろな処理を行います)から起動され、
プロセスが不具合によってクラッシュしない限りは常にバックグランドで動作しています。

このZygoteの起動がAndroid 5.0になって、ちょっと変わっています。
どのように変わったのか、Android 4.4.4と比較してみましょう。
まず、Android 4.4.4のinit.rcです。

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd
rootdir/init.rc - platform/system/core - Git at Google

Android 4.4.4ではinit.rcの中に直接Zygoteを起動するための処理が記載されていました。

続いてAndroid 5.0のinit.rcを見てみましょう。

import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc
import /init.trace.rc
rootdir/init.rc - platform/system/core - Git at Google

こちらでは外部のZygote用のinit.xxx.rcを呼び出すように変更されています。
"ro.zygote"というシステムプロパティに設定されているのは64bitな環境であれば"zygote64_32"といったような値になりますので、
init.zygote64_32.rcというスクリプトがimportされることになります。

それではinit.zygote64_32.rcの中身を見てみましょう。

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd
service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
    class main
    socket zygote_secondary stream 660 root system
    onrestart restart zygote
rootdir/init.zygote64_32.rc - platform/system/core - Git at Google

Android 4.4.4ではapp_processのみをサービスとして起動していましたが、Android 5.0ではapp_process32とapp_process64という別々のプログラムをサービスとして起動しています。
このことから、Android 5.0 (Lollipop) では32bitプロセスとして起動する場合は32bitのZygoteを、64bitであれば64bitのZygoteを使うのだろうと予想されます。
(元々ZygoteはLibraryのLoadを省いて起動を高速化したりリソースを効率よく使用するための仕組みなので、上記の予想はそれほど的外れではないと思います)

また、app_processに渡される"--socket-name"オプションに異なる名前が渡されていますが、これは単純に32/64bitそれぞれで異なるUNIX domain socketを用意しておき、
32bitアプリケーションなら32bit版のZygoteを、64bitアプリケーションなら64bit版のZygoteを使うようになるということだと予想されます。

ZygoteとABI

さて、それでは次はZygoteに実際にJVMの起動をお願いする部分を見て行きたいと思います。
先に結論を書いてしまうと、
Zygoteの用意しているsocketに対してアクセスを行うのは、android.os.ProcessクラスにあるProcess#startメソッドになります。

    public static final ProcessStartResult start(final String processClass,
                                  final String niceName,
                                  int uid, int gid, int[] gids,
                                  int debugFlags, int mountExternal,
                                  int targetSdkVersion,
                                  String seInfo,
                                  String abi,
                                  String instructionSet,
                                  String appDataDir,
                                  String[] zygoteArgs) {
        try {
            return startViaZygote(processClass, niceName, uid, gid, gids,
                    debugFlags, mountExternal, targetSdkVersion, seInfo,
                    abi, instructionSet, appDataDir, zygoteArgs);
        } catch (ZygoteStartFailedEx ex) {
            Log.e(LOG_TAG,
                    "Starting VM process through Zygote failed");
            throw new RuntimeException(
                    "Starting VM process through Zygote failed", ex);
        }
    }
core/java/android/os/Process.java - platform/frameworks/base - Git at Google

Process#startViaZygoteだけ見てもちょっと判りませんが、Process#startViaZygoteの内部でProcess#openZygoteSocketIfNeededを呼び出した後、Process#zygoteSendArgsAndGetResultをによって、Zygoteのsocketへアクセスを行います。
このZygote socketへのアクセスの結果として、ZygoteではプロセスのforとkJVMの起動が行われます。
さて、それではこの時32/64bitのどちらのZygoteにアクセスするかを決めているのは何でしょうか?

    private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
        if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
            try {
                primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);
            } catch (IOException ioe) {
                throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
            }
        }
        if (primaryZygoteState.matches(abi)) {
            return primaryZygoteState;
        }
        // The primary zygote didn't match. Try the secondary.
        if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
            try {
            secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET);
            } catch (IOException ioe) {
                throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
            }
        }
        if (secondaryZygoteState.matches(abi)) {
            return secondaryZygoteState;
        }
        throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
    }

Process#openZygoteSocketIfNeededの中身を見てみると、Zygoteへの接続を予め確立しておき、32/64bit版それぞれに対してZygoteState.matchesメソッドを呼び出すことでどのsocketを使用するかを決定しています。
では、ZygoteState.matchesを見てみましょう。
ZygoteStateクラスはProcessクラスの内部に定義されています。

        boolean matches(String abi) {
            return abiList.contains(abi);
        }
core/java/android/os/Process.java - platform/frameworks/base - Git at Google

実に完結で、内部に保持しているABIのリストに該当するものがあるかどうかを見ているわけですね。
ではこの内部のabiListなるものはどうやって生成されるのか?ですが、これはZygoteState#connectの呼び出し時に解決されます。

        public static ZygoteState connect(String socketAddress) throws IOException {
            DataInputStream zygoteInputStream = null;
            BufferedWriter zygoteWriter = null;
            final LocalSocket zygoteSocket = new LocalSocket();
            try {
                zygoteSocket.connect(new LocalSocketAddress(socketAddress,
                        LocalSocketAddress.Namespace.RESERVED));
                zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream());
                zygoteWriter = new BufferedWriter(new OutputStreamWriter(
                        zygoteSocket.getOutputStream()), 256);
            } catch (IOException ex) {
                try {
                    zygoteSocket.close();
                } catch (IOException ignore) {
                }
                throw ex;
            }
            String abiListString = getAbiList(zygoteWriter, zygoteInputStream);
            Log.i("Zygote", "Process: zygote socket opened, supported ABIS: " + abiListString);
            return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter,
                    Arrays.asList(abiListString.split(",")));
        }
    private static String getAbiList(BufferedWriter writer, DataInputStream inputStream)
            throws IOException {
        // Each query starts with the argument count (1 in this case)
        writer.write("1");
        // ... followed by a new-line.
        writer.newLine();
        // ... followed by our only argument.
        writer.write("--query-abi-list");
        writer.newLine();
        writer.flush();
        // The response is a length prefixed stream of ASCII bytes.
        int numBytes = inputStream.readInt();
        byte[] bytes = new byte[numBytes];
        inputStream.readFully(bytes);
        return new String(bytes, StandardCharsets.US_ASCII);
    }
core/java/android/os/Process.java - platform/frameworks/base - Git at Google

ZygoteState#connectではProcess#getAbiListを呼び出して、動的にZygoteから対応しているABI(Application Binary Interface)のリストを取得しています。
ABIについての詳細はNDKに同梱されているCPU-ARCH-ABISのドキュメントを読んで頂ければよいかと思います。
さて、ここまでをまとめてみましょう。

  1. Zygoteは常時32bit版と64bit版が同時に動いている
  2. 32bit Javaアプリケーションは32bit版のZygoteを使用する(推測)
  3. 64bit Javaアプリケーションは64bit版のZygoteを使用する(推測)
  4. ZygoteへのJVMの起動依頼はProcess#startメソッドで行われる
  5. Process#startメソッドは渡されたABIから、32bit/64bit版のそれぞれのZygoteのうちABIに対応している方の接続を利用する。

あとは、「Process#startメソッドに渡されるABIがどのようにして決定されるのか?」という点と、「Zygoteが対応しているABIはどうやって決定されるのか?」が判れば、Javaアプリケーションが32/64bitのどちらで動作するのかが判るということになります。

ZygoteとABI

「Zygoteが対応しているABIはどうやって決定されるのか?」については、Zygoteの元になっているapp_processのソースを読むことで知ることができます。
app_processが呼び出されると、SystemPropertyからABIのリストを取得し、それをそのままZygoteInitに渡すようになっています。
Zygoteの内部ではZygoteInitに渡されたパラメータを保持しておき、Zygote socketを経由して問い合わせがあると、保持していたパラメータを返すという構造になっています。

    Vector<String8> args;
    if (!className.isEmpty()) {
        // We're not in zygote mode, the only argument we need to pass
        // to RuntimeInit is the application argument.
        //
        // The Remainder of args get passed to startup class main(). Make
        // copies of them before we overwrite them with the process name.
        args.add(application ? String8("application") : String8("tool"));
        runtime.setClassNameAndArgs(className, argc - i, argv + i);
    } else {
        // We're in zygote mode.
        maybeCreateDalvikCache();
        if (startSystemServer) {
            args.add(String8("start-system-server"));
        }
        char prop[PROP_VALUE_MAX];
        if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
            LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
                ABI_LIST_PROPERTY);
            return 11;
        }
        String8 abiFlag("--abi-list=");
        abiFlag.append(prop);
        args.add(abiFlag);
        // In zygote mode, pass all remaining arguments to the zygote
        // main() method.
        for (; i < argc; ++i) {
            args.add(String8(argv[i]));
        }
    }
    if (!niceName.isEmpty()) {
        runtime.setArgv0(niceName.string());
        set_process_name(niceName.string());
    }
    if (zygote) {
        runtime.start("com.android.internal.os.ZygoteInit", args);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args);
    } else {
        fprintf(stderr, "Error: no class name or --zygote supplied.\n");
        app_usage();
        LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
        return 10;
    }
cmds/app_process/app_main.cpp - platform/frameworks/base - Git at Google

ちょっと長いですが、肝心なのはproperty_getを呼び出している箇所以降になります。
property_getでSystemPropertyから取得したABIのリストが使用されるわけですが、ではこのSystemPropertyのキーはどうなっているのか?というと、下記のように定義されています。

#if defined(__LP64__)
static const char ABI_LIST_PROPERTY[] = "ro.product.cpu.abilist64";
static const char ZYGOTE_NICE_NAME[] = "zygote64";
#else
static const char ABI_LIST_PROPERTY[] = "ro.product.cpu.abilist32";
static const char ZYGOTE_NICE_NAME[] = "zygote";
#endif
cmds/app_process/app_main.cpp - platform/frameworks/base - Git at Google

わかりやすいですね。32/64bit版それぞれにABIのリストをもっているということになります。

各アプリケーションのABI

それでは最後の、「Process#startメソッドに渡されるABIがどのようにして決定されるのか?」についてを見て行きたいと思います。
Process#startメソッドが呼び出されているのはcom.android.server.am.ActivityManagerServiceのActivityManagerService.startProcessLockedメソッドになります。

            String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;
            if (requiredAbi == null) {
                requiredAbi = Build.SUPPORTED_ABIS[0];
            }
            String instructionSet = null;
            if (app.info.primaryCpuAbi != null) {
                instructionSet = VMRuntime.getInstructionSet(app.info.primaryCpuAbi);
            }
            // Start the process.  It will either succeed and return a result containing
            // the PID of the new process, or else throw a RuntimeException.
            boolean isActivityProcess = (entryPoint == null);
            if (entryPoint == null) entryPoint = "android.app.ActivityThread";
            checkTime(startTime, "startProcess: asking zygote to start proc");
            Process.ProcessStartResult startResult = Process.start(entryPoint,
                    app.processName, uid, uid, gids, debugFlags, mountExternal,
                    app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,
                    app.info.dataDir, entryPointArgs);
services/core/java/com/android/server/am/ActivityManagerService.java - platform/frameworks/base - Git at Google

abiOverrideが指定されている場合はそのABIを、そうでなければApplicationInfoクラスのprimaryCpuAbiをABIとして渡しています。
現状ではabiOverrideはInstrumentationの場合にだけ使用されるオプションになるため、通常のアプリケーションの起動時にはabiOverrideはnullになります。
このことから、Process#startに渡されるのはアプリケーションごとのABIに関する情報で、その情報はProcessRecordクラスに保持されているということが判ります。
では、ProcessRecordはどのように生成され、primaryCpuAbiがどのように設定されるのでしょうか?
ProcessRecordを生成するのはActivityManagerService.newProcessRecordLockedメソッドになります。

    final ProcessRecord newProcessRecordLocked(ApplicationInfo info, String customProcess,
            boolean isolated, int isolatedUid) {
        String proc = customProcess != null ? customProcess : info.processName;
        BatteryStatsImpl.Uid.Proc ps = null;
        BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics();
        int uid = info.uid;
        if (isolated) {
            if (isolatedUid == 0) {
                int userId = UserHandle.getUserId(uid);
                int stepsLeft = Process.LAST_ISOLATED_UID - Process.FIRST_ISOLATED_UID + 1;
                while (true) {
                    if (mNextIsolatedProcessUid < Process.FIRST_ISOLATED_UID
                            || mNextIsolatedProcessUid > Process.LAST_ISOLATED_UID) {
                        mNextIsolatedProcessUid = Process.FIRST_ISOLATED_UID;
                    }
                    uid = UserHandle.getUid(userId, mNextIsolatedProcessUid);
                    mNextIsolatedProcessUid++;
                    if (mIsolatedProcesses.indexOfKey(uid) < 0) {
                        // No process for this uid, use it.
                        break;
                    }
                    stepsLeft--;
                    if (stepsLeft <= 0) {
                        return null;
                    }
                }
            } else {
                // Special case for startIsolatedProcess (internal only), where
                // the uid of the isolated process is specified by the caller.
                uid = isolatedUid;
            }
        }
        return new ProcessRecord(stats, info, proc, uid);
    }
https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r2/services/core/java/com/android/server/am/ActivityManagerService.java

ここではProcessRecordを作っているだけで、パラメータとして受け取っとApplicationInfoをそのまま使いまわしているように見えます。
ではこのApplicationInfoはどこからやってくるのでしょうか?
正解はcom.android.server.pm.PackageManagerServiceクラスです。
ApplicationInfoはPackageManagerServiceで生成され、ActivityManagerServiceからその情報を参照する形になっています。
ActivityManagerService内部でPackageManagerService.queryIntentAcitivitiesなどを呼び出すことで、ApplicationInfoを取得することができるようになっています。

さて、ではApplicationInfoのprimaryCpuAbiが設定されるのは、PackageManagerServiceのどこでしょうか?
答えはPackageManagerService.scanPackageDirtyLIメソッドの中になります。

                pkg.applicationInfo.primaryCpuAbi = null;
                pkg.applicationInfo.secondaryCpuAbi = null;
                if (isMultiArch(pkg.applicationInfo)) {
                    // Warn if we've set an abiOverride for multi-lib packages..
                    // By definition, we need to copy both 32 and 64 bit libraries for
                    // such packages.
                    if (pkg.cpuAbiOverride != null
                            && !NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(pkg.cpuAbiOverride)) {
                        Slog.w(TAG, "Ignoring abiOverride for multi arch application.");
                    }
                    int abi32 = PackageManager.NO_NATIVE_LIBRARIES;
                    int abi64 = PackageManager.NO_NATIVE_LIBRARIES;
                    if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
                        if (isAsec) {
                            abi32 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_32_BIT_ABIS);
                        } else {
                            abi32 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
                                    nativeLibraryRoot, Build.SUPPORTED_32_BIT_ABIS,
                                    useIsaSpecificSubdirs);
                        }
                    }
                    maybeThrowExceptionForMultiArchCopy(
                            "Error unpackaging 32 bit native libs for multiarch app.", abi32);
                    if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
                        if (isAsec) {
                            abi64 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_64_BIT_ABIS);
                        } else {
                            abi64 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
                                    nativeLibraryRoot, Build.SUPPORTED_64_BIT_ABIS,
                                    useIsaSpecificSubdirs);
                        }
                    }
                    maybeThrowExceptionForMultiArchCopy(
                            "Error unpackaging 64 bit native libs for multiarch app.", abi64);
                    if (abi64 >= 0) {
                        pkg.applicationInfo.primaryCpuAbi = Build.SUPPORTED_64_BIT_ABIS[abi64];
                    }
                    if (abi32 >= 0) {
                        final String abi = Build.SUPPORTED_32_BIT_ABIS[abi32];
                        if (abi64 >= 0) {
                            pkg.applicationInfo.secondaryCpuAbi = abi;
                        } else {
                            pkg.applicationInfo.primaryCpuAbi = abi;
                        }
                    }
                } else {
                    String[] abiList = (cpuAbiOverride != null) ?
                            new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS;
                    // Enable gross and lame hacks for apps that are built with old
                    // SDK tools. We must scan their APKs for renderscript bitcode and
                    // not launch them if it's present. Don't bother checking on devices
                    // that don't have 64 bit support.
                    boolean needsRenderScriptOverride = false;
                    if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null &&
                            NativeLibraryHelper.hasRenderscriptBitcode(handle)) {
                        abiList = Build.SUPPORTED_32_BIT_ABIS;
                        needsRenderScriptOverride = true;
                    }
                    final int copyRet;
                    if (isAsec) {
                        copyRet = NativeLibraryHelper.findSupportedAbi(handle, abiList);
                    } else {
                        copyRet = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle,
                                nativeLibraryRoot, abiList, useIsaSpecificSubdirs);
                    }
                    if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES) {
                        throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
                                "Error unpackaging native libs for app, errorCode=" + copyRet);
                    }
                    if (copyRet >= 0) {
                        pkg.applicationInfo.primaryCpuAbi = abiList[copyRet];
                    } else if (copyRet == PackageManager.NO_NATIVE_LIBRARIES && cpuAbiOverride != null) {
                        pkg.applicationInfo.primaryCpuAbi = cpuAbiOverride;
                    } else if (needsRenderScriptOverride) {
                        pkg.applicationInfo.primaryCpuAbi = abiList[0];
                    }
                }
services/core/java/com/android/server/pm/PackageManagerService.java - platform/frameworks/base - Git at Google

少々長いですが、このコードの中でやっているのは、APKがマルチアーキテクチャ構成かどうかをチェックして、マルチアーキテクチャ構成であれば32/64bitのネイティブライブラリを含んでいるかをそれぞれチェックし、primaryCpuAbiとsecondaryCpuAbiを設定するということです。
マルチアーキテクチャ構成の場合、64bitがサポートされている環境下で64bit版のネイティブライブラリを持っている場合はprimaryCpuAbiに64bit用のABIを設定、32bit版のネイティブライブラリを持っている場合はsecondaryCpuAbiに32bit用のABIを設定します。64bit版のネイティブライブラリを持っていない場合で、32bitがサポートされている環境下ではprimaryCpuAbiに32bit用のABIが設定されます。
マルチアーキテクチャ構成でない場合は、RenderScriptを使用するかどうかと、ネイティブライブラリの有無によって自動的に適切なABIが選択されます。

まとめ

Android 5.0 Lollipopにおいて、Javaアプリケーションが32/64bitのどちらで動作するかは、プラットフォームがサポートしているABIと、APKがマルチアーキテクチャ構成をとっているかどうかによります。

プラットフォームが64bitをサポートしている場合

  • マルチアーキテクチャ構成で64bitのネイティブライブラリを含むと64bit
  • マルチアーキテクチャ構成で32bitのネイティブライブラリしか含まない場合は32bit
  • マルチアーキテクチャ構成でない場合は含まれているネイティブライブラリに合わせた方

プラットフォームが64bitをサポートしていない場合

  • どの構成でも32bit

ということになります。