主頁 > 移動端開發 > Zygote——Android系統中java世界的受精卵(二、Welcome To Java)

Zygote——Android系統中java世界的受精卵(二、Welcome To Java)

2021-10-28 08:12:58 移動端開發

0、引言

Android的底層內核是基于Linux構建而成,是在Native世界,而Android上層的應用是隸屬Java世界,那么在Android系統啟動程序中,系統是如何從Native范訓出Java世界的呢?這便是這篇文章的主角Zygote的主要職責,

本文所選Android系統版本是9.0 Pie,文中所有代碼片段路徑在代碼塊第一行已經標注,文章的目的是記錄自己的學習歷程與心得,不做商用或盈利,凡是學習程序中學習或參考過的大佬博文或著作都會盡力標注,在此感謝各位前輩的不吝分享,本文借鑒如下:

  • 《Android系統啟動-zygote篇》—— 袁輝輝
  • 《Android系統行程Zygote啟動程序的源代碼分析》—— 羅升陽
  • 《[深入理解Android卷一全文-第四章]深入理解zygote》 —— 鄧平凡
  • 《Android10.0系統啟動之Zygote行程-[Android取經之路]》—— IngresGe

2、Welcome To Java

在上篇博文《Zygote——Android系統中java世界的受精卵(一、C/C++中的Zygote)》中,我們分析了,C/C++世界Zygote相關的啟動代碼,在結尾處,終于在ZygoteInit.main()函式執行時,進入到了Java世界,所以這邊文章就接著main函式往下看,追蹤Java世界中Zygote相關的內容,main()函式的代碼依舊是分割開來決議 ,

2.1、準備作業

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
public static void main(String[] argv) {
        ZygoteServer zygoteServer = null;
        //pie\libcore\dalvik\src\main\java\dalvik\system\ZygoteHooks.java
        ZygoteHooks.startZygoteNoThreadCreation();    //呼叫native函式,功能是確保此時沒有其他執行緒啟動
        try {
            Os.setpgid(0, 0);                         //設定pid
        } catch (ErrnoException ex) {
            throw new RuntimeException("Failed to setpgid(0,0)", ex);
        }

        Runnable caller;
        try {
            final long startTime = SystemClock.elapsedRealtime();    //系統啟動到現在的時間,包含設備深度休眠的時間
            final boolean isRuntimeRestarted = "1".equals( 
                    SystemProperties.get("sys.boot_completed"));     //該屬性值在設備物理重啟時為空,reboot重啟后為1 
            String bootTimeTag = Process.is64Bit() ? "Zygote64Timing" : "Zygote32Timing";    //設定boot時間列印TAG
            //pie\frameworks\base\core\java\android\util\TimingsTraceLog.java
            TimingsTraceLog bootTimingsTraceLog = new 
                TimingsTraceLog(bootTimeTag,Trace.TRACE_TAG_DALVIK); //通過systrace來追蹤
            bootTimingsTraceLog.traceBegin("ZygoteInit");            //追蹤開始,每個traceBegin()對應一個traceEnd()
            //使能DDMS(Dalvik Debug Monitor Server),注冊所有已知的Java VM的處理塊的監聽器,
            //執行緒監聽、記憶體監聽、native堆記憶體監聽、debug模式監聽…
            RuntimeInit.preForkInit();    

            boolean startSystemServer = false;
            String zygoteSocketName = "zygote";
            String abiList = null;
            boolean enableLazyPreload = false;
            for (int i = 1; i < argv.length; i++) {
                if ("start-system-server".equals(argv[i])) {         //讀取"start-system-server"引數
                    startSystemServer = true;
                //ro.zygote屬性值為zygote64_32或zygote64_32時,會存在另外一個行程zygote_secondary,
                //zygote_secondary -Xzygote /system/bin --zygote --socket-name=zygote_secondary --enable-lazy-preload
                } else if ("--enable-lazy-preload".equals(argv[i])) {
                    enableLazyPreload = true;
                } else if (argv[i].startsWith(ABI_LIST_ARG)) {       //讀取"--abi-list"引數
                    abiList = argv[i].substring(ABI_LIST_ARG.length());
                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {    //讀取"--socket-name"引數
                    zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length());
                } else {
                    throw new RuntimeException("Unknown command line argument: " + argv[i]);
                }
            }
            // PRIMARY_SOCKET_NAME = "zygote"
            final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME);
            if (!isRuntimeRestarted) {
                if (isPrimaryZygote) {
                    FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
                            BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START,
                            startTime);    //FrameworkStatsLog.java == statslog-framework-java-gen
                //SECONDARY_SOCKET_NAME = "zygote_secondary"
                } else if (zygoteSocketName.equals(Zygote.SECONDARY_SOCKET_NAME)) {    
                    FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED,
                            BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START,
                            startTime);
                }
            }

            if (abiList == null) {
                throw new RuntimeException("No ABI list supplied.");
            }
            …………

這一部分主要是為后面的任務做準備作業:

  • 禁止啟動其他執行緒;
  • 設定pid;
  • 決議C/C++層傳進來的引數argv;
  • 設定相關日志追蹤;

2.2、preload()

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
   public static void main(String[] argv) {
        …………
        if (!enableLazyPreload) {
                bootTimingsTraceLog.traceBegin("ZygotePreload");
                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,SystemClock.uptimeMillis());
                preload(bootTimingsTraceLog);    //預加載
                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,SystemClock.uptimeMillis());
                bootTimingsTraceLog.traceEnd();
        }
   }

這里的預加載是指將Java類、資源檔案、影像資源等公共資源在zygote啟動的時候就進行加載,這樣一來,根據fork的copy-on-write機制,其他由zygote fork出來的行程在使用這些資源的時候就不需要再次加載了,而是直接使用,所以這是一種犧牲系統開機時間,來提高系統應用運行時的運行效率的手段,

//pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
    static void preload(TimingsTraceLog bootTimingsTraceLog) {
        Log.d(TAG, "begin preload");
        bootTimingsTraceLog.traceBegin("BeginPreload");
        beginPreload();                          //ZygoteHooks.onBeginPreload();
        bootTimingsTraceLog.traceEnd();          //BeginPreload
        bootTimingsTraceLog.traceBegin("PreloadClasses");
        preloadClasses();                        //預加載一些類
        bootTimingsTraceLog.traceEnd();          //PreloadClasses
        bootTimingsTraceLog.traceBegin("CacheNonBootClasspathClassLoaders");
        //加載一些應用程式使用但不能放入引導類路徑的jar包庫,這些庫過去是引導類路徑的一部分,但必須洗掉,
        //由于向后兼容性的原因,舊的系統應用程式仍然會使用它們,因此它們被快取在這里以保持性能特征
        cacheNonBootClasspathClassLoaders();    
        bootTimingsTraceLog.traceEnd();          //CacheNonBootClasspathClassLoaders
        bootTimingsTraceLog.traceBegin("PreloadResources");
        preloadResources();                      //加載常用資源,以便它們可以跨行程共享,比如apk開發常用到的color、drawable等資源
        bootTimingsTraceLog.traceEnd();          //PreloadResources
        Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadAppProcessHALs");
        nativePreloadAppProcessHALs();           //一些被大多數app行程加載的內容,需要通過HAL來添加(native)
        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
        Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadGraphicsDriver");
        maybePreloadGraphicsDriver();            //根據屬性ro.zygote.disable_gl_preload來判斷是否禁止預加載影像驅動相關內容(native)
        Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
        preloadSharedLibraries();                //加載幾個共享庫:libandroid.so、libcompiler_rt.so、libjnigraphics.so                          
        preloadTextResources();                  //啟動字體快取,設定Typeface              
        WebViewFactory.prepareWebViewInZygote(); //為了記憶體共享,WebViewFactory執行所有必須在zygote行程中運行的初始化
        endPreload();
        warmUpJcaProviders();                    //注冊AndroidKeyStoreProvider并預熱已經注冊的provider
        Log.d(TAG, "end preload");

        sPreloadComplete = true;
    }

其中的preloadClasses()函式是去加載目標設備目錄樹中,/system/etc/preloaded-classes這個檔案中每行一個全限定名格式的類(#開頭的注釋行和空白行則自動跳過),該檔案是由檔案frameworks\base\tools\preload\WritePreloadedClassFile.java自動生成,其對于哪些類需要預加載有明確的說明:

/*
* pie\frameworks\base\tools\preload\WritePreloadedClassFile.java
* The set of classes to preload. We preload a class if:
* a) it's loaded in the bootclasspath (i.e., is a system class)                1、即系統類
* b) it takes > MIN_LOAD_TIME_MICROS = 1250 us to load, and                    2、加載時長超過1250ms的類
* c) it's loaded by more than one process, or it's loaded by anapplication     3、不止一個行程會去加載的類
*/

2.3、gcAndFinalize()

//pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
    public static void main(String[] argv) {
            …………
            bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
            // 呼叫ZygoteHooks.gcAndFinalize(),通過runFinalizationSync()可以在沒有HeapWorker執行緒的Zygote中呼叫finalizers,
            //以運行幾個特殊的gc來嘗試清理幾代軟可及和最終可及的物件,以及任何其他垃圾,
            gcAndFinalize();
            bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC
            bootTimingsTraceLog.traceEnd(); // 對應bootTimingsTraceLog.traceBegin("ZygoteInit")   
            Zygote.initNativeState(isPrimaryZygote);    //初始化zygote的native狀態(native方法)
            ZygoteHooks.stopZygoteNoThreadCreation();   //可以啟動其他執行緒了,對應前面的ZygoteHooks.startZygoteNoThreadCreation();
            …………
    }

gcAndFinalize()方法主要就是在預加載動作之后、后續從zygote fork其他行程的動作之前,進行的一次垃圾回收,這里需要補充看一下上面這個代碼段中的Zygote.initNativeState()方法:

// pie\frameworks\base\core\java\com\android\internal\os\Zygote.java
    static void initNativeState(boolean isPrimary) {
        nativeInitNativeState(isPrimary);
    }

// pie\frameworks\base\core\jni\com_android_internal_os_Zygote.cpp
static void com_android_internal_os_Zygote_nativeInitNativeState(JNIEnv* env, jclass, jboolean is_primary) {
  gZygoteSocketFD = android_get_control_socket(is_primary ? "zygote" : "zygote_secondary");    //獲取socket的句柄fd
  if (gZygoteSocketFD >= 0) {
    ALOGV("Zygote:zygoteSocketFD = %d", gZygoteSocketFD);
  } else {
    ALOGE("Unable to fetch Zygote socket file descriptor");
  }
  gUsapPoolSocketFD = android_get_control_socket(is_primary ? "usap_pool_primary" : "usap_pool_secondary");
  if (gUsapPoolSocketFD >= 0) {
    ALOGV("Zygote:usapPoolSocketFD = %d", gUsapPoolSocketFD);
  } else {
    ALOGE("Unable to fetch USAP pool socket file descriptor");
  }
  //創建套接字,該套接字將被用來發送未經請求的訊息到system_server,該套接字將在派生子行程后被關閉
  initUnsolSocketToSystemServer();    

  gIsSecurityEnforced = security_getenforce();    //根據selinux策略,普通apk是禁止security_getenforce的,
  selinux_android_seapp_context_init();           //所以在zygote fork之前初始化并快取該策略值

  //Zygote行程在fork每個子行程之前首先卸載根存盤空間,因為Zygote行程不使用根存盤空間,所以取消對其下面的掛載名稱空間的共享,
  //每個fork的子行程(包括SystemServer)只掛載它們自己的根存盤空間,在MountEmulatedStorage方法中不需要卸載存盤操作,
  UnmountStorageOnInit(env);

  if (!SetTaskProfiles(0, {})) {    //加載必須的performance profile資訊
    zygote::ZygoteFailure(env, "zygote", nullptr, "Zygote SetTaskProfiles failed");
  }
}

該函式的功能概括來講做了這四件事:

  • 從環境變數中獲取socket句柄fd;
  • 初始化安全屬性;
  • 卸載適當的存盤;
  • 加載必要的性能概要資訊;

還有需要說明一下的是代碼里出現的USAP(Unspecialized App Process),是指在android高版本里提出來的一種zygote fork子行程的機制,通過prefork的方式提前創建好一批行程,當有應用啟動時,直接將已經創建好的行程分配給它,從而省去了fork的動作,從而可以提升性能,詳情參考《Android Framework | 一種新型的應用啟動機制:USAP》,

2.4、 forkSystemServer()

前面的三個小節做好準備作業后,下面就要開始做zygote比較重要的的一個任務了,那就是fork出system_server行程:

//pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
    public static void main(String[] argv) {
            …………
            zygoteServer = new ZygoteServer(isPrimaryZygote);    //創建zygote的ServerSocket
            if (startSystemServer) {
                Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
                if (r != null) {    //{r == null} in the parent process, and {r != null} in the child process
                    r.run();    //通過反射機制執行SystemServer.java的main()函式
                    return;
                }
            }
            …………
    }

這里先是通過ZygoteServer類的建構式,去創建zygote的LocalServerSocket:

//pie\frameworks\base\core\java\com\android\internal\os\ZygoteServer.java
ZygoteServer(boolean isPrimaryZygote) {
        mUsapPoolEventFD = Zygote.getUsapPoolEventFD();

        if (isPrimaryZygote) {
            mZygoteSocket = Zygote.createManagedSocketFromInitSocket(Zygote.PRIMARY_SOCKET_NAME);
            mUsapPoolSocket = Zygote.createManagedSocketFromInitSocket(Zygote.USAP_POOL_PRIMARY_SOCKET_NAME);
        } else {
            mZygoteSocket =Zygote.createManagedSocketFromInitSocket(Zygote.SECONDARY_SOCKET_NAME);
            mUsapPoolSocket = Zygote.createManagedSocketFromInitSocket(Zygote.USAP_POOL_SECONDARY_SOCKET_NAME);
        }
        mUsapPoolSupported = true;
        fetchUsapPoolPolicyProps();    //TODO
    }

// pie\frameworks\base\core\java\com\android\internal\os\Zygote.java
    static LocalServerSocket createManagedSocketFromInitSocket(String socketName) {
        int fileDesc;
        final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;    // "ANDROID_SOCKET_zygote"

        try {
            String env = System.getenv(fullSocketName);
            fileDesc = Integer.parseInt(env);
        } catch (RuntimeException ex) {
            throw new RuntimeException("Socket unset or invalid: " + fullSocketName, ex);
        }

        try {
            FileDescriptor fd = new FileDescriptor();
            fd.setInt$(fileDesc);
            return new LocalServerSocket(fd);
        } catch (IOException ex) {
            throw new RuntimeException(
                "Error building socket from file descriptor: " + fileDesc, ex);
        }
    }

可以看出createManagedSocketFromInitSocket()函式首先是根據socket默認前綴 "ANDROID_SOCKET_"和該socket名稱"zygote",拼接成環境變數的key = "ANDROID_SOCKET_zygote" ,然后以該key值從環境變數中獲取目標socket的檔案描述符fd,(這個環境變數的鍵值在init階段決議init.zygote.rc、讀取并啟動zygote這個service下面的socket(socket zygote stream 660 root system),在目標設備創建/dev/socket/zygote 這個檔案時,就已經以"ANDROID_SOCKET_zygote"為鍵,創建該socket的檔案描述符fd為值,存盤到環境變數中了,)這里獲取到目標socket的檔案描述符后,就用其創建了LocalServerSocket,

以上這些準備好了之后,就要做forkSystemServer這個重要的動作了,鑒于該函式挺長,所以依然分割開來決議,

2.4.1、引數準備

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
private static Runnable forkSystemServer(String abiList, String socketName,
            ZygoteServer zygoteServer) {
        long capabilities = posixCapabilitiesAsBits(
                OsConstants.CAP_IPC_LOCK,
                OsConstants.CAP_KILL,
                OsConstants.CAP_NET_ADMIN,
                OsConstants.CAP_NET_BIND_SERVICE,
                OsConstants.CAP_NET_BROADCAST,
                OsConstants.CAP_NET_RAW,
                OsConstants.CAP_SYS_MODULE,
                OsConstants.CAP_SYS_NICE,
                OsConstants.CAP_SYS_PTRACE,
                OsConstants.CAP_SYS_TIME,
                OsConstants.CAP_SYS_TTY_CONFIG,
                OsConstants.CAP_WAKE_ALARM,
                OsConstants.CAP_BLOCK_SUSPEND
        );
        StructCapUserHeader header = new StructCapUserHeader(OsConstants._LINUX_CAPABILITY_VERSION_3, 0);
        StructCapUserData[] data;
        try {
            data = Os.capget(header);
        } catch (ErrnoException ex) {
            throw new RuntimeException("Failed to capget()", ex);
        }
        capabilities &= ((long) data[0].effective) | (((long) data[1].effective) << 32);

        String[] args = {
                "--setuid=1000",
                "--setgid=1000",
                "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,"
                        + "1024,1032,1065,3001,3002,3003,3006,3007,3009,3010,3011",
                "--capabilities=" + capabilities + "," + capabilities,
                "--nice-name=system_server",
                "--runtime-args",
                "--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT,
                "com.android.server.SystemServer",
        };
        …………
}

這里主要做的作業就是為后面fork出system_server行程準備啟動引數,首先是通過posixCapabilitiesAsBits()函式配置一個long型別的(POSIX capability)能力引數(說人話就是配置system_server行程可以擁有的能力或權限,即能做哪些事),然后和其他命令列引數一起組成啟動system_server所需要傳入的字串陣列型別的引數args,

其中的OsConstants類位于pie\libcore\luni\src\main\java\android\system\OsConstants.java中,這個類把CAP_KILL這些靜態常量都設定為0,原始碼注釋:A hack to avoid these constants being inlined by javac......because we want to initialize them at runtime.大概意思就是說,這樣做是不想在靜態編譯(javac)的時候這些常量被行內進去,而是想借用native方法在運行時去初始化,具體操作就是在該Java類的靜態塊里面呼叫了一個native介面,該native介面在運行時才會通過GetStaticFieldID()和SetStaticIntField()函式去給這些靜態常量設定具體的值,該native方法位于pie\libcore\luni\src\main\native\android_system_OsConstants.cpp中,而這些值的具體定義是在<linux/capability.h>中,而這個檔案位于pie\bionic\libc\kernel\uapi\linux\capability.h,這些原檔案中對每個數值定義都有詳細的說明注釋,有興趣可以看看這些注釋,看明白這些數值的的定義范圍,回傳來就看得懂函式posixCapabilitiesAsBits()通過傳入的不定引數,對system_server行程能力(Capabilities)的配置程序了:

/**
*kitkak\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
* Gets the bit array representation of the provided list of POSIX capabilities.
*/
private static long posixCapabilitiesAsBits(int... capabilities) {
    long result = 0;
    for (int capability : capabilities) {
        if ((capability < 0) || (capability > OsConstants.CAP_LAST_CAP)) {
            throw new IllegalArgumentException(String.valueOf(capability));
        }
        result |= (1L << capability);
    }
    return result;
}

其無非就是通過位操作,將目標long型別整數的二進制形式中,能力(Capabilities)使能所代表的位置為1而已,比如不定引數的第一個常量CAP_KILL的數值為5,則代碼將long型別的1有符號左移五位,其他不同的數值也是類似左移不同的位數,最終通過 |= 操作整合出目標數值,

2.4.2、引數決議與標識置位

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
private static Runnable forkSystemServer(String abiList, String socketName,
            ZygoteServer zygoteServer) {
        …………
        ZygoteArguments parsedArgs;
        int pid;
        try {
            //將args存盤到zygote命令緩沖區中,這個ZygoteCommandBuffer是一個用于Zygote命令的本機可訪問的緩沖區,
            //設計支持重復fork的應用程式,而不干預記憶體分配,從而保持zygote記憶體盡可能穩定
            ZygoteCommandBuffer commandBuffer = new ZygoteCommandBuffer(args);
            try {   //單例模式決議引數
                parsedArgs = ZygoteArguments.getInstance(commandBuffer);
            } catch (EOFException e) {
                throw new AssertionError("Unexpected argument error for forking system server", e);
            }
            commandBuffer.close();    //及時釋放本地資源,避免命令重復呼叫
            Zygote.applyDebuggerSystemProperty(parsedArgs);
            Zygote.applyInvokeWithSystemProperty(parsedArgs); //--invoke-with=?

            if (Zygote.nativeSupportsMemoryTagging()) {
                /* The system server has ASYNC MTE by default, in order to allow system services to specify 
                 * their own MTE level later, as you can't re-enable MTE once it's disabled. */
                String mode = SystemProperties.get("arm64.memtag.process.system_server", "async");
                if (mode.equals("async")) {
                    parsedArgs.mRuntimeFlags |= Zygote.MEMORY_TAG_LEVEL_ASYNC;
                } else if (mode.equals("sync")) {
                    parsedArgs.mRuntimeFlags |= Zygote.MEMORY_TAG_LEVEL_SYNC;
                } else if (!mode.equals("off")) {
                    // When we have an invalid memory tag level, keep the current level.
                    parsedArgs.mRuntimeFlags |= Zygote.nativeCurrentTaggingLevel();
                    Slog.e(TAG, "Unknown memory tag level for the system server: \"" + mode + "\"");
                }
            } else if (Zygote.nativeSupportsTaggedPointers()) {
                //Enable pointer tagging in the system server. Hardware support for this is present in all ARMv8 CPUs
                parsedArgs.mRuntimeFlags |= Zygote.MEMORY_TAG_LEVEL_TBI;
            }
            /* Enable gwp-asan on the system server with a small probability. This is the same
             * policy as applied to native processes and system apps. */
            parsedArgs.mRuntimeFlags |= Zygote.GWP_ASAN_LEVEL_LOTTERY;

            if (shouldProfileSystemServer()) {
                parsedArgs.mRuntimeFlags |= Zygote.PROFILE_SYSTEM_SERVER;
            }
        …………   
}

這里主要是先將2.4.1部分準備的引數args存盤到ZygoteCommandBuffer這個命令緩沖區中,然后通過ZygoteArguments.getInstance()介面,獲取單例模式的ZygoteArguments類的實體,之后ZygoteArguments類的建構式呼叫parseArgs()函式對這些zygote命令引數進行決議,最后根據一些配置對引數決議出來的結果物件parsedArgs的mRuntimeFlags成員進行標識位置位,

獲取單例模式的ZygoteArguments的實體時,在建構式中呼叫parseArgs()去決議引數的程序:

//pie\frameworks\base\core\java\com\android\internal\os\ZygoteArguments.java
    private ZygoteArguments(ZygoteCommandBuffer args, int argCount)
            throws IllegalArgumentException, EOFException {
        parseArgs(args, argCount);
    }

    public static ZygoteArguments getInstance(ZygoteCommandBuffer args)
            throws IllegalArgumentException, EOFException {
        int argCount = args.getCount();
        return argCount == 0 ? null : new ZygoteArguments(args, argCount);
    }

2.4.3、真正的fork動作

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
    private static Runnable forkSystemServer(String abiList, String socketName,
            ZygoteServer zygoteServer) {
        …………
        int pid;
        try {
            …………
            pid = Zygote.forkSystemServer(
                    parsedArgs.mUid, parsedArgs.mGid,
                    parsedArgs.mGids,
                    parsedArgs.mRuntimeFlags,
                    null,
                    parsedArgs.mPermittedCapabilities,
                    parsedArgs.mEffectiveCapabilities);
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException(ex);
        }
        …………
    }

這里直接去找Zygote.forkSystemServer()方法:

//pie\frameworks\base\core\java\com\android\internal\os\Zygote.java
    static int forkSystemServer(int uid, int gid, int[] gids, int runtimeFlags,
            int[][] rlimits, long permittedCapabilities, long effectiveCapabilities) {
        ZygoteHooks.preFork();    //停掉守護執行緒,停掉當前行程的所有的執行緒,zygote每次fork前呼叫

        int pid = nativeForkSystemServer(
                uid, gid, gids, runtimeFlags, rlimits,
                permittedCapabilities, effectiveCapabilities);

        Thread.currentThread().setPriority(Thread.NORM_PRIORITY);    //設定當前執行緒優先級
        
        //每次呼叫preFork()后,都會在子行程上呼叫postForkChild(),
        //并且都會在父行程和子行程上呼叫postForkCommon(),
        //子行程呼叫postForkCommon()在postForkCommon()之后
        ZygoteHooks.postForkCommon();
        return pid;
    }

這里做了fork前的準備后,主要就是通過jni去呼叫了nativeForkSystemServer()函式:

//pie\frameworks\base\core\jni\com_android_internal_os_Zygote.cpp
static jint com_android_internal_os_Zygote_nativeForkSystemServer(
        JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
        jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities,
        jlong effective_capabilities) {
  //初始化USAP相關的一個vector
  std::vector<int> fds_to_close(MakeUsapPipeReadFDVector()), fds_to_ignore(fds_to_close);
  fds_to_close.push_back(gUsapPoolSocketFD);

  if (gUsapPoolEventFD != -1) {
    fds_to_close.push_back(gUsapPoolEventFD);
    fds_to_ignore.push_back(gUsapPoolEventFD);
  }

  if (gSystemServerSocketFd != -1) {
      fds_to_close.push_back(gSystemServerSocketFd);
      fds_to_ignore.push_back(gSystemServerSocketFd);
  }

  pid_t pid = zygote::ForkCommon(env, true, fds_to_close, fds_to_ignore, true);    //fork動作
  if (pid == 0) {
      /在子行程中進行一些system_server相關配置,有興趣的話去詳細看看
      SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits,
                       permitted_capabilities, effective_capabilities,
                       MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true, false, 
                       nullptr, nullptr, false, nullptr, false, false);
  } else if (pid > 0) {
      ALOGI("System server process %d has been created", pid);
      gSystemServerPid = pid;
      int status;
      //在父行程zygote中使用waitpid()函式以及WNOHANG這個選項,監控子行程的結束情況,
      //監控到system_server行程結束時,需要重啟zygote,
      //waitpid()函式參考https://www.cnblogs.com/zhaihongliangblogger/p/6367041.html
      if (waitpid(pid, &status, WNOHANG) == pid) {
          ALOGE("System server process %d has died. Restarting Zygote!", pid);
          RuntimeAbort(env, __LINE__, "System server process has died. Restarting Zygote!");
      }

      if (UsePerAppMemcg()) {    //檢測是否掛載了memcg
          if (!SetTaskProfiles(pid, std::vector<std::string>{"SystemMemoryProcess"})) {
              ALOGE("couldn't add process %d into system memcg group", pid);
          }
      }
  }
  return pid;
}

這里重點就是通過ForkCommon()函式fork子行程,然后通過SpecializeCommon()函式對子行程做一些配置,這里主要來看一下ForkCommon()函式的fork程序:

// pie\frameworks\base\jni\com_android_internal_os_Zygote.cpp
pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server,
                         const std::vector<int>& fds_to_close,
                         const std::vector<int>& fds_to_ignore,
                         bool is_priority_fork,
                         bool purge) {
  SetSignalHandlers();    //為zygote管理子行程配置信號SIGCHLD/SIGHUP
  //指定ZygoteFailure函式,其用來向runtime報告致命錯誤
  auto fail_fn = std::bind(zygote::ZygoteFailure, env, is_system_server ? "system_server" : "zygote", nullptr, _1);

  //在fork期間臨時阻塞SIGCHLD,SIGCHLD處理程式可能會記錄日志,
  BlockSignal(SIGCHLD, fail_fn); //這將導致我們關閉的日志fd被重新打開,會導致失敗,因為不允許列出fd
  __android_log_close();         //在開始計算檔案描述符串列之前,關閉所有與日志記錄相關的fd
  AStatsSocket_close();

  if (gOpenFdTable == nullptr) { //如果這是zygote的第一次fork,則創建一個打開的FD表,驗證檔案受支持的型別和允許串列
    gOpenFdTable = FileDescriptorTable::Create(fds_to_ignore, fail_fn);
  } else {    //如果不是,則檢查打開的檔案是否沒有更改,不希望并且未來會禁止打開新的檔案,
    gOpenFdTable->Restat(fds_to_ignore, fail_fn);    //目前的做法是,如果通過了上面的Create檢測,則允許打開新的檔案
  }
  android_fdsan_error_level fdsan_error_level = android_fdsan_get_error_level();

  if (purge) {           //清除未使用的本機記憶體,以減少與子行程的錯誤共享,通過減少與子行程共享的libc_malloc區域的大小,  
    mallopt(M_PURGE, 0); //當malloc在fork之后調整它所管理的每個頁面上的元資料時,可以減少轉換到私有臟狀態的頁面數量
  }

  pid_t pid = fork();    //核心的fork動作
  if (pid == 0) {        //子行程
    if (is_priority_fork) {
      setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_MAX);
    } else {
      setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_MIN);
    }

    PreApplicationInit();
    DetachDescriptors(env, fds_to_close, fail_fn);    //通過dup3()函式清除那些需要立即關閉的檔案描述符
    ClearUsapTable();                                 //清除USAP行程表
    gOpenFdTable->ReopenOrDetach(fail_fn);            //重新打開所有剩余的打開的檔案描述符,這樣它們就不會通過fork與zygote共享
    android_fdsan_set_error_level(fdsan_error_level);
    gSystemServerSocketFd = -1;
  } else {               //父行程
    ALOGD("Forked child process %d", pid);
  }

  UnblockSignal(SIGCHLD, fail_fn);                    //fork結束后打開阻塞,對應上面的BlockSignal()
  return pid;
}

2.4.4、fork后的掃尾作業

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
    private static Runnable forkSystemServer(String abiList, String socketName,
            ZygoteServer zygoteServer) {
        …………
        //由于fork()函式完成任務后回傳的位置不確定,如果在子行程中,fork函式回傳0;
        //如果在父行程中,fork回傳新創建子行程的行程ID
        if (pid == 0) {    
            if (hasSecondZygote(abiList)) {            //通過比較設備ABI串列和受精卵串列來確定這一點,
                waitForSecondaryZygote(socketName);    //如果這個受精卵支持該設備支持的所有abi,就不會有另一個受精卵
            }

            zygoteServer.closeServerSocket();    //fork出來的子行程中不會用到zygoteServer這個socket,所以要關掉
            return handleSystemServerProcess(parsedArgs);    //做一些fork system_server的掃尾作業
        }

        return null;
    }

在上一小節的native fork動作結束回傳后,這里需要通過handleSystemServerProcess()方法來做一些system_server行程的收尾動作:

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) {
        Os.umask(S_IRWXG | S_IRWXO);    //行程權限設定為0077,這樣新檔案和目錄將默認為所有者權限
        if (parsedArgs.mNiceName != null) {    //引數準備部分的"--nice-name=system_server"
            Process.setArgV0(parsedArgs.mNiceName);    //行程名設定為system_server
        }
        //環境變數SYSTEMSERVERCLASSPATH=/system/framework/services.jar:
        //                        /system/framework/ethernet-service.jar:
        //                        /system/framework/wifi-service.jar:…………
        final String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH");
        if (systemServerClasspath != null) {
            if (shouldProfileSystemServer() && (Build.IS_USERDEBUG || Build.IS_ENG)) {
                try {
                    Log.d(TAG, "Preparing system server profile");
                    prepareSystemServerProfile(systemServerClasspath);    //debug或eng模式下準備system_server的profile檔案
                } catch (Exception e) {
                    Log.wtf(TAG, "Failed to set up system server profile", e);
                }
            }
        }

        if (parsedArgs.mInvokeWith != null) {    //TODO
            String[] args = parsedArgs.mRemainingArgs;
            if (systemServerClasspath != null) {
                String[] amendedArgs = new String[args.length + 2];
                amendedArgs[0] = "-cp";
                amendedArgs[1] = systemServerClasspath;
                System.arraycopy(args, 0, amendedArgs, 2, args.length);
                args = amendedArgs;
            }

            WrapperInit.execApplication(parsedArgs.mInvokeWith, parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
                                        VMRuntime.getCurrentInstructionSet(), null, args);
            throw new IllegalStateException("Unexpected return from WrapperInit.execApplication");
        } else {    //一般system_server走這個分支
            ClassLoader cl = getOrCreateSystemServerClassLoader(); //為system_server創建類加載器
            if (cl != null) {
                Thread.currentThread().setContextClassLoader(cl);  //為當前執行緒設定背景關系類加載器
            }
            return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion, parsedArgs.mDisabledCompatChanges,
                                         parsedArgs.mRemainingArgs, cl);
        }
    }

繼續追蹤ZygoteInit.zygoteInit() 方法:

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
    public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
        if (RuntimeInit.DEBUG) {
            Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        RuntimeInit.redirectLogStreams();    //將java的system.out和system.err log輸出流重定向到AndroidPrintStream
        RuntimeInit.commonInit(); //設定log配置、通過persist.sys.timezone的屬性值設定時區、設定默認的HTTP User-agent格式到http.agent屬性    
        //通過JNI呼叫AndroidRuntime的成員gCurRuntime(即app_process.cpp中)的onZygoteInit()函式,
        ZygoteInit.nativeZygoteInit();     //主要完成創建binder的作業
        return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv, classLoader);
    }

追蹤RuntimeInit.applicationInit()方法:

// pie\frameworks\base\core\java\com\android\internal\os\RuntimeInit.java
    protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges, String[] argv, ClassLoader classLoader) {
        …………
        //從前面準備的引數中決議出"com.android.server.SystemServer"賦值給args.startClass
        final Arguments args = new Arguments(argv);
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);    //對應上面ZygoteInit的traceBegin()
        //有了類的全限定名,通過反射機制找到SystemServer.java的main()方法
        return findStaticMain(args.startClass, args.startArgs, classLoader);
    }


    protected static Runnable findStaticMain(String className, String[] argv, ClassLoader classLoader) {
        …………    //省略通過反射機制尋找main方法的程序
        return new MethodAndArgsCaller(m, argv);
    }

    //將main方法封裝到Runnable實體中回傳,最侄訓傳給2.4節一開始的Runnable r,然后通過r.run()執行該main方法
    static class MethodAndArgsCaller implements Runnable {
        private final Method mMethod;
        private final String[] mArgs;

        public MethodAndArgsCaller(Method method, String[] args) {
            mMethod = method;
            mArgs = args;
        }

        public void run() {
            try {
                mMethod.invoke(null, new Object[] { mArgs });
            } catch (IllegalAccessException ex) {
              …………
            }
        }
    }

總結一下2.4.4小節主要做了:

  1. 對fork出來的新行程的權限設定;
  2. 行程命名為system_server;
  3. 創建類加載器加載system_server的java類;
  4. 通過反射機制找到SystemServer.java的main函式,并封裝到Runnable r;
  5. 方法return到2.4節一開始,通過r.run()執行該main方法,以啟動SystsmServer;

2.5、runSelectLoop()

zygote除了fork出system_server行程這個任務外,還有一個重要的任務,那就是接收AMS(ActivityManagerService)發來創建java層應用程式的請求,fork出一個個行程,并在新行程中執行該請求中相關應用程式的main方法,上層的一個個應用程式就是這樣通過zygote創建而來,因為應用程式都是由zygote孕育而來,所以就不難理解zygote(受精卵)的名稱的由來了,我們繼續往下看看它是如何完成這一任務的:

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java
    public static void main(String[] argv) {
        …………
        Runnable caller;
        try {
            …………
            Log.i(TAG, "Accepting command socket connections");
            caller = zygoteServer.runSelectLoop(abiList);
        } catch (Throwable ex) {
            Log.e(TAG, "System zygote died with fatal exception", ex);
            throw ex;
        } finally {
            if (zygoteServer != null) {
                zygoteServer.closeServerSocket();    //zygote掛掉的時候關閉ServerSocket
            }
        }

        if (caller != null) {
            caller.run();
        }
    }

runSelectLoop()方法冗長,有太多USAP相關的操作,我們這里省略相關內容,只看核心操作:

// pie\frameworks\base\core\java\com\android\internal\os\ZygoteServer.java
Runnable runSelectLoop(String abiList) {
        ArrayList<FileDescriptor> socketFDs = new ArrayList<>();
        ArrayList<ZygoteConnection> peers = new ArrayList<>();
        socketFDs.add(mZygoteSocket.getFileDescriptor());    //先將server socket加入到這個socketFDs串列
        peers.add(null);

        while (true) {    //間隔時間持續輪詢
            …………
            int[] usapPipeFDs = null;
            StructPollfd[] pollFDs;
            pollFDs = new StructPollfd[socketFDs.size()];    //每輪回圈,都重新創建需要監聽的pollFDs
            int pollIndex = 0;
            for (FileDescriptor socketFD : socketFDs) {
                pollFDs[pollIndex] = new StructPollfd();
                pollFDs[pollIndex].fd = socketFD;
                pollFDs[pollIndex].events = (short) POLLIN;    //關注poll事件到來
                ++pollIndex;
            }
            …………
            int pollTimeoutMs;//設定poll輪詢時延間隔 
            int pollReturnValue;
            try {
                pollReturnValue = Os.poll(pollFDs, pollTimeoutMs);
            …………

            if (pollReturnValue == 0) {    …………
            } else {
                while (--pollIndex >= 0) {
                    if ((pollFDs[pollIndex].revents & POLLIN) == 0) {
                        continue;
                    }

                    if (pollIndex == 0) {    //server socket最先加入fds, 因此這里是server socket收到資料
                        ZygoteConnection newPeer = acceptCommandPeer(abiList);    //收到新的建立通信的請求,建立通信連接
                        peers.add(newPeer);  //加入到peers和fds, 即下一次也開始監聽
                        socketFDs.add(newPeer.getFileDescriptor());
                    } else if (pollIndex < usapPoolEventFDIndex) {    //說明接收到AMS通過socket發送過來創建應用程式的請求
                        try {
                            //有socket連接時創建ZygoteConnection物件,并添加到pollFDs
                            ZygoteConnection connection = peers.get(pollIndex);
                            boolean multipleForksOK = !isUsapPoolEnabled()
                                    && ZygoteHooks.isIndefiniteThreadSuspensionSafe();
                            final Runnable command =
                                    connection.processCommand(this, multipleForksOK);    //完成創建子行程的請求
                            if (mIsForkChild) {
                                if (command == null) {
                                    throw new IllegalStateException("command == null");
                                }
                                return command;    //依然是提供Runnable介面,通過反射機制執行目標應用程式的main方法
                            } else {
                                if (command != null) {
                                    throw new IllegalStateException("command != null");
                                }
                                //處理完后,關閉socket連接,并從peers和socketFDs串列中移除
                                if (connection.isClosedByPeer()) {    
                                    connection.closeSocket();
                                    peers.remove(pollIndex);
                                    socketFDs.remove(pollIndex);
                                }
                            }
                        } catch (Exception e) {
                            …………
    }

這里大概意思就是,回圈間隔一段時間輪詢連接socket訊息,看是否有AMS客戶端發過來創建應用程式的請求,有的話則通過pie\frameworks\base\core\java\com\android\internal\os\ZygoteConnection.processCommand()方法創建行程并啟動目標應用程式,我們只需要直到這里是接收AMS創建應用程式的請求,完成目標應用程式創建與啟動的就好,詳細細節我后面學習到AMS之后,單獨寫一篇文章來追蹤AMS與zygote之間通過socket通信,創建應用程式的程序吧,

3、結語

總結來說,zygote在開機程序中,其先是啟動虛擬機、注冊JNI函式、進入java世界,然后fork出system_server行程,之后作為守護行程監聽并處理創建普通應用程式的作業,呼叫流程圖原圖請見:| ProcessOn

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/339187.html

標籤:其他

上一篇:漂泊不定的人生,終究在層層浪花中,站穩了跟腳,姐成功了

下一篇:軟體測驗 | 手把手教你如何使用 ABD除錯工具,學不會算我的!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more