<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>orangeboy的小窝</title><description>No description</description><link>https://blog.nowcent.cn/</link><language>zh_CN</language><item><title>Android启动2 - Zygote进程的创建</title><link>https://blog.nowcent.cn/posts/android-boot-zygote-process/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/android-boot-zygote-process/</guid><pubDate>Thu, 12 Feb 2026 14:59:00 GMT</pubDate><content:encoded>&lt;h2&gt;Zygote的创建&lt;/h2&gt;
&lt;p&gt;首先，Android的每一个APP，都对应着至少一个进程。而Zygote，就是所有进程的祖先。为什么这么说，因为APP内的所有进程，&lt;strong&gt;都是Zygote fork的&lt;/strong&gt;。（Zygote这个名字，真的取得太好了！）&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://source.android.com/docs/core/runtime/zygote?hl=en&quot;}&lt;/p&gt;
&lt;p&gt;但是，这个祖先，是怎么创建的？这个很简单，Android代码上就写死了，系统启动后直接执行&lt;code&gt;/system/bin/init&lt;/code&gt;进程（PID=1）。要了解&lt;code&gt;init&lt;/code&gt;进程在做什么，那就要先了解一下&lt;code&gt;init.rc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://android.googlesource.com/platform/system/core/+/master/init/README.md&quot;}&lt;/p&gt;
&lt;p&gt;&lt;code&gt;init.rc&lt;/code&gt;其实是一个配置脚本，告诉&lt;code&gt;init&lt;/code&gt;进程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动哪些系统进程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动时机&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置系统参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;响应系统事件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;init&lt;/code&gt;进程的入口在&lt;code&gt;main.cpp&lt;/code&gt;里：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/main.cpp&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.cpp
// ...
using namespace android::init;

int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
    __hwasan_set_error_report_callback(AsanReportCallback);
#endif
    // Boost prio which will be restored later
    setpriority(PRIO_PROCESS, 0, -20);
    if (!strcmp(basename(argv[0]), &quot;ueventd&quot;)) {
        return ueventd_main(argc, argv);
    }

    if (argc &amp;gt; 1) {
        if (!strcmp(argv[1], &quot;subcontext&quot;)) {
            android::base::InitLogging(argv, &amp;amp;android::base::KernelLogger);
            const BuiltinFunctionMap&amp;amp; function_map = GetBuiltinFunctionMap();

            return SubcontextMain(argc, argv, &amp;amp;function_map);
        }

        if (!strcmp(argv[1], &quot;selinux_setup&quot;)) {
            return SetupSelinux(argv);
        }

        if (!strcmp(argv[1], &quot;second_stage&quot;)) {
            return SecondStageMain(argc, argv);
        }
    }

    return FirstStageMain(argc, argv);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恭喜你，发现了Android大名鼎鼎的&lt;strong&gt;多阶段启动&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;多阶段启动&lt;/h3&gt;
&lt;p&gt;我们先假设一种情况。假设&lt;code&gt;argc&lt;/code&gt;为空，那么就会直接走到&lt;code&gt;FirstStageMain&lt;/code&gt;。这个函数代码在&lt;code&gt;first_state_init.cpp&lt;/code&gt;里，我们接着看看具体实现：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/first_stage_init.cpp&quot;}&lt;/p&gt;
&lt;p&gt;代码我暂时不贴了，其实主要做了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mount /proc /sys&lt;/li&gt;
&lt;li&gt;初始化 device node&lt;/li&gt;
&lt;li&gt;加载 sepolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是，最后一部分的代码很有意思：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.cpp
int FirstStageMain(int argc, char** argv) {
  // ...
  const char* path = &quot;/system/bin/init&quot;;
    const char* args[] = {path, &quot;selinux_setup&quot;, nullptr};
    auto fd = open(&quot;/dev/kmsg&quot;, O_WRONLY | O_CLOEXEC);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    close(fd);
    execv(path, const_cast&amp;lt;char**&amp;gt;(args));

    // execv() only returns if an error happened, in which case we
    // panic and never fall through this conditional.
    PLOG(FATAL) &amp;lt;&amp;lt; &quot;execv(\&quot;&quot; &amp;lt;&amp;lt; path &amp;lt;&amp;lt; &quot;\&quot;) failed&quot;;

    return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;唉，当前不就是在&lt;code&gt;init&lt;/code&gt;进程吗？为什么又会启动一次&lt;code&gt;init&lt;/code&gt;进程呢？细心看发现，这里传了一个参数，&lt;code&gt;selinux_setup&lt;/code&gt;。而我们再看看&lt;code&gt;main.cpp&lt;/code&gt;，就会发现这时就不会执行&lt;code&gt;FirstStageMain&lt;/code&gt;了，而是&lt;code&gt;SetupSelinux&lt;/code&gt;。说明这个&lt;code&gt;main.cpp&lt;/code&gt;，其实是一个&lt;strong&gt;有限状态机&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我们梳理一下&lt;code&gt;main.cpp&lt;/code&gt;的流程：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/selinux.cpp&quot;}&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/init.cpp&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FirstStageMain&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mount /proc /sys&lt;/li&gt;
&lt;li&gt;初始化 device node&lt;/li&gt;
&lt;li&gt;加载 sepolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SetupSelinux&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;apply selinux policy&lt;/li&gt;
&lt;li&gt;sepolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SecondStageMain&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;property service&lt;/li&gt;
&lt;li&gt;解析&lt;code&gt;init.rc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启动zygote&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;接下来，到了解析&lt;code&gt;init.rc&lt;/code&gt;并执行的部分。我们来看个&lt;code&gt;init.rc&lt;/code&gt;的例子：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:system/core/rootdir/init.rc;l=192?q=init.rc&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ...
import /system/etc/init/hw/init.${ro.zygote}.rc

# Cgroups are mounted right before early-init using list from /etc/cgroups.json
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的&lt;code&gt;import /system/etc/init/hw/init.${ro.zygote}.rc&lt;/code&gt;，&lt;code&gt;ro.zygote&lt;/code&gt;是什么呢？其实，这是Android编译时的参数，在构建时期指定。我们也可以通过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb shell getprop ro.zygote
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到这个参数具体值。&lt;/p&gt;
&lt;p&gt;我们以&lt;code&gt;ro.zygote=zygote64&lt;/code&gt;为例，对应的rc文件就是&lt;code&gt;system/core/rootdir/init.zygote64.rc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:system/core/rootdir/init.zygote64.rc&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    priority -20
    user root
    group root readproc reserved_disk
    socket zygote stream 660 root system
    socket usap_pool_primary stream 660 root system
    onrestart exec_background - system system -- /system/bin/vdc volume abort_fuse
    onrestart write /sys/power/state on
    # NOTE: If the wakelock name here is changed, then also
    # update it in SystemSuspend.cpp
    onrestart write /sys/power/wake_lock zygote_kwl
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart --only-if-running media.tuner
    onrestart restart netd
    onrestart restart wificond
    task_profiles ProcessCapacityHigh MaxPerformance
    critical window=${zygote.critical_window.minute:-off} target=zygote-fatal
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;service&lt;/code&gt; 启动一个服务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;zygote&lt;/code&gt; 服务名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/system/bin/app_process64&lt;/code&gt; 映像&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-Xzygote&lt;/code&gt; 指定这是一个zygote进程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/system/bin&lt;/code&gt; Java classpath&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--zygote&lt;/code&gt; 传给&lt;code&gt;app_process&lt;/code&gt;的，最终进入&lt;code&gt;ZygoteInit.main()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--start-system-server&lt;/code&gt; 传给&lt;code&gt;app_process&lt;/code&gt;的，说明要启动SystemServer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--socket-name=zygote&lt;/code&gt;指定socket名，对应下面的&lt;code&gt;socket zygote&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class main
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优先阶段启动&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;priority -20
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优先级最高&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;user root
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;指定zygote为root。不然zygote不能fork。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;socket zygote stream 660 root system
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;定义zygote socket。zygote用它来fork进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;onrestart write /sys/power/wake_lock zygote_kwl
onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart --only-if-running media.tuner
onrestart restart netd
onrestart restart wificond
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;zygote crash后进行的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;task_profiles ProcessCapacityHigh MaxPerformance
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;资源限制&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;critical window=${zygote.critical_window.minute:-off} target=zygote-fatal
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;设定zygote服务关键窗口时间。如果该时间内服务没启动成功，就视为致命错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，由上面也可以看出来，像&lt;code&gt;audioserver&lt;/code&gt; &lt;code&gt;cameraserver&lt;/code&gt; 这类进程，都是&lt;code&gt;zygote&lt;/code&gt;的子进程。&lt;/p&gt;
&lt;h3&gt;启动app_process进程&lt;/h3&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/cmds/app_process/app_main.cpp&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// frameworks/base/cmds/app_process/app_main.cpp
// ...
int main(int argc, char* const argv[])
{
    // ...

    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
   	// ...

    // Everything up to &apos;--&apos; or first non &apos;-&apos; arg goes to the vm.
    //
    // The first argument after the VM args is the &quot;parent dir&quot;, which
    // is currently unused.
    //
    // After the parent dir, we expect one or more the following internal
    // arguments :
    //
    // --zygote : Start in zygote mode
    // --start-system-server : Start the system server.
    // --application : Start in application (stand alone, non zygote) mode.
    // --nice-name : The nice name for this process.
    //
    // For non zygote starts, these arguments will be followed by
    // the main class name. All remaining arguments are passed to
    // the main method of this class.
    //
    // For zygote starts, all remaining arguments are passed to the zygote.
    // main function.
    //
    // Note that we must copy argument string values since we will rewrite the
    // entire argument block when we apply the nice name to argv0.
    //
    // As an exception to the above rule, anything in &quot;spaced commands&quot;
    // goes to the vm even though it has a space in it.
   
 		// ...
    // Parse runtime arguments.  Stop at first unrecognized option.
    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;

    ++i;  // Skip unused &quot;parent dir&quot; argument.
    while (i &amp;lt; argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, &quot;--zygote&quot;) == 0) {
            zygote = true;
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, &quot;--start-system-server&quot;) == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, &quot;--application&quot;) == 0) {
            application = true;
        } else if (strncmp(arg, &quot;--nice-name=&quot;, 12) == 0) {
            niceName = (arg + 12);
        } else if (strncmp(arg, &quot;--&quot;, 2) != 0) {
            className = arg;
            break;
        } else {
            --i;
            break;
        }
    }

    Vector&amp;lt;String8&amp;gt; args;
    if (!className.empty()) {
       // ...
    } else {
        // We&apos;re in zygote mode.
        maybeCreateDalvikCache();

        if (startSystemServer) {
            args.add(String8(&quot;start-system-server&quot;));
        }

        // ...
        // In zygote mode, pass all remaining arguments to the zygote
        // main() method.
        for (; i &amp;lt; argc; ++i) {
            args.add(String8(argv[i]));
        }
    }

    if (!niceName.empty()) {
        runtime.setArgv0(niceName.c_str(), true /* setProcName */);
    }

    if (zygote) {
        runtime.start(&quot;com.android.internal.os.ZygoteInit&quot;, args, zygote);
    } else if (!className.empty()) {
        runtime.start(&quot;com.android.internal.os.RuntimeInit&quot;, args, zygote);
    } else {
        fprintf(stderr, &quot;Error: no class name or --zygote supplied.\n&quot;);
        app_usage();
        LOG_ALWAYS_FATAL(&quot;app_process: no class name or --zygote supplied.&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，这里大部分都在做&lt;strong&gt;参数解析&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果参数带&lt;code&gt;--zygote&lt;/code&gt;，就用&lt;code&gt;ZygoteInit&lt;/code&gt;启动，否则用&lt;code&gt;RuntimeInit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;该进程剩下的启动参数，透传给&lt;code&gt;runtime.start(xxx)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;盯着后面的&lt;code&gt;runtime.start(xxx)&lt;/code&gt;，我们看看&lt;code&gt;runtime.cpp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/jni/AndroidRuntime.cpp&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// frameworks/base/core/jni/AndroidRuntime.cpp
/*
 * Start the Android runtime.  This involves starting the virtual machine
 * and calling the &quot;static void main(String[] args)&quot; method in the class
 * named by &quot;className&quot;.
 *
 * Passes the main function two arguments, the class name and the specified
 * options string.
 */
void AndroidRuntime::start(const char* className, const Vector&amp;lt;String8&amp;gt;&amp;amp; options, bool zygote)
{
    ALOGD(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; START %s uid %d &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;\n&quot;,
            className != NULL ? className : &quot;(unknown)&quot;, getuid());

    static const String8 startSystemServer(&quot;start-system-server&quot;);
    // Whether this is the primary zygote, meaning the zygote which will fork system server.
    bool primary_zygote = false;

    /*
     * &apos;startSystemServer == true&apos; means runtime is obsolete and not run from
     * init.rc anymore, so we print out the boot start event here.
     */
    for (size_t i = 0; i &amp;lt; options.size(); ++i) {
        if (options[i] == startSystemServer) {
            primary_zygote = true;
           /* track our progress through the boot sequence */
           const int LOG_BOOT_PROGRESS_START = 3000;
           LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START,  ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));
        }
    }

    // ...

    //const char* kernelHack = getenv(&quot;LD_ASSUME_KERNEL&quot;);
    //ALOGD(&quot;Found LD_ASSUME_KERNEL=&apos;%s&apos;\n&quot;, kernelHack);

    /* start the virtual machine */
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&amp;amp;mJavaVM, &amp;amp;env, zygote, primary_zygote) != 0) {
        return;
    }
    onVmCreated(env);

    /*
     * Register android functions.
     */
    if (startReg(env) &amp;lt; 0) {
        ALOGE(&quot;Unable to register all android natives\n&quot;);
        return;
    }

    /*
     * We want to call main() with a String array with arguments in it.
     * At present we have two arguments, the class name and an option string.
     * Create an array to hold them.
     */
    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;

    stringClass = env-&amp;gt;FindClass(&quot;java/lang/String&quot;);
    assert(stringClass != NULL);
    strArray = env-&amp;gt;NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env-&amp;gt;NewStringUTF(className);
    assert(classNameStr != NULL);
    env-&amp;gt;SetObjectArrayElement(strArray, 0, classNameStr);

    for (size_t i = 0; i &amp;lt; options.size(); ++i) {
        jstring optionsStr = env-&amp;gt;NewStringUTF(options.itemAt(i).c_str());
        assert(optionsStr != NULL);
        env-&amp;gt;SetObjectArrayElement(strArray, i + 1, optionsStr);
    }

    /*
     * Start VM.  This thread becomes the main thread of the VM, and will
     * not return until the VM exits.
     */
    char* slashClassName = toSlashClassName(className != NULL ? className : &quot;&quot;);
    jclass startClass = env-&amp;gt;FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE(&quot;JavaVM unable to locate class &apos;%s&apos;\n&quot;, slashClassName);
        /* keep going */
    } else {
        jmethodID startMeth = env-&amp;gt;GetStaticMethodID(startClass, &quot;main&quot;,
            &quot;([Ljava/lang/String;)V&quot;);
        if (startMeth == NULL) {
            ALOGE(&quot;JavaVM unable to find main() in &apos;%s&apos;\n&quot;, className);
            /* keep going */
        } else {
            env-&amp;gt;CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
            if (env-&amp;gt;ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }
    free(slashClassName);

    ALOGD(&quot;Shutting down VM\n&quot;);
    if (mJavaVM-&amp;gt;DetachCurrentThread() != JNI_OK)
        ALOGW(&quot;Warning: unable to detach main thread\n&quot;);
    if (mJavaVM-&amp;gt;DestroyJavaVM() != 0)
        ALOGW(&quot;Warning: VM did not shut down cleanly\n&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化&lt;code&gt;libart.so&lt;/code&gt; （&lt;code&gt;jni_invocation.Init(NULL);&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;初始化ART虚拟机（&lt;code&gt;startVm(&amp;amp;mJavaVM, &amp;amp;env, zygote, primary_zygote)&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;通过反射找到传入classpath的&lt;code&gt;main&lt;/code&gt;函数并执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而&lt;code&gt;JniInvocation&lt;/code&gt; 到底做了什么呢？&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:libnativehelper/include_platform/nativehelper/JniInvocation.h&quot;}&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/main/+/main:libnativehelper/JniInvocation.c&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// libnativehelper/JniInvocation.c

// ...

// Name the default library providing the JNI Invocation API.
static const char* kDefaultJniInvocationLibrary = &quot;libart.so&quot;;
static const char* kDebugJniInvocationLibrary = &quot;libartd.so&quot;;

// ...
bool JniInvocationInit(struct JniInvocationImpl* instance, const char* library_name) {
#ifdef __ANDROID__
  char buffer[PROP_VALUE_MAX];
#else
  char* buffer = NULL;
#endif
  library_name = JniInvocationGetLibrary(library_name, buffer);
  DlLibrary library = DlOpenLibrary(library_name);
  if (library == NULL) {
    if (strcmp(library_name, kDefaultJniInvocationLibrary) == 0) {
      // Nothing else to try.
      ALOGE(&quot;Failed to dlopen %s: %s&quot;, library_name, DlGetError());
      return false;
    }
    // Note that this is enough to get something like the zygote
    // running, we can&apos;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(&quot;Falling back from %s to %s after dlopen error: %s&quot;,
          library_name, kDefaultJniInvocationLibrary, DlGetError());
    library_name = kDefaultJniInvocationLibrary;
    library = DlOpenLibrary(library_name);
    if (library == NULL) {
      ALOGE(&quot;Failed to dlopen %s: %s&quot;, library_name, DlGetError());
      return false;
    }
  }

  DlSymbol JNI_GetDefaultJavaVMInitArgs_ = FindSymbol(library, &quot;JNI_GetDefaultJavaVMInitArgs&quot;);
  if (JNI_GetDefaultJavaVMInitArgs_ == NULL) {
    return false;
  }

  DlSymbol JNI_CreateJavaVM_ = FindSymbol(library, &quot;JNI_CreateJavaVM&quot;);
  if (JNI_CreateJavaVM_ == NULL) {
    return false;
  }

  DlSymbol JNI_GetCreatedJavaVMs_ = FindSymbol(library, &quot;JNI_GetCreatedJavaVMs&quot;);
  if (JNI_GetCreatedJavaVMs_ == NULL) {
    return false;
  }

  instance-&amp;gt;jni_provider_library_name = library_name;
  instance-&amp;gt;jni_provider_library = library;
  instance-&amp;gt;JNI_GetDefaultJavaVMInitArgs = (jint (*)(void *)) JNI_GetDefaultJavaVMInitArgs_;
  instance-&amp;gt;JNI_CreateJavaVM = (jint (*)(JavaVM**, JNIEnv**, void*)) JNI_CreateJavaVM_;
  instance-&amp;gt;JNI_GetCreatedJavaVMs = (jint (*)(JavaVM**, jsize, jsize*)) JNI_GetCreatedJavaVMs_;

  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;dlopen&lt;/code&gt;载入&lt;code&gt;libart.so&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  A[&quot;Init 启动&quot;] --&amp;gt; B[&quot;解析 init.rc / 启动 Zygote 服务&quot;]
  B --&amp;gt; C[&quot;启动 app_process (Zygote 进程)&quot;]
  C --&amp;gt; D[&quot;初始化运行时 (ART)&quot;]
  D --&amp;gt; E[&quot;进入 com.android.internal.os.ZygoteInit.main&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本章描述了Android系统从Init进程启动到&lt;code&gt;Zygote&lt;/code&gt;进程的创建。接下来的几张，我们会重点来讲，&lt;code&gt;Zygote&lt;/code&gt;进程到底做了什么。&lt;/p&gt;
</content:encoded></item><item><title>Android启动1 - Launcher简介</title><link>https://blog.nowcent.cn/posts/android-boot-launcher-introduction/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/android-boot-launcher-introduction/</guid><pubDate>Thu, 12 Feb 2026 14:58:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note
Android的启动流程太繁琐了，如果从头讲起，那肯定是讲不明白的😭。&lt;/p&gt;
&lt;p&gt;我这里先从最直观的入口开始入手，先定个小目标，从Zygote进程初始化讲起，到Launcher Activity首帧结束。从Zygote进程初始化讲起很好理解，因为Zygote之后几乎就是Java的世界；而到Launcher Activity结束，是因为用户看到的第一个东西就是Launcher。&lt;/p&gt;
&lt;p&gt;总的顺序是：&lt;/p&gt;
&lt;p&gt;Zygote → system_server → AMS/ATMS → Launcher 进程 → Launcher Activity 首帧。
暂时不讲 Bootloader / init / Kernel。
:::&lt;/p&gt;
&lt;h2&gt;Launcher是什么&lt;/h2&gt;
&lt;p&gt;这个问题，如果放在十年前，应该所有用过Android的人都知道。不过，放在6202年，知道的人已经越来越少了。&lt;/p&gt;
&lt;p&gt;Launcher是&lt;strong&gt;桌面APP&lt;/strong&gt;。在Android系统启动后，启动的第一个&lt;strong&gt;APP&lt;/strong&gt;就是Launcher。Launcher对应iOS的组件是SpringBoard。这里也提到，Launcher是&lt;strong&gt;APP&lt;/strong&gt;，也就意味着他的生命周期与普通的APP一致。它也有自己的Application和Activity。&lt;/p&gt;
&lt;p&gt;不知道大家是否还记得，Android是支持多桌面的！这是Android开放的表现，是跟iOS相比下少数的优点之一。记得在2014年那会，最流行的Launcher是原生Launcher；而2015年，流行的Launcher变成了仿Windows Phone桌面。不过，现在没什么人换Launcher了。一个是各厂商ROM提供的Launcher已经做得很好了。并且，还可能会用到定制ROM提供的私有API，给ROM做特别优化。不但如此，厂商ROM的Launcher可能还会&lt;strong&gt;加私料&lt;/strong&gt;，比如miui/澎湃OS的「小部件」，就是在Launcher层面实现的。&lt;/p&gt;
&lt;h2&gt;Launcher3&lt;/h2&gt;
&lt;p&gt;Launcher3是AOSP默认的启动器。为什么要讲它呢？纯属是因为他集成在AOSP里且开源。&lt;/p&gt;
&lt;p&gt;各厂商ROM的Launcher，几乎是基于Launcher3做定制开发的。因此，我们只需要看Launcher3的源码，就可以大致了解Android APP在桌面点击时的启动流程。&lt;/p&gt;
&lt;p&gt;Launcher3代码：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://cs.android.com/android/platform/superproject/+/master:packages/apps/Launcher3/src/com/android/launcher3/&quot;}&lt;/p&gt;
&lt;h2&gt;Launcher的声明&lt;/h2&gt;
&lt;p&gt;上面也提到，Launcher的本质是一个&lt;strong&gt;APP&lt;/strong&gt;。Android系统启动后，第一个拉起的&lt;strong&gt;用户可见的前台APP&lt;/strong&gt;就是Launcher。那么系统是怎么拉的呢？&lt;/p&gt;
&lt;p&gt;打住！这里我们先不介绍「系统如何拉起Launcher」，这个后面的章节会提到。不过，既然系统需要知道哪些APP是Launcher，那么说明Launcher内部肯定有&lt;strong&gt;标记&lt;/strong&gt;才对？&lt;/p&gt;
&lt;p&gt;没错！所有Launcher APP，都需要在&lt;code&gt;AndroidManifest.xml&lt;/code&gt; 里添加声明。&lt;/p&gt;
&lt;p&gt;以Launcher3为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// AndroidManifest.xml
&amp;lt;manifest
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    package=&quot;com.android.launcher3&quot;&amp;gt;
    &amp;lt;uses-sdk android:targetSdkVersion=&quot;33&quot; android:minSdkVersion=&quot;30&quot;/&amp;gt;
  ...
   &amp;lt;activity
            android:name=&quot;com.android.launcher3.Launcher&quot;
            android:launchMode=&quot;singleTask&quot;
            android:clearTaskOnLaunch=&quot;true&quot;
             ...
              &amp;lt;intent-filter&amp;gt;
                &amp;lt;action android:name=&quot;android.intent.action.MAIN&quot; /&amp;gt;
                &amp;lt;action android:name=&quot;android.intent.action.SHOW_WORK_APPS&quot; /&amp;gt;
  							&amp;lt;!-- 下面这行是关键 --&amp;gt;
                &amp;lt;category android:name=&quot;android.intent.category.HOME&quot; /&amp;gt;
                &amp;lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&amp;gt;
                &amp;lt;category android:name=&quot;android.intent.category.MONKEY&quot;/&amp;gt;
  							&amp;lt;!-- 下面这行是关键 --&amp;gt;
                &amp;lt;category android:name=&quot;android.intent.category.LAUNCHER_APP&quot; /&amp;gt;
            &amp;lt;/intent-filter&amp;gt;
						...
	...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Launcher的实现&lt;/h2&gt;
&lt;p&gt;都说了Launcher是个A！P！P！，还要问怎么实现的吗（急&lt;/p&gt;
&lt;p&gt;从&lt;code&gt;AndroidManifest.xml&lt;/code&gt;可以知道，首个启动的Activity是&lt;code&gt;Launcher&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Launcher.java
public class Launcher extends StatefulActivity&amp;lt;LauncherState&amp;gt;
        implements Callbacks, InvariantDeviceProfile.OnIDPChangeListener,
        PluginListener&amp;lt;LauncherOverlayPlugin&amp;gt; {
          // ...
          @Override
    @TargetApi(Build.VERSION_CODES.S)
    protected void onCreate(Bundle savedInstanceState) {
      // ...
       setContentView(getRootView());
      // ...
    }
 // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getRootView()&lt;/code&gt;里就是桌面View的实现。然后就，没了！真的没了！&lt;/p&gt;
&lt;p&gt;但是，桌面的应用列表是怎么查的？Android本身就有API：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.android.com/reference/android/content/pm/PackageManager?hl=en&quot;}&lt;/p&gt;
&lt;p&gt;你可以通过这个API拿到用户安装的所有应用。然后，获取所有应用的icon，并平铺放在View里。在用户点击icon时，就通过Intent拉起Activity。&lt;/p&gt;
&lt;p&gt;在Launcher3怎么做的？&lt;/p&gt;
&lt;p&gt;首先，每个icon的View，都由&lt;code&gt;ItemInflater&lt;/code&gt;解析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Launcher.java
@Override
    @TargetApi(Build.VERSION_CODES.S)
    protected void onCreate(Bundle savedInstanceState) {
      // ...
       mItemInflater = new ItemInflater&amp;lt;&amp;gt;(this, mAppWidgetHolder, getItemOnClickListener(),
                mFocusHandler, new CellLayout(mWorkspace.getContext(), mWorkspace));
      // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ItemInflater&lt;/code&gt;是什么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Launcher.kt
class ItemInflater&amp;lt;T&amp;gt;(
    private val context: T,
    private val widgetHolder: LauncherWidgetHolder,
    private val clickListener: OnClickListener,
    private val focusListener: OnFocusChangeListener,
    private val defaultParent: ViewGroup
) where T : Context, T : ActivityContext {
  // ...
  
   @JvmOverloads
    fun inflateItem(item: ItemInfo, writer: ModelWriter, nullableParent: ViewGroup? = null): View? {
        val parent = nullableParent ?: defaultParent
        when (item.itemType) {
            Favorites.ITEM_TYPE_APPLICATION,
            Favorites.ITEM_TYPE_DEEP_SHORTCUT,
            Favorites.ITEM_TYPE_SEARCH_ACTION -&amp;gt; {
                var info =
                    if (item is WorkspaceItemFactory) {
                        (item as WorkspaceItemFactory).makeWorkspaceItem(context)
                    } else {
                        item as WorkspaceItemInfo
                    }
                if (info.container == Favorites.CONTAINER_PREDICTION) {
                    // Came from all apps prediction row -- make a copy
                    info = WorkspaceItemInfo(info)
                }
                return createShortcut(info, parent)
            }
            Favorites.ITEM_TYPE_FOLDER -&amp;gt;
                return FolderIcon.inflateFolderAndIcon(
                    R.layout.folder_icon,
                    context,
                    parent,
                    item as FolderInfo
                )
            Favorites.ITEM_TYPE_APP_PAIR -&amp;gt;
                return AppPairIcon.inflateIcon(
                    R.layout.app_pair_icon,
                    context,
                    parent,
                    item as AppPairInfo,
                    BubbleTextView.DISPLAY_WORKSPACE
                )
            Favorites.ITEM_TYPE_APPWIDGET,
            Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -&amp;gt;
                return inflateAppWidget(item as LauncherAppWidgetInfo, writer)
            else -&amp;gt; throw RuntimeException(&quot;Invalid Item Type&quot;)
        }
    }
  
   /**
     * Creates a view representing a shortcut inflated from the specified resource.
     *
     * @param parent The group the shortcut belongs to. This is not necessarily the group where the
     *   shortcut should be added.
     * @param info The data structure describing the shortcut.
     * @return A View inflated from layoutResId.
     */
    private fun createShortcut(info: WorkspaceItemInfo, parent: ViewGroup): View {
        val favorite =
            LayoutInflater.from(parent.context).inflate(R.layout.app_icon, parent, false)
                as BubbleTextView
        favorite.applyFromWorkspaceItem(info)
        favorite.setOnClickListener(clickListener)
        favorite.onFocusChangeListener = focusListener
        return favorite
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的&lt;code&gt;createShortcut&lt;/code&gt;，并不是指长按APP出现的菜单，而就是icon（为什么不是APP呢？桌面上还可能有文件夹是吧）。对于Launcher来说，桌面上每个icon都是一个shortcut。其实也合理，你在Windows 11桌面上的APP，不几乎都是快捷方式（shortcut）嘛。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;clickListener&lt;/code&gt;是什么？其实在&lt;code&gt;Launcher&lt;/code&gt;里就传入啦！但是，&lt;code&gt;Launcher&lt;/code&gt;并没有实现&lt;code&gt;getItemOnClickListener()&lt;/code&gt;，因为这个方法是&lt;strong&gt;父类&lt;/strong&gt;&lt;code&gt;BaseDraggingActivity&lt;/code&gt; 实现的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// BaseDraggingActivity.java
public abstract class BaseDraggingActivity extends BaseActivity {
  // ...
  @Override
    public View.OnClickListener getItemOnClickListener() {
        return ItemClickHandler.INSTANCE;
    }
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ItemClickHandler.INSTANCE&lt;/code&gt; 对应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ItemClickHandler.java
public class ItemClickHandler {
  // ...
   public static final OnClickListener INSTANCE = ItemClickHandler::onClick;
  // ...
  private static void onClick(View v) {
        // Make sure that rogue clicks don&apos;t get through while allapps is launching, or after the
        // view has detached (it&apos;s possible for this to happen if the view is removed mid touch).
        if (v.getWindowToken() == null) return;

        Launcher launcher = Launcher.getLauncher(v.getContext());
        if (!launcher.getWorkspace().isFinishedSwitchingState()) return;

        Object tag = v.getTag();
        if (tag instanceof WorkspaceItemInfo) {
            onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
        } else if (tag instanceof FolderInfo) {
            onClickFolderIcon(v);
        } else if (tag instanceof AppPairInfo) {
            onClickAppPairIcon(v);
        } else if (tag instanceof AppInfo) {
            startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
        } else if (tag instanceof LauncherAppWidgetInfo) {
            if (v instanceof PendingAppWidgetHostView) {
                if (DEBUG) {
                    String targetPackage = ((LauncherAppWidgetInfo) tag).getTargetPackage();
                    Log.d(TAG, &quot;onClick: PendingAppWidgetHostView clicked for&quot;
                            + &quot; package=&quot; + targetPackage);
                }
                onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
            } else {
                if (DEBUG) {
                    String targetPackage = ((LauncherAppWidgetInfo) tag).getTargetPackage();
                    Log.d(TAG, &quot;onClick: LauncherAppWidgetInfo clicked,&quot;
                            + &quot; but not instance of PendingAppWidgetHostView. Returning.&quot;
                            + &quot; package=&quot; + targetPackage);
                }
            }
        } else if (tag instanceof ItemClickProxy) {
            ((ItemClickProxy) tag).onItemClicked(v);
        }
    }
  // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;桌面上的icon，不一定是APP，还可能是文件夹等等。不过，点击的统一处理逻辑都在&lt;code&gt;onClick&lt;/code&gt;里了。我们集中看看&lt;code&gt;onClickAppShortcut&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ItemClickHandler.java
public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
        if (shortcut.isDisabled() &amp;amp;&amp;amp; handleDisabledItemClicked(shortcut, launcher)) {
            return;
        }

        // Check for abandoned promise
        if ((v instanceof BubbleTextView) &amp;amp;&amp;amp; shortcut.hasPromiseIconUi()
                &amp;amp;&amp;amp; (!Flags.enableSupportForArchiving() || !shortcut.isArchived())) {
            String packageName = shortcut.getIntent().getComponent() != null
                    ? shortcut.getIntent().getComponent().getPackageName()
                    : shortcut.getIntent().getPackage();
            if (!TextUtils.isEmpty(packageName)) {
                onClickPendingAppItem(
                        v,
                        launcher,
                        packageName,
                        (shortcut.runtimeStatusFlags
                                &amp;amp; ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0);
                return;
            }
        }

        // Start activities
        startAppShortcutOrInfoActivity(v, shortcut, launcher);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里分两种情况：&lt;/p&gt;
&lt;p&gt;包名为空，可能APP还在下载。跳转至&lt;code&gt;onClickPendingAppItem&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ItemClickHandler.java
private static void onClickPendingAppItem(View v, Launcher launcher, String packageName,
            boolean downloadStarted) {
        ItemInfo item = (ItemInfo) v.getTag();
        CompletableFuture&amp;lt;SessionInfo&amp;gt; siFuture;
        siFuture = CompletableFuture.supplyAsync(() -&amp;gt;
                        InstallSessionHelper.INSTANCE.get(launcher)
                                .getActiveSessionInfo(item.user, packageName),
                UI_HELPER_EXECUTOR);
        Consumer&amp;lt;SessionInfo&amp;gt; marketLaunchAction = sessionInfo -&amp;gt; {
            if (sessionInfo != null) {
                LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
                try {
                    launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
                            launcher.getActivityLaunchOptions(v, item).toBundle());
                    return;
                } catch (Exception e) {
                    Log.e(TAG, &quot;Unable to launch market intent for package=&quot; + packageName, e);
                }
            }
            // Fallback to using custom market intent.
            Intent intent = ApiWrapper.INSTANCE.get(launcher).getAppMarketActivityIntent(
                    packageName, Process.myUserHandle());
            launcher.startActivitySafely(v, intent, item);
        };

        if (downloadStarted) {
            // If the download has started, simply direct to the market app.
            siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR);
            return;
        }
        new AlertDialog.Builder(launcher)
                .setTitle(R.string.abandoned_promises_title)
                .setMessage(R.string.abandoned_promise_explanation)
                .setPositiveButton(R.string.abandoned_search,
                        (d, i) -&amp;gt; siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR))
                .setNeutralButton(R.string.abandoned_clean_this,
                        (d, i) -&amp;gt; launcher.getWorkspace()
                                .persistRemoveItemsByMatcher(ItemInfoMatcher.ofPackages(
                                        Collections.singleton(packageName), item.user),
                                        &quot;user explicitly removes the promise app icon&quot;))
                .create().show();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里会尝试到应用商店开始下载。而如果下载没开始，就会弹出一个对话框：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;title: 未安装此应用&lt;/li&gt;
&lt;li&gt;message: 未安装此图标对应的应用。您可以移除此图标，也可以尝试搜索相应的应用并手动安装。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;包名不为空，调用&lt;code&gt;startAppShortcutOrInfoActivity&lt;/code&gt;尝试拉起APP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ItemClickHandler.java
private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
        TestLogging.recordEvent(
                TestProtocol.SEQUENCE_MAIN, &quot;start: startAppShortcutOrInfoActivity&quot;);
        Intent intent = item.getIntent();
        if (item instanceof ItemInfoWithIcon itemInfoWithIcon) {
            if ((itemInfoWithIcon.runtimeStatusFlags
                    &amp;amp; ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
                intent = ApiWrapper.INSTANCE.get(launcher).getAppMarketActivityIntent(
                        itemInfoWithIcon.getTargetComponent().getPackageName(),
                        Process.myUserHandle());
            } else if (itemInfoWithIcon.itemType
                    == LauncherSettings.Favorites.ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON) {
                intent = ApiWrapper.INSTANCE.get(launcher).getAppMarketActivityIntent(
                        BuildConfig.APPLICATION_ID,
                        launcher.getAppsView().getPrivateProfileManager().getProfileUser());
                launcher.getStatsLogManager().logger().log(
                        LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP);
            }
        }
        if (intent == null) {
            throw new IllegalArgumentException(&quot;Input must have a valid intent&quot;);
        }
        if (item instanceof WorkspaceItemInfo) {
            WorkspaceItemInfo si = (WorkspaceItemInfo) item;
            if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)
                    &amp;amp;&amp;amp; Intent.ACTION_VIEW.equals(intent.getAction())) {
                // make a copy of the intent that has the package set to null
                // we do this because the platform sometimes disables instant
                // apps temporarily (triggered by the user) and fallbacks to the
                // web ui. This only works though if the package isn&apos;t set
                intent = new Intent(intent);
                intent.setPackage(null);
            }
            if ((si.options &amp;amp; WorkspaceItemInfo.FLAG_START_FOR_RESULT) != 0) {
                launcher.startActivityForResult(item.getIntent(), 0);
                InstanceId instanceId = new InstanceIdSequence().newInstanceId();
                launcher.logAppLaunch(launcher.getStatsLogManager(), item, instanceId);
                return;
            }
        }
        if (v != null &amp;amp;&amp;amp; launcher.supportsAdaptiveIconAnimation(v)
                &amp;amp;&amp;amp; !item.shouldUseBackgroundAnimation()) {
            // Preload the icon to reduce latency b/w swapping the floating view with the original.
            FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
        }
        launcher.startActivitySafely(v, intent, item);
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，这里逻辑比较多，因为对很多场景做了判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;APP还在安装，对应代码里的&lt;code&gt;FLAG_INSTALL_SESSION_ACTIVE&lt;/code&gt;。这时会跳转至应用商店。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐私空间，对应代码里的&lt;code&gt;ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Instant App，对应&lt;code&gt;FLAG_SUPPORTS_WEB_UI&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后，调用&lt;code&gt;FloatingIconView.fetchIcon&lt;/code&gt;预加载APP打开的icon动画。&lt;/p&gt;
&lt;p&gt;最后，通过&lt;code&gt;Launcher&lt;/code&gt;调用&lt;code&gt;startActivitySafely&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Launcher.java
@Override
    public RunnableList startActivitySafely(View v, Intent intent, ItemInfo item) {
        if (!hasBeenResumed()) {
            RunnableList result = new RunnableList();
            // Workaround an issue where the WM launch animation is clobbered when finishing the
            // recents animation into launcher. Defer launching the activity until Launcher is
            // next resumed.
            addEventCallback(EVENT_RESUMED, () -&amp;gt; {
                RunnableList actualResult = startActivitySafely(v, intent, item);
                if (actualResult != null) {
                    actualResult.add(result::executeAllAndDestroy);
                } else {
                    result.executeAllAndDestroy();
                }
            });
            if (mOnDeferredActivityLaunchCallback != null) {
                mOnDeferredActivityLaunchCallback.run();
                mOnDeferredActivityLaunchCallback = null;
            }
            return result;
        }

        RunnableList result = super.startActivitySafely(v, intent, item);
        if (result != null &amp;amp;&amp;amp; v instanceof BubbleTextView) {
            // This is set to the view that launched the activity that navigated the user away
            // from launcher. Since there is no callback for when the activity has finished
            // launching, enable the press state and keep this reference to reset the press
            // state when we return to launcher.
            BubbleTextView btv = (BubbleTextView) v;
            btv.setStayPressed(true);
            result.add(() -&amp;gt; btv.setStayPressed(false));
        }
        return result;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;icon点击之后，需要做&lt;strong&gt;icon被点击&lt;/strong&gt;的深色效果。上述的代码逻辑主要在做这个。而真正的APP启动逻辑，藏在&lt;code&gt;super.startActivitySafely(v, intent, item)&lt;/code&gt;里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ActivityContext.java
// ...
/**
     * Safely starts an activity.
     *
     * @param v View starting the activity.
     * @param intent Base intent being launched.
     * @param item Item associated with the view.
     * @return RunnableList for listening for animation finish if the activity was properly
     *         or started, {@code null} if the launch finished
     */
    default RunnableList startActivitySafely(
            View v, Intent intent, @Nullable ItemInfo item) {
        Preconditions.assertUIThread();
        Context context = (Context) this;
        if (isAppBlockedForSafeMode() &amp;amp;&amp;amp; !new ApplicationInfoWrapper(context, intent).isSystem()) {
            Toast.makeText(context, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
            return null;
        }

        boolean isShortcut = (item instanceof WorkspaceItemInfo)
                &amp;amp;&amp;amp; item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
                &amp;amp;&amp;amp; !((WorkspaceItemInfo) item).isPromise();
        if (isShortcut &amp;amp;&amp;amp; !WIDGETS_ENABLED) {
            return null;
        }
        ActivityOptionsWrapper options = v != null ? getActivityLaunchOptions(v, item)
                : makeDefaultActivityOptions(item != null &amp;amp;&amp;amp; item.animationType == DEFAULT_NO_ICON
                        ? SPLASH_SCREEN_STYLE_SOLID_COLOR : -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */);
        UserHandle user = item == null ? null : item.user;
        Bundle optsBundle = options.toBundle();
        // Prepare intent
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (v != null) {
            intent.setSourceBounds(Utilities.getViewBounds(v));
        }
        try {
            if (isShortcut) {
                String id = ((WorkspaceItemInfo) item).getDeepShortcutId();
                String packageName = intent.getPackage();
                ((Context) this).getSystemService(LauncherApps.class).startShortcut(
                        packageName, id, intent.getSourceBounds(), optsBundle, user);
            } else if (user == null || user.equals(Process.myUserHandle())) {
                // Could be launching some bookkeeping activity
                context.startActivity(intent, optsBundle);
            } else {
                context.getSystemService(LauncherApps.class).startMainActivity(
                        intent.getComponent(), user, intent.getSourceBounds(), optsBundle);
            }
            if (item != null) {
                InstanceId instanceId = new InstanceIdSequence().newInstanceId();
                logAppLaunch(getStatsLogManager(), item, instanceId);
            }
            return options.onEndCallback;
        } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
            Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
            Log.e(TAG, &quot;Unable to launch. tag=&quot; + item + &quot; intent=&quot; + intent, e);
        }
        return null;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果是Deep Shortcut，使用&lt;code&gt;LauncherApps.startShortcut&lt;/code&gt;调启；&lt;/li&gt;
&lt;li&gt;如果是当前用户，直接调用&lt;code&gt;Context.startActivity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;否则（跨用户），使用&lt;code&gt;LauncherApps.startMainActivity&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Android启动0 - 前言</title><link>https://blog.nowcent.cn/posts/android-boot-introduction/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/android-boot-introduction/</guid><pubDate>Thu, 12 Feb 2026 14:55:00 GMT</pubDate><content:encoded>&lt;p&gt;这个是我新开的专栏。其实这方面的知识，我从2023年就开始学习了。但是因为这部分知识在工作中很少用到，并且我日常的主力机型是iPhone，导致这里的知识一直处于学了忘、忘了学的状态。&lt;/p&gt;
&lt;p&gt;这部分知识，说重要也重要，&lt;s&gt;不然连main函数不知道在哪还好意思说自己是安卓开发&lt;/s&gt;；说不重要也确实不重要，应用开发很少会牵涉这方面的知识。&lt;/p&gt;
&lt;p&gt;最近我在折腾&lt;a href=&quot;/posts/mtls-to-cold-wallet-key-security-and-hardware-trust/&quot;&gt;客户端层面的硬件认证&lt;/a&gt;。本来以为整个流程已经十分安全了，但是不幸的是，经过跟HsukqiLee的友好交流，我们发现这个认证流程仍然有漏洞。于是乎，我又开始学习Android设备启动的流程，试图弄清 StrongBox / TEE 所依赖的可信执行环境究竟是否真正可信。这下得把学习的东西记录在博客里了，不然过一会又忘了。好在，最后的结论是StrongBox / TEE 所依赖的可执行环境仍然是可信的。有面具模块能“绕过“硬件证书认证，是因为Hook了Android KeyStore的Java层接口，用泄漏的硬件根证书完成签名。详见：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;hanakim3945/bl_sbx&quot;}&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;从手指点击屏幕上的APP Icon，到新的Activity被拉起，软件过程中间发生了什么？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个问题本来是我公司组内的一个技术分享选题，并且是我主推的。但是，后来有人觉得这个题目&lt;strong&gt;对开发无帮助&lt;/strong&gt;，把选题改成了《TXSP启动流程分享》。&lt;/p&gt;
&lt;p&gt;我想：我草，这有什么好分享的？自己去看代码不就行了吗？当然，因为我当时的话语权不够，也不好说什么。~~果然一旦涉及到底层知识，就知道谁在裸泳。~~不过，既然公司内无法分享，那我就把这部分的知识整理成博客吧。&lt;/p&gt;
</content:encoded></item><item><title>提升APP冷启动速度-iOS篇</title><link>https://blog.nowcent.cn/posts/speed-up-app-cold-start-ios/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/speed-up-app-cold-start-ios/</guid><description>最近Ham的冷启动速度真的是越来越慢了，慢到令人发指。从手指点击APP Icon到首个页面出现，居然需要3.5秒，是时候要好好优化下了！</description><pubDate>Tue, 10 Feb 2026 22:07:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本篇是iOS篇，关于Android优化的部分在&lt;a href=&quot;/posts/dev/%E6%8F%90%E5%8D%87app%E5%86%B7%E5%90%AF%E5%8A%A8%E9%80%9F%E5%BA%A6-android%E7%AF%87/&quot;&gt;《提升APP冷启动速度-Android篇》&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最近Ham的冷启动速度真的是越来越慢了，慢到令人发指。从手指点击APP Icon到首个页面出现，居然需要3.5秒，是时候要好好优化下了！&lt;/p&gt;
&lt;h2&gt;理论知识&lt;/h2&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/documentation/uikit/about-the-app-launch-sequence&quot;}&lt;/p&gt;
&lt;h2&gt;工具&lt;/h2&gt;
&lt;p&gt;Xcode贴心地为我们准备好了耗时排查工具&lt;strong&gt;App Launch&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/Screenshot%202026-02-10%20at%2022.38.48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/Screenshot%202026-02-10%20at%2022.39.35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们选择好要测量的APP和超时时间后，就可以点击左上角的按钮开始抓trace啦～&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/measure.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;测量&lt;/h2&gt;
&lt;p&gt;设备信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;iPhone 13&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统 iOS 26.0.2&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动过程：强杀APP并打开多个其他APP，避免dyld缓存优化启动速度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/progress.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;细看Progress部分，APP的启动过程可以分为三个部分：&lt;/p&gt;
&lt;h3&gt;初始化阶段T1&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./img/progress-t1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Progress Creation&lt;/code&gt; 系统创建进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;System Interface Initialization&lt;/code&gt; 系统接口初始化，此时dyld会解析动态符号&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;启动阶段T2&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./img/progress-t2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UIKit Initialization&lt;/code&gt; - UIKit初始化，不可规避&lt;/li&gt;
&lt;li&gt;&lt;code&gt;willFinishLaunchingWithOptions()&lt;/code&gt; -  &lt;code&gt;AppDelegate&lt;/code&gt;里的委托方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;didFinishLaunchingWithOptions()&lt;/code&gt; -  &lt;code&gt;AppDelegate&lt;/code&gt;里的委托方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sceneWillConnectTo()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sceneWillEnterForeground()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Initial Frame Rendering&lt;/code&gt; - 首帧渲染&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;前台阶段T3&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./img/progress-t3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;APP初始化完成，用户看到APP第一帧。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;当然，由系统的特性我们可以知道，因为第一帧是主线程绘制的，要优化冷启动时间，就必须要让主线程干更少的活。于是，我们看trace的时候，可以把目光瞄准在主线程上。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;通过分析trace，我们可以知道，占APP启动时间大头，并且可优化的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Initial Frame Rendering&lt;/code&gt; - 2.99s&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;System Interface Initialization&lt;/code&gt; - 757.31 ms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;willFinishLaunchingWithOptions()&lt;/code&gt; - 115.47ms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;didFinishLaunchingWithOptions()&lt;/code&gt; - 94.85ms&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Initial Frame Rendering优化&lt;/h2&gt;
&lt;p&gt;我们观察下这段trace：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/initial-frame-rendering.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;😯，居然有View在主线程开Database&lt;/p&gt;
&lt;h3&gt;日程卡优化&lt;/h3&gt;
&lt;p&gt;先来说下背景。Ham启动成功后，会进入“状态”tab，里面展示很多实时通知的状态卡片，比如天气、课程、校车、日程等：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/Screenshot%202026-02-11%20at%2011.26.49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ham里带日程功能，日程数据存在Realm数据库里。 &lt;code&gt;StatusScheduleCard&lt;/code&gt; 是一张状态卡片，用来展示用户的日程情况（见上图）。&lt;code&gt;StatusScheduleCard&lt;/code&gt;为什么会卡主线程？我们看看代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import SwiftUI
import RealmSwift
struct StatusScheduleCard: View {
    @ObservedResults(ScheduleItemModel.self,
            where: {
                let now = Date()
                return ($0.end == nil &amp;amp;&amp;amp; $0.begin &amp;gt;= now) ||
                        ($0.end != nil &amp;amp;&amp;amp; $0.end &amp;gt;= now)
            },
            sortDescriptor: SortDescriptor(keyPath: &quot;begin&quot;, ascending: true)) var scheduleItemList
  
  ...
  var body: some View {
		...
    ForEach(1..&amp;lt;5) { i in
                            if i &amp;lt; scheduleList.count {
                                let scheduleModel = scheduleList[i]
                                scheduleItemView(scheduleItem: scheduleModel)
                            }
                        }
    ...
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合trace，这下我们明白了：&lt;/p&gt;
&lt;p&gt;APP在渲染第一帧时会创造根View的struct，而因为日程卡要立刻上屏，因此也会初始化&lt;code&gt;StatusScheduleCard&lt;/code&gt;。&lt;code&gt;StatusScheduleCard&lt;/code&gt; 里有一个属性&lt;code&gt;scheduleItemList&lt;/code&gt;，被&lt;code&gt;ObservedResults&lt;/code&gt; wrap了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StatusScheduleCard&lt;/code&gt;上屏时需要获取前五项日程数据，此时访问&lt;code&gt;scheduleList[i]&lt;/code&gt;，实际会触发Realm数据库的初始化。在渲染View时初始化数据库，当然会卡啦😭&lt;/p&gt;
&lt;p&gt;当然，这个问题不能怪开发者，只能说明是框架本身的缺陷：开发者也不知道这个&lt;code&gt;@ObservedResults&lt;/code&gt;的lazy fetch会在主线程读取数据库啊😭😭&lt;/p&gt;
&lt;p&gt;怎么解决呢：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;延时加载日程卡。在T3阶段后再加载+展示。&lt;/li&gt;
&lt;li&gt;放弃使用&lt;code&gt;@ObservedResults&lt;/code&gt;，使用传统方式+线程切换打开Realm获取数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// StatusScheduleCard.swift
struct StatusScheduleCard: View {
    @ObservedObject var vm: StatusScheduleCardViewModel
    
    @ViewBuilder
    var body: some View {
        if vm.inited {
            StatusScheduleCardInner(vm: vm)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// StatusScheduleCardViewModel.swift
@MainActor
class StatusScheduleCardViewModel: ObservableObject, StatusCardViewModel {
   @Published var inited = false
  ...
  func onInit() {
        inited = true
        initUpdateTask()
    }
  
  private func initUpdateTask() {
        Task {
            while !Task.isCancelled {
                updateData()
                do {
                    try await Task.sleep(nanoseconds: 10_000_000_000)
                } catch {
                    break
                }
            }
        }
    }
    
    private func updateData() {
        currentTime = Date()
        let currentWeekInfo = getCurrentWeekInfo(date: currentTime)
        Task.detached(priority: .background) { [currentWeekInfo, currentTime] in
            let realm = try! Realm(queue: nil)
            let results = realm.objects(ScheduleItemModel.self)
                .where {
                    ($0.end == nil &amp;amp;&amp;amp; $0.begin &amp;gt;= currentTime) ||
                    ($0.end != nil &amp;amp;&amp;amp; $0.end &amp;gt;= currentTime)
                }
                .freeze()
                .sorted(byKeyPath: &quot;begin&quot;, ascending: true)
            let weekScheduleListResult = results
                .where {
                    return ($0.end == nil &amp;amp;&amp;amp; $0.begin &amp;lt;= currentWeekInfo.end) || ($0.end != nil &amp;amp;&amp;amp; $0.end &amp;lt;= currentWeekInfo.end)
                }
                .freeze()
            await MainActor.run {
                let snapshot = Array(results)
                let weekScheduleList = Array(weekScheduleListResult)
                withAnimation {
                    self.scheduleList = snapshot
                    self.weekScheduleList = weekScheduleList
                }
            }
        }
    }
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;校巴卡优化&lt;/h3&gt;
&lt;p&gt;除了日程卡，校巴卡也占据了很多时间，初始化的时候居然在创建WebView！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/bus-card-trace.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里再说下背景。&lt;code&gt;BusView&lt;/code&gt;是校巴的二级H5页。按道理来说这里不应该直接初始化才对？🤔&lt;/p&gt;
&lt;p&gt;真正原因其实在&lt;code&gt;CommonStatusCard&lt;/code&gt;上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// CommonStatusCard.swift
struct CommonStatusCard&amp;lt;Content: View&amp;gt;: View {
    let icon: String
    let title: String
    let color: Color
    let padding: CGFloat
    var navDest: AnyView? = nil
    let content: () -&amp;gt; Content

    init&amp;lt;NavView: View&amp;gt;(icon: String,
                        title: String,
                        color: Color,
                        padding: Int = 12,
                        navDest: () -&amp;gt; NavView,
                        content: @escaping () -&amp;gt; Content) {
        self.icon = icon
        self.title = title
        self.color = color
        self.navDest = AnyView(navDest())
        self.content = content
        self.padding = CGFloat(padding)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CommonStatusView&lt;/code&gt;在初始化时会&lt;strong&gt;直接&lt;/strong&gt;执行&lt;code&gt;navDest&lt;/code&gt;，并保存在&lt;code&gt;AnyView&lt;/code&gt;里。所以，&lt;code&gt;CommonStatusView&lt;/code&gt;初始化时，会初始化二级页里的内容。本质上说，是&lt;code&gt;CommonStatusView&lt;/code&gt;编码不合理引起了这个问题。&lt;/p&gt;
&lt;p&gt;最佳的解决方法是，&lt;code&gt;CommonStatusView&lt;/code&gt;里就不该使用&lt;code&gt;AnyView&lt;/code&gt;，应使用&lt;code&gt;ViewBuilder&lt;/code&gt;去存View。后期Ham的导航架构从&lt;code&gt;NavigationView&lt;/code&gt;迁移至&lt;code&gt;NavigationStack&lt;/code&gt;，这样&lt;code&gt;navDest&lt;/code&gt;里就不用真传一个&lt;code&gt;ViewBuilder&lt;/code&gt;进来，只用传一个Route就好了，也就没有了改造的烦恼。&lt;/p&gt;
&lt;h3&gt;ViewModel初始化优化&lt;/h3&gt;
&lt;p&gt;一般来说，每个Card下面都会有一个&lt;code&gt;ViewModel&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StatusCourseCard.swift
struct StatusCourseCard: View {

    @ObservedObject let vm: StatusCourseCardViewModel
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// StatusBusCard.swift
struct StatusBusCard: View {
    
    @ObservedObject var vm: StatusBusCardViewModel
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些卡片的&lt;code&gt;ViewModel&lt;/code&gt;，被外层&lt;code&gt;StatusView&lt;/code&gt;的&lt;code&gt;ViewModel&lt;/code&gt;创建并持有。外层的&lt;code&gt;ViewModel&lt;/code&gt;在初始化时就会去创建这些卡片的&lt;code&gt;ViewModel&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StatusContentViewModel.swift
@MainActor
class StatusContentViewModel: ObservableObject, @MainActor StatusCardController {
    let weatherCardVM = StatusWeatherCardViewModel()
    let libraryCardVM = StatusLibraryCardViewModel()
    let courseCardVM = StatusCourseCardViewModel()
    let busCardVM = StatusBusCardViewModel()
    let sportCardVM = StatusSportCardVM()
    let scheduleCardVM = StatusScheduleCardViewModel()
    let casAlertCardVM = StatusCasAlertCardViewModel()
  
  	...
    
    init() {
        onInit()
    }
  
  	...
    
    func onInit() {
        getAllVM().forEach { $0.onInit() }
    }
  
  	...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些卡片的&lt;code&gt;ViewModel&lt;/code&gt;在创建时：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StatusBusCardViewModel&lt;/code&gt;：初始化数据并开始定位&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StatusBusCardViewModel.swift
class StatusBusCardViewModel: ObservableObject, StatusCardViewModel, LocationListener {
  ...
  func onInit() {
        LocationManager.shared.registerListener(self)
        update()
        casContext.isLoginFlow.sink { [weak self] _ in
            self?.update()
        }
        .store(in: &amp;amp;cancellables)
        initTimer()
    }
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;StatusCourseCardViewModel&lt;/code&gt; 从sqlite数据库拉数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class StatusCourseCardViewModel: ObservableObject, StatusCardViewModel {
	...
  func onInit() {
        NotificationCenter.default.addObserver(self, selector: #selector(onCourseListUpdated), name: .byKey(.ham_courseUpdated), object: nil)
        updateCourseList()
    }
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些操作都是需要耗时的，真的有那么需要在T2时刻就要做吗？能否挪到T3再开始做呢？&lt;/p&gt;
&lt;p&gt;当然可以！但首先有个问题，T3时刻什么时候开始呢？好在，Apple提供了一个通知&lt;code&gt;didBecomeActiveNotification&lt;/code&gt;。外层的&lt;code&gt;ViewModel&lt;/code&gt;接收到该通知后，再调用每张卡片的&lt;code&gt;ViewModel&lt;/code&gt;初始化即可。但是注意，这个&lt;code&gt;didBecomeActiveNotification&lt;/code&gt;可能会触发多次，我们在代码层面上需要去重。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StatusView.swift
struct StatusView: View {
  var body: some View {
    ...
    .onReceive(NotificationCenter.default.publisher(
            for: UIApplication.didBecomeActiveNotification
        )) { _ in
            vm.contentVM.doInit()
        }
  }
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;冷启动后第一帧时，因为没有数据，屏幕上不会展示任何卡片了，这显然不是我们想要的。接下来的&lt;strong&gt;缓存&lt;/strong&gt;章节，就是为了解决这一点。&lt;/p&gt;
&lt;h3&gt;缓存&lt;/h3&gt;
&lt;p&gt;最近在用小红书，发现小红书冷启动时一个很有意思的点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;APP冷启动时，会先展示上次的数据，再执行刷新步骤。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;「执行刷新步骤」其实很好理解，这和我们的优化方案一致，在T3时刻再执行重逻辑。但是，「展示上次的数据」是怎么做到的？&lt;/p&gt;
&lt;p&gt;或许可以...直接打开db获取数据？但这不才在&lt;strong&gt;日程卡优化&lt;/strong&gt;补的坑嘛，冷启动阶段尽量不能操作重逻辑的IO。&lt;/p&gt;
&lt;p&gt;答案如下：把要展示的首帧数据，保存在&lt;code&gt;UserDefault&lt;/code&gt;里。&lt;/p&gt;
&lt;p&gt;但是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;UserDefault&lt;/code&gt; 的原理也是读文件做IO啊，首次访问也会非常慢。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过，&lt;strong&gt;这也总比打开数据库要强&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以天气卡为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StatusWeatherCardViewModel.swift

struct StatusWeatherForecastInfo: SmartCodable {
    var day: String = &quot;&quot;
    var weather: String = &quot;&quot;
    var dayTemp: String = &quot;&quot;
    var nightTemp: String = &quot;&quot;
}

struct StatusWeatherDisplayInfo: SmartCodable {
    var temp: String = &quot;&quot;
    var weather: String = &quot;&quot;
    var description: String = &quot;&quot;
    var forecastInfo: [StatusWeatherForecastInfo] = []
}

@MainActor
class StatusWeatherCardViewModel: ObservableObject, LocationListener, StatusCardViewModel {
  
   private let TAG = &quot;StatusWeatherCardViewModel&quot;

    @Published var loadState = LoadStatus.unload
    @Published var weatherInfo: StatusWeatherDisplayInfo?
    @Published var errorMessage: String = &quot;&quot;
    weak var controller: StatusCardController?

    private var lastUpdateWeatherTimestamp: Int64 = 0
    private var placemark: CLPlacemark? = nil
    private var weatherObj: Weather? = nil
    @Published var inited = false

    // MARK: - Init
    
    init() {
      // 初始化时读取数据
        let displayInfo = StatusWeatherDisplayInfo.deserialize(from: LocalStorageHelper.shared.getStringValue(.weatherCache))
        _weatherInfo = .init(initialValue: displayInfo)
    }
  
  	...
  	private func updateWeather(location: CLLocation) {
      ...
      // 天气获取成功时
      storeWeatherInfo(weatherInfo: weatherInfo)
    }
  
  	private func storeWeatherInfo(weatherInfo: StatusWeatherDisplayInfo) {
      // 存数据
        LocalStorageHelper.shared.set(.weatherCache, value: weatherInfo.toJSONString())
    }
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，就能保证在冷启动第一帧，看到上次的天气数据。&lt;/p&gt;
&lt;h3&gt;其他优化&lt;/h3&gt;
&lt;h4&gt;背景图片&lt;/h4&gt;
&lt;p&gt;状态卡的背景图片，是需要从网络上拉取的。冷启动时还没有拉取图片时，怎么办？&lt;/p&gt;
&lt;p&gt;首先，这里的背景图片使用&lt;code&gt;SDWebImage&lt;/code&gt; 组件，因为它支持图片&lt;strong&gt;硬盘缓存&lt;/strong&gt;。也就意味着，冷启动时只要传入上次展示图片的链接，图片会从本地取出并加载。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StatusBackgroundView.swift
struct StatusBackgroundView: View {
    @ObservedObject var vm: StatusBackgroundViewModel
    let onGetImage: (UIImage) -&amp;gt; Void
    
    var body: some View {
        Group {
            if let url = URL(string: vm.picURL) {
                WebImage(url: url)
                    .resizable()
                    .onFailure { error in
                        Log.e(&quot;StatusBackgroundView&quot;, &quot;load pic error&quot;, error)
                        vm.loadState = .loadedWithError
                    }
                    .onSuccess { image, data, cacheType in
                        Log.i(&quot;StatusBackgroundView&quot;, &quot;load pic success&quot;)
                        vm.loadState = .loaded
                        vm.savePicCache()
                        onGetImage(image as UIImage)
                    }
                    .scaledToFill()
                    .transition(.fade(duration: 0.5))
            }
        }
        .onReceive(NotificationCenter.default.publisher(
            for: UIApplication.didBecomeActiveNotification
        )) { _ in
            vm.doInit()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在T3时刻更新图片链接数据。如果返回的图片链接列表里，存在当前展示的图片链接，就不需要更新缓存了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StatusBackgroundViewModel.swift
class StatusBackgroundViewModel: ObservableObject {
  ...
  func fetchPicUrl() {
        if !inited {
            return
        }
        
        loadState = .loading
        HamRequestHelper.shared.doRequest(DailyPicRequest()) { [weak self] response in
            if let error = response.error {
                Log.e(TAG, &quot;fetchPicUrl - error&quot;, error)
                self?.loadState = .loadedWithError
                return
            }
            
            guard let data = response.data as? DailyPicResponse else {
                self?.loadState = .loadedWithError
                return
            }
            if data.picUrlList.isEmpty {
                Log.i(TAG, &quot;fetchPicUrl - data is empty&quot;)
                self?.loadState = .loadedWithError
                return
            }
            
            Task { @MainActor in
                guard let self else { return }
                let firstLoad = self.firstLoad
                self.firstLoad = false
                if firstLoad {
                    if !self.picURL.isEmpty {
                        for picUrlData in data.picUrlList {
                            if picUrlData.url == self.picURL {
                                return
                            }
                        }
                    }
                }
                let url = data.picUrlList[ Int.random(in: data.picUrlList.indices) ].url
                if self.picURL != url {
                    if firstLoad &amp;amp;&amp;amp; !self.picURL.isEmpty {
                        try? await Task.sleep(for: .seconds(10))
                    }
                    withAnimation {
                        self.picURL = data.picUrlList[ Int.random(in: data.picUrlList.indices) ].url
                    }
                }
                Log.i(TAG, &quot;fetchPicUrl - success =&amp;gt; \(self.picURL)&quot;)
            }
        }
    }
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;React Native模块初始化&lt;/h4&gt;
&lt;p&gt;为什么会牵涉到React Native？虽然在二级页里才会用到React Native，但是APP初始化阶段更新bundle是有必要的。不过，这个更新并不紧急，没有必要因为这个占据冷启动时间。&lt;/p&gt;
&lt;p&gt;首先，在&lt;code&gt;ContentView&lt;/code&gt;上overlay一个RNView（老版本也是如此）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ContentView.swift
struct ContentView: View {
  var body: some View {
    ...
    .overlay(alignment: .topLeading) {
            RNCommonView()
                .frame(width: 0.5, height: 0.5)
        }
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，在T3时刻再加载RNView。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// RNCommonView.swift

struct RNCommonView: View {
    
    @State var show = false
    
    var body: some View {
        ZStack {
            if show {
                RNContainerAsyncView(moduleName: &quot;RNCommon&quot;)
            }
        }
        .onReceive(NotificationCenter.default.publisher(
            for: UIApplication.didBecomeActiveNotification
        )) { _ in
            show = true
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;RNContainerAsyncView&lt;/code&gt;其实就是跑了个task去初始化RNView，为了不占渲染队列。而且因为React Native不支持在子线程初始化，还不能把task放在子线程里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct RNContainerView: UIViewRepresentable {
    
    private let moduleName: String
    
    init(moduleName: String) {
        self.moduleName = moduleName
    }

    func makeUIView(context: Context) -&amp;gt; UIView {
        RNViewManager.shared.getView(moduleName: moduleName)
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        
    }
}

struct RNContainerAsyncView: View {
    
    let moduleName: String
    
    @State var rnView: UIView? = nil
    
    var body: some View {
        ZStack {
            if let rnView = rnView {
                RNContainerInnerView(view: rnView)
            }
        }
        .task {
            if rnView != nil {
                return
            }
            let view = RNViewManager.shared.getView(moduleName: moduleName)
            await MainActor.run {
                rnView = view
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React Native的代码，可参考：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;whu-ham/ham-rn&quot;}&lt;/p&gt;
&lt;h3&gt;优化后&lt;/h3&gt;
&lt;p&gt;该阶段骤降至152ms。&lt;/p&gt;
&lt;h2&gt;System Interface Initialization优化&lt;/h2&gt;
&lt;p&gt;看看该项的trace，发现有一堆的Map image:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/dyld.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说明APP里有一堆动态库：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/Screenshot%202026-02-11%20at%2012.29.47.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那为什么不能把这些动态库打包进APP里，这样不就节省了dyld解析符号的时间吗？🤔&lt;/p&gt;
&lt;p&gt;没错，cocoapods提供了一个选项，支持将包以静态库的方式引入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;platform :ios, &apos;15.1&apos;
-use_frameworks!
+use_frameworks! :linkage =&amp;gt; :static
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，将Pods改成静态链接引入会导致包体积增大。不过，用少部分包体积增长换来更快的冷启动速度，是一个值得的trade-off。&lt;/p&gt;
&lt;p&gt;完工后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态库数量急剧减少&lt;/li&gt;
&lt;li&gt;该阶段启动市场优化至 350.04 ms，立省350ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./img/Screenshot%202026-02-11%20at%2014.20.18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;启动队列&lt;/h2&gt;
&lt;p&gt;其实，之前就做过一版启动队列优化，有效果但是不大：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ColdStartManager.swift
class ColdStartManager {
    static let shared = ColdStartManager()

    var privacyAgree: Bool {
        set {
            LocalStorageHelper.shared.set(.app_isReadPrivacy, value: newValue)
        }
        get {
            LocalStorageHelper.shared.getBool(.app_isReadPrivacy) ?? false
        }
    }

    private init() {}

    private weak var appDelegate: AppDelegate? = nil
    
    private let prepareLaunchCodeStartTask = PrepareLaunchCodeStartTask()
    private let primaryColdStartTask = PrimaryColdStartTask()
    private let secondaryAsyncColdStartTask = SecondaryAsyncColdStartTask()

    func onPrepareLaunch(delegate: AppDelegate) {
        logTime(&quot;prepareLaunch&quot;) { prepareLaunchCodeStartTask.action(delegate: delegate, privacyAgreed: privacyAgree) }
    }
    
    func doColdStart(delegate: AppDelegate) {
        appDelegate = delegate
        logTime(&quot;primaryColdStartTask.action&quot;) { primaryColdStartTask.action(delegate: delegate, privacyAgreed: privacyAgree) }
        logTime(&quot;secondaryAsyncColdStartTask.action&quot;) { secondaryAsyncColdStartTask.action(delegate: delegate, privacyAgreed: privacyAgree) }
    }

    func setPrivacyAgree() {
        guard let appDelegate = appDelegate else {
            return
        }
        LocalStorageHelper.shared.set(.app_isReadPrivacy, value: true)
        logTime(&quot;primaryColdStartTask.onPrivacyAgree&quot;) { primaryColdStartTask.onPrivacyAgree(delegate: appDelegate) }
        logTime(&quot;secondaryAsyncColdStartTask.onPrivacyAgree&quot;) { secondaryAsyncColdStartTask.onPrivacyAgree(delegate: appDelegate) }
        NotificationCenter.default.post(name: .byKey(.ham_privacyRead), object: nil)
    }
}

protocol ColdStartActionTask {
    func action(delegate: AppDelegate, privacyAgreed: Bool)
    func onPrivacyAgree(delegate: AppDelegate)
}

class PrepareLaunchCodeStartTask: ColdStartActionTask {
    private let actionList: [ColdStartAction] = [...]
    
    func action(delegate: AppDelegate, privacyAgreed: Bool) {
        actionList.forEach { action in
            action.action(delegate: delegate, privacyAgreed: privacyAgreed)
        }
    }
    
    func onPrivacyAgree(delegate: AppDelegate) {
        actionList.forEach { action in
            action.onPrivacyAgree(delegate: delegate)
        }
    }
}

class PrimaryColdStartTask: ColdStartActionTask {
    private let actionList: [ColdStartAction] = [...]
    
    func action(delegate: AppDelegate, privacyAgreed: Bool) {
        actionList.forEach { action in
            action.action(delegate: delegate, privacyAgreed: privacyAgreed)
        }
    }
    
    func onPrivacyAgree(delegate: AppDelegate) {
        actionList.forEach { action in
            action.onPrivacyAgree(delegate: delegate)
        }
    }
}

class SecondaryAsyncColdStartTask: ColdStartActionTask {
    private let actionList: [ColdStartAction] = [...]
    
    func action(delegate: AppDelegate, privacyAgreed: Bool) {
        actionList.forEach { action in
            Task {
                action.action(delegate: delegate, privacyAgreed: privacyAgreed)
            }
        }
    }
    
    func onPrivacyAgree(delegate: AppDelegate) {
        actionList.forEach { action in
            Task {
                action.onPrivacyAgree(delegate: delegate)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;onPrepareLaunch&lt;/code&gt; 对应 &lt;code&gt;willFinishLaunchingWithOptions&lt;/code&gt;，而&lt;code&gt;doColdStart&lt;/code&gt; 对应&lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为什么效果不大？因为所有的task几乎都是在主线程上跑的。包括&lt;code&gt;SecondaryAsyncColdStartTask&lt;/code&gt;，因为在主线程域开的Task，也是由主线程调度。&lt;/p&gt;
&lt;h3&gt;子任务Task改Detach&lt;/h3&gt;
&lt;p&gt;将任务调度里的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task {
 ... 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task.detached(name: &quot;ColdStartTask&quot;, priority: .background) {
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以不继承&lt;code&gt;MainActor&lt;/code&gt;的上下文，减缓主线程压力。&lt;/p&gt;
&lt;p&gt;但是，detach是有风险的，你必须要确保任务能detach才能这么做。&lt;/p&gt;
&lt;h3&gt;添加idle队列&lt;/h3&gt;
&lt;p&gt;idle阶段在APP首帧展示时触发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MainView.swift
struct MainView: View {
    ...
    
    var body: some View {
        ...
        .onReceive(NotificationCenter.default.publisher(
            for: UIApplication.didBecomeActiveNotification
        )) { _ in
            ColdStartManager.shared.onIdle()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;完工后的ColdStartManager&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ColdStartManager.swift
class ColdStartManager {
    static let shared = ColdStartManager()

    var privacyAgree: Bool {
        set {
            LocalStorageHelper.shared.set(.app_isReadPrivacy, value: newValue)
        }
        get {
            LocalStorageHelper.shared.getBool(.app_isReadPrivacy) ?? false
        }
    }

    private init() {}

    private weak var appDelegate: AppDelegate? = nil
    
    private let prepareLaunchCodeStartTask = PrepareLaunchCodeStartTask()
    private let primaryMainColdStartTask = PrimaryMainColdStartTask()
    private let primaryAsyncColdStartTask = PrimaryAsyncColdStartTask()
    private let secondaryAsyncColdStartTask = SecondaryAsyncColdStartTask()
    private let idleMainColdStartTask = IdleMainColdStartTask()
    private let idleAsyncColdStartTask = IdleAsyncColdStartTask()
    
    private var idleInited = false

    func onPrepareLaunch(delegate: AppDelegate) {
        logTime(&quot;prepareLaunch&quot;) { prepareLaunchCodeStartTask.action(delegate: delegate, privacyAgreed: privacyAgree) }
    }
    
    func onDidFinishLaunching(delegate: AppDelegate) {
        appDelegate = delegate
        logTime(&quot;primaryMainColdStartTask.action&quot;) {
            primaryMainColdStartTask.action(
                delegate: delegate,
                privacyAgreed: privacyAgree
            )
        }
        Task
            .detached(name: &quot;ColdStartTask&quot;, priority: .background) { [
                primaryAsyncColdStartTask,
                secondaryAsyncColdStartTask,
                privacyAgree
            ] in
            logTime(&quot;primaryColdStartTask.action&quot;) {
                primaryAsyncColdStartTask.action(
                    delegate: delegate,
                    privacyAgreed: privacyAgree
                )
            }
            
            logTime(&quot;secondaryAsyncColdStartTask.action&quot;) {
                secondaryAsyncColdStartTask.action(
                    delegate: delegate,
                    privacyAgreed: privacyAgree
                )
            }
        }
    }

    func onPrivacyAgreed() {
        guard let appDelegate = appDelegate else {
            return
        }
        LocalStorageHelper.shared.set(.app_isReadPrivacy, value: true)
        NotificationCenter.default.post(name: .byKey(.ham_privacyRead), object: nil)
        logTime(&quot;primaryMainColdStartTask.onPrivacyAgree&quot;) {
            primaryMainColdStartTask.onPrivacyAgree(delegate: appDelegate)
        }
        Task.detached(name: &quot;ColdStartTask - After privacy read&quot;, priority: .background) { [primaryAsyncColdStartTask, secondaryAsyncColdStartTask, appDelegate] in
            logTime(&quot;primaryColdStartTask.onPrivacyAgree&quot;) {
                primaryAsyncColdStartTask.onPrivacyAgree(delegate: appDelegate)
            }
            
            logTime(&quot;secondaryAsyncColdStartTask.onPrivacyAgree&quot;) {
                secondaryAsyncColdStartTask.onPrivacyAgree(delegate: appDelegate)
            }
        }
    }
    
    func onIdle() {
        guard let appDelegate = appDelegate, !idleInited else {
            return
        }
        idleInited = true
        Task { @MainActor in
            logTime(&quot;idleMainColdStartTask.action&quot;) {
                idleMainColdStartTask.action(
                    delegate: appDelegate,
                    privacyAgreed: privacyAgree
                )
            }
        }
        Task.detached(name: &quot;ColdStartTask - onIdle&quot;, priority: .background) { [idleAsyncColdStartTask, appDelegate, privacyAgree] in
            logTime(&quot;idleAsyncColdStartTask.action&quot;) {
                idleAsyncColdStartTask.action(delegate: appDelegate, privacyAgreed: privacyAgree)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前的启动队列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须启动前完成（主线程/同步依赖）&lt;/li&gt;
&lt;li&gt;可异步但需尽快（后台异步）&lt;/li&gt;
&lt;li&gt;idle 触发（完全可延迟）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后，我们把各冷启动Task按照需要放进不同的队列里。&lt;/p&gt;
&lt;p&gt;举个例子，像Firebase这种监测崩溃的SDK，需要在启动时就初始化，因此放在&lt;code&gt;PrimaryMainColdStartTask&lt;/code&gt;里；&lt;/p&gt;
&lt;p&gt;而像QQ SDK这种不急于初始化的操作，就可以放在&lt;code&gt;IdleAsyncColdStartTask&lt;/code&gt; 里。&lt;/p&gt;
&lt;h2&gt;成果与反思&lt;/h2&gt;
&lt;p&gt;优化前：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/before.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;优化后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/after.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;优化后，APP的启动时间骤降至平均500ms，最快仅需300ms。&lt;/p&gt;
&lt;p&gt;当然，这个过程也充满了各种坑。比如我把Firebase的SDK初始化过程放到idle阶段，会导致APP启动即崩溃的上报失效。还有，我把Xlog的初始化放在子线程，导致部分日志丢失。本质上，启动任务队列本身就是一个不断权衡（trade-off）的过程。&lt;/p&gt;
&lt;p&gt;好在，经过大量反复测试与调整，我最终拿到了一个稳定可运行的启动队列。&lt;/p&gt;
</content:encoded></item><item><title>Kotlin Native编译原理03 - 简单「深入」理解Objective-C运行时（二）</title><link>https://blog.nowcent.cn/posts/kotlin-native-compiler-03-objective-c-runtime-2/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/kotlin-native-compiler-03-objective-c-runtime-2/</guid><pubDate>Tue, 10 Feb 2026 13:17:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;其实上一篇文章&lt;a href=&quot;/posts/kotlin-native-compiler-02-objective-c-runtime-1/&quot;&gt;《Kotlin Native编译原理02 - 简单「深入」理解Objective-C运行时（一）》&lt;/a&gt;写完后，这篇文章就立马开始写了。但是在写文章的那段时间，有很多活动，所以写文章的事情也渐渐耽搁了下来，直到最近。&lt;/p&gt;
&lt;p&gt;上一篇文章写完后，我就在思考，写的文章是不是跑题了？明明我要讲的是“运行时”，为什么会牵涉到很多内核甚至指令集的知识？但是事实就是这么有意思，“运行时”确实是底层原理息息相关。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;OOP与消息&lt;/h2&gt;
&lt;p&gt;世间本无OOP，OOP的概念是如何发明的呢？&lt;/p&gt;
&lt;p&gt;1950-1960年代，大家还在用LISP语言开发时，开始对某块连续的内存（结构体）描述成&lt;strong&gt;对象&lt;/strong&gt;。1960年代，Simula语言诞生，首次引入了对象、类、继承、虚过程（早期的vtable）的概念，是世界上第一门面向对象的语言。Simula首次将代码跟子过程绑定在一起，后期这一概念称为“方法”，即某个类对一个函数的实现。类里的每个方法都会被编译为单独的函数，每个函数的第一个参数固定为对象地址。这样调用某个对象的方法，其实是调用方法对应的函数，第一个参数传入该对象的地址。&lt;/p&gt;
&lt;p&gt;Alan Kay受Simula的启发，结合自己对OOP的理解，于1970年发布第一门纯面向对象语言Smalltalk。Alan Kay对OOP的理解和Simula是不完全相同的，他认为对象间只能通过&lt;strong&gt;消息&lt;/strong&gt;通讯，而不是方法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages (so messaging came at the very beginning – it took a while to see how to do messaging in a programming language efficiently enough to be useful).
Alan Kay&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;消息跟方法有什么不同？方法本质上还是函数，只是第一个参数写死为对象地址，所以不能调用动态地往对象里加方法。而消息是让对象执行某个逻辑的&lt;strong&gt;请求&lt;/strong&gt;，对象收到消息后内部决定是否处理，以及如何处理消息。不管是编译时还是运行时，给对象发任何消息都是可行的。在实现上，对象内部会有一个类似&lt;strong&gt;消息派发中心&lt;/strong&gt;的逻辑，专门负责处理消息。如果消息能被处理，再派发到对应的处理子过程/函数里。&lt;/p&gt;
&lt;p&gt;1981年Brad Cox在ITT上班，开始接触Smalltalk。而他也意识到C语言面向过程的局限性，决定给C语言加上Smalltalk的特性。并于1983年，发布了支持Smalltalk对象特性的C语言预编译器：OOPC&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://dl.acm.org/doi/pdf/10.1145/948093.948095&quot;}&lt;/p&gt;
&lt;p&gt;后面他们发现，用预编译器来实现面向对象的特性，局限性太大了。于是他转向支持C语言面向对象拓展的开发，并于1986年，通过Stepstone发布了支持面向对象特性的C语言——Objective-C。&lt;/p&gt;
&lt;p&gt;1988年，乔布斯离开Apple，创办NeXT公司，开发NeXTSTEP操作系统，正苦于为NeXTSTEP寻找一门面向对象且效率高&lt;strong&gt;并且支持C语言&lt;/strong&gt;的语言。乔布斯先前访问过开发Smalltalk语言的公司，Smalltalk对他产生了非常大的影响。后面他发现了Objective-C，所以一拍即合，选择Objective-C作为NeXTSTEP系统的开发语言。&lt;/p&gt;
&lt;p&gt;后面的事情大家也知道了。20世纪末，乔布斯重返Apple，Apple收购了NeXT公司，NeXTSTEP里优秀的Cocoa库收归Apple。Brad Cox创办的Stepstone公司也于20世纪末期被Apple收购，至此Objective-C成为了Apple开发首选语言，直到2014年Swift的发布。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;为什么Objective-C调用对象子过程被称为消息？因为这本来就是Objective-C&lt;strong&gt;诞生的原因&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;初探objc_msgSend&lt;/h2&gt;
&lt;p&gt;在上一章我们就知道，对于&lt;code&gt;[objc sayHello]&lt;/code&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实际调用&lt;code&gt;objc_msgSend(objc, SEL(&quot;sayHello&quot;)) &lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEL(&quot;sayHello&quot;)&lt;/code&gt; 是伪代码，实际是一个指向&lt;code&gt;sayHello&lt;/code&gt; 的可读区域字符串指针。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;objc_msgSend&lt;/code&gt;的原型是&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// message.h
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt; 传入接收消息的对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SEL&lt;/code&gt; selector&lt;/li&gt;
&lt;li&gt;&lt;code&gt;op&lt;/code&gt; 可变参数，是消息参数，&lt;strong&gt;并受 method type encoding 与 ABI 约束&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SEL&lt;/h2&gt;
&lt;p&gt;我们来看一个demo：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;Foundation/Foundation.h&amp;gt;
#include &amp;lt;objc/message.h&amp;gt;

@interface MyClass: NSObject
@end

@implementation MyClass
- (void)sayHi {
	printf(&quot;Hi\n&quot;);
}
@end

int main() {
	MyClass *obj = [[MyClass alloc] init];
	// 1
	((void (*)(id, SEL))objc_msgSend)(obj, @selector(sayHi));
	// 2
	((void (*)(id, SEL))objc_msgSend)(obj, NSSelectorFromString(@&quot;sayHi&quot;));
	// 3 下面的代码会导致异常
	((void (*)(id, SEL))objc_msgSend)(obj, &quot;sayHi&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3出现异常，说明在Objective-C里，&lt;code&gt;&quot;sayHi&quot;&lt;/code&gt; 与 &lt;code&gt;@selector(sayHi)&lt;/code&gt; / &lt;code&gt;NSSelectorFromString(@&quot;sayHi&quot;)&lt;/code&gt; 并不属于同一条消息。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;@selector&lt;/code&gt; 是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从上一章我们可以知道，&lt;code&gt;@selector(msg_name)&lt;/code&gt;实际上是从&lt;code&gt;__objc_selrefs&lt;/code&gt;段取指向&lt;code&gt;__objc_methname&lt;/code&gt;段里字符串为&lt;code&gt;msg_name&lt;/code&gt;的指针。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;NSSelectorFromString&lt;/code&gt;是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们先看结果，看下 &lt;code&gt;NSSelectorFromString(@&quot;sayHi&quot;)&lt;/code&gt; 返回什么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;Foundation/Foundation.h&amp;gt;
#include &amp;lt;objc/message.h&amp;gt;

int main() {
	printf(&quot;%p\n&quot;, @selector(sayHi));
	printf(&quot;%p\n&quot;, NSSelectorFromString(@&quot;sayHi&quot;));
}

0x10244ca22
0x10244ca22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@selector(sayHi) == NSSelectorFromString(@&quot;sayHi&quot;)&lt;/code&gt; ，这也就能说明为什么demo中1和2的执行结果一致。&lt;/p&gt;
&lt;p&gt;并且，&lt;code&gt;&quot;sayHi&quot;&lt;/code&gt; 存在字符串常量区，地址与 &lt;code&gt;@selector(sayHi)&lt;/code&gt; / &lt;code&gt;NSSelectorFromString(@&quot;sayHi&quot;)&lt;/code&gt; 不同。这也能够说明对于两个selector，Objective-C是通过比对selector的地址来判断是否属于同一条消息，并不是通过简单的字符串比对判断。换句话说，&lt;code&gt;SEL&lt;/code&gt; 的唯一性来自 &lt;code&gt;sel_registerName&lt;/code&gt; 的**字符串驻留（intern）**逻辑。&lt;/p&gt;
&lt;p&gt;而这种「将字符串当作SEL传入&lt;code&gt;objc_msgSend&lt;/code&gt;」的行为，属于UB。实际编码过程万万不可这么写。😩&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NSSelectorFromString&lt;/code&gt; 为什么会返回一个指向&lt;code&gt;__objc_methname&lt;/code&gt; 段里的selector呢？&lt;code&gt;NSSelectorFromString&lt;/code&gt; 并没有开源，我们写个程序打断点看看。&lt;/p&gt;
&lt;h3&gt;写一个非常简单的demo程序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// test.c
#include &amp;lt;Foundation/Foundation.h&amp;gt;
#include &amp;lt;objc/message.h&amp;gt;

int main() {
	void *sel = NSSelectorFromString(@&quot;sayHi&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编译+打断点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;(lldb) br set -r NSSelectorFromString
Breakpoint 1: where = Foundation`NSSelectorFromString, address = 0x000000018ee87f20
(lldb) r
Process 6955 launched: &apos;/Users/orangeboy/Downloads/untitled folder/test&apos; (arm64)
Process 6955 stopped
* thread #1, queue = &apos;com.apple.main-thread&apos;, stop reason = breakpoint 1.1
    frame #0: 0x000000018ee87f20 Foundation`NSSelectorFromString
Foundation`NSSelectorFromString:
-&amp;gt;  0x18ee87f20 &amp;lt;+0&amp;gt;:  pacibsp 
    0x18ee87f24 &amp;lt;+4&amp;gt;:  stp    x22, x21, [sp, #-0x30]!
    0x18ee87f28 &amp;lt;+8&amp;gt;:  stp    x20, x19, [sp, #0x10]
    0x18ee87f2c &amp;lt;+12&amp;gt;: stp    x29, x30, [sp, #0x20]
Target 0: (test) stopped.
(lldb) disa
Foundation`NSSelectorFromString:
-&amp;gt;  0x18ee87f20 &amp;lt;+0&amp;gt;:   pacibsp 
    0x18ee87f24 &amp;lt;+4&amp;gt;:   stp    x22, x21, [sp, #-0x30]!
    0x18ee87f28 &amp;lt;+8&amp;gt;:   stp    x20, x19, [sp, #0x10]
    0x18ee87f2c &amp;lt;+12&amp;gt;:  stp    x29, x30, [sp, #0x20]
    0x18ee87f30 &amp;lt;+16&amp;gt;:  add    x29, sp, #0x20
    0x18ee87f34 &amp;lt;+20&amp;gt;:  sub    sp, sp, #0x3f0
    0x18ee87f38 &amp;lt;+24&amp;gt;:  adrp   x8, 402965
    0x18ee87f3c &amp;lt;+28&amp;gt;:  ldr    x8, [x8, #0x388]
    0x18ee87f40 &amp;lt;+32&amp;gt;:  ldr    x8, [x8]
    0x18ee87f44 &amp;lt;+36&amp;gt;:  stur   x8, [x29, #-0x28]
    0x18ee87f48 &amp;lt;+40&amp;gt;:  cbz    x0, 0x18ee87fc0 ; &amp;lt;+160&amp;gt;
    0x18ee87f4c &amp;lt;+44&amp;gt;:  mov    x19, x0
    0x18ee87f50 &amp;lt;+48&amp;gt;:  bl     0x18fc95e80    ; objc_msgSend$length
    0x18ee87f54 &amp;lt;+52&amp;gt;:  mov    x20, x0
    0x18ee87f58 &amp;lt;+56&amp;gt;:  mov    x2, sp
    0x18ee87f5c &amp;lt;+60&amp;gt;:  mov    x0, x19
    0x18ee87f60 &amp;lt;+64&amp;gt;:  mov    w3, #0x3e8 ; =1000 
    0x18ee87f64 &amp;lt;+68&amp;gt;:  mov    w4, #0x4 ; =4 
    0x18ee87f68 &amp;lt;+72&amp;gt;:  bl     0x18fc8f1c0    ; objc_msgSend$getCString:maxLength:encoding:
    0x18ee87f6c &amp;lt;+76&amp;gt;:  cbz    w0, 0x18ee87f88 ; &amp;lt;+104&amp;gt;
    0x18ee87f70 &amp;lt;+80&amp;gt;:  mov    x0, sp
    0x18ee87f74 &amp;lt;+84&amp;gt;:  bl     0x18f88336c    ; symbol stub for: strlen
    0x18ee87f78 &amp;lt;+88&amp;gt;:  cmp    x0, x20
    0x18ee87f7c &amp;lt;+92&amp;gt;:  b.ne   0x18ee87f88    ; &amp;lt;+104&amp;gt;
    0x18ee87f80 &amp;lt;+96&amp;gt;:  mov    x0, sp
    0x18ee87f84 &amp;lt;+100&amp;gt;: b      0x18ee87fb4    ; &amp;lt;+148&amp;gt;
    0x18ee87f88 &amp;lt;+104&amp;gt;: cbz    x20, 0x18ee87fac ; &amp;lt;+140&amp;gt;
    0x18ee87f8c &amp;lt;+108&amp;gt;: mov    x21, #0x0 ; =0 
    0x18ee87f90 &amp;lt;+112&amp;gt;: mov    x0, x19
    0x18ee87f94 &amp;lt;+116&amp;gt;: mov    x2, x21
    0x18ee87f98 &amp;lt;+120&amp;gt;: bl     0x18fc891e0    ; objc_msgSend$characterAtIndex:
    0x18ee87f9c &amp;lt;+124&amp;gt;: cbz    w0, 0x18ee87fbc ; &amp;lt;+156&amp;gt;
    0x18ee87fa0 &amp;lt;+128&amp;gt;: add    x21, x21, #0x1
    0x18ee87fa4 &amp;lt;+132&amp;gt;: cmp    x20, x21
    0x18ee87fa8 &amp;lt;+136&amp;gt;: b.ne   0x18ee87f90    ; &amp;lt;+112&amp;gt;
    0x18ee87fac &amp;lt;+140&amp;gt;: mov    x0, x19
    0x18ee87fb0 &amp;lt;+144&amp;gt;: bl     0x18fc7b220    ; objc_msgSend$UTF8String
    0x18ee87fb4 &amp;lt;+148&amp;gt;: bl     0x18f88322c    ; symbol stub for: sel_registerName
    0x18ee87fb8 &amp;lt;+152&amp;gt;: b      0x18ee87fc0    ; &amp;lt;+160&amp;gt;
    0x18ee87fbc &amp;lt;+156&amp;gt;: mov    x0, #0x0 ; =0 
    0x18ee87fc0 &amp;lt;+160&amp;gt;: ldur   x8, [x29, #-0x28]
    0x18ee87fc4 &amp;lt;+164&amp;gt;: adrp   x9, 402965
    0x18ee87fc8 &amp;lt;+168&amp;gt;: ldr    x9, [x9, #0x388]
    0x18ee87fcc &amp;lt;+172&amp;gt;: ldr    x9, [x9]
    0x18ee87fd0 &amp;lt;+176&amp;gt;: cmp    x9, x8
    0x18ee87fd4 &amp;lt;+180&amp;gt;: b.ne   0x18ee87fec    ; &amp;lt;+204&amp;gt;
    0x18ee87fd8 &amp;lt;+184&amp;gt;: add    sp, sp, #0x3f0
    0x18ee87fdc &amp;lt;+188&amp;gt;: ldp    x29, x30, [sp, #0x20]
    0x18ee87fe0 &amp;lt;+192&amp;gt;: ldp    x20, x19, [sp, #0x10]
    0x18ee87fe4 &amp;lt;+196&amp;gt;: ldp    x22, x21, [sp], #0x30
    0x18ee87fe8 &amp;lt;+200&amp;gt;: retab  
    0x18ee87fec &amp;lt;+204&amp;gt;: bl     0x18f8811cc    ; symbol stub for: __stack_chk_fail
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;NSSelectorFromString 反汇编路径解读&lt;/h3&gt;
&lt;p&gt;前半部分很简单，是把传入的&lt;code&gt;NSString&lt;/code&gt;转为&lt;code&gt;CString&lt;/code&gt;（实际是一个char数组）。先调用&lt;code&gt;NSString&lt;/code&gt;的&lt;code&gt;getCString:maxLength:encoding:&lt;/code&gt; ，如果失败就尝试调用&lt;code&gt;strlen&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个&lt;code&gt;CString&lt;/code&gt;会传递给&lt;code&gt;sel_registerName&lt;/code&gt; 。正好 &lt;code&gt;sel_registerName&lt;/code&gt; 是开源的，我们看看里面具体逻辑：&lt;/p&gt;
&lt;h3&gt;sel_registerName&lt;/h3&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-sel.mm&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc-sel.mm

static objc::ExplicitInitDenseSet&amp;lt;const char *&amp;gt; namedSelectors;

...

SEL sel_registerName(const char *name) {
    return __sel_registerName(name, 1, 1);     // YES lock, YES copy
}

...

static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) lockdebug::assert_unlocked(&amp;amp;selLock.get());
    else            lockdebug::assert_locked(&amp;amp;selLock.get());

    if (!name) return (SEL)0;

    result = _sel_searchBuiltins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
	auto it = namedSelectors.get().insert(name);
	if (it.second) {
		// No match. Insert.
		*it.first = (const char *)sel_alloc(name, copy);
	}
	return (SEL)*it.first;
}

...

SEL _sel_searchBuiltins(const char *name)
{
#if SUPPORT_PREOPT
  if (SEL result = (SEL)_dyld_get_objc_selector(name))
    return result;
#endif
    return nil;
}

...

static SEL sel_alloc(const char *name, bool copy)
{
    lockdebug::assert_locked(&amp;amp;selLock.get());
    return (SEL)(copy ? strdupIfMutable(name) : name);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑非常清晰也很简单，主要分为以下几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;判断该selector有没有加载过。实际是调用 &lt;code&gt;dyld&lt;/code&gt; 的 &lt;code&gt;_dyld_get_objc_selector&lt;/code&gt; 尝试获取selector表，有则直接返回。&lt;/li&gt;
&lt;li&gt;尝试往 &lt;code&gt;namedSelectors&lt;/code&gt; 插入selector。&lt;code&gt;namedSelectors&lt;/code&gt; 是一个 &lt;code&gt;ExplicitInitDenseSetDenseSet&amp;lt;const char *&amp;gt;&lt;/code&gt; ，可以简单理解为一个Set，在插入时会比对字符串的&lt;strong&gt;值&lt;/strong&gt;。如果存在相同的值就直接返回，否则新插入一条selector。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为什么插入的时候会比较字符串内容呢？&lt;/p&gt;
&lt;h3&gt;DenseSet&lt;/h3&gt;
&lt;p&gt;实际上 &lt;code&gt;ExplicitInitDenseSetDenseSet&amp;lt;const char *&amp;gt;&lt;/code&gt; 有以下继承关系：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ExplicitInitDenseSetDenseSet&amp;lt;const char *&amp;gt;&lt;/code&gt; ←  &lt;code&gt;ExplicitInit&amp;lt;DenseSet&amp;lt;const char *&amp;gt;&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ExplicitInit&lt;/code&gt; 可暂时不看，是方便初始化用的。我们先来看看 &lt;code&gt;DenseSet&amp;lt;const char *&amp;gt;&lt;/code&gt; ，实际实现是&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/llvm-DenseSet.h#L255&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename ValueT, typename ValueInfoT = DenseMapInfo&amp;lt;ValueT&amp;gt;&amp;gt;
class DenseSet : public detail::DenseSetImpl&amp;lt;
                     ValueT, DenseMap&amp;lt;ValueT, detail::DenseSetEmpty,
                                      DenseMapValueInfo&amp;lt;detail::DenseSetEmpty&amp;gt;,
                                      ValueInfoT, detail::DenseSetPair&amp;lt;ValueT&amp;gt;&amp;gt;,
                     ValueInfoT&amp;gt; {
  using BaseT =
      detail::DenseSetImpl&amp;lt;ValueT,
                           DenseMap&amp;lt;ValueT, detail::DenseSetEmpty,
                                    DenseMapValueInfo&amp;lt;detail::DenseSetEmpty&amp;gt;,
                                    ValueInfoT, detail::DenseSetPair&amp;lt;ValueT&amp;gt;&amp;gt;,
                           ValueInfoT&amp;gt;;

public:
  using BaseT::BaseT;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的&lt;code&gt;ValueT = const char *&lt;/code&gt; ，那么 &lt;code&gt;DenseSetImpl&lt;/code&gt; 是什么呢&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// Base class for DenseSet and DenseSmallSet.
///
/// MapTy should be either
///
///   DenseMap&amp;lt;ValueT, detail::DenseSetEmpty,
///            DenseMapValueInfo&amp;lt;detail::DenseSetEmpty&amp;gt;,
///            ValueInfoT, detail::DenseSetPair&amp;lt;ValueT&amp;gt;&amp;gt;
///
/// or the equivalent SmallDenseMap type.  ValueInfoT must implement the
/// DenseMapInfo &quot;concept&quot;.
template &amp;lt;typename ValueT, typename MapTy, typename ValueInfoT&amp;gt;
class DenseSetImpl {
  static_assert(sizeof(typename MapTy::value_type) == sizeof(ValueT),
                &quot;DenseMap buckets unexpectedly large!&quot;);
  MapTy TheMap;

  template &amp;lt;typename T&amp;gt;
  using const_arg_type_t = typename const_pointer_or_const_ref&amp;lt;T&amp;gt;::type;

public:
  using key_type = ValueT;
  using value_type = ValueT;
  using size_type = unsigned;
  
...

std::pair&amp;lt;iterator, bool&amp;gt; insert(const ValueT &amp;amp;V) {
    detail::DenseSetEmpty Empty;
    return TheMap.try_emplace(V, Empty);
  }

  std::pair&amp;lt;iterator, bool&amp;gt; insert(ValueT &amp;amp;&amp;amp;V) {
    detail::DenseSetEmpty Empty;
    return TheMap.try_emplace(std::move(V), Empty);
  }
...
  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，&lt;code&gt;DenseSet&lt;/code&gt; 实际是一个Key为实际值（这里是&lt;code&gt;const char *&lt;/code&gt;），Value为空的 &lt;code&gt;DenseMap&lt;/code&gt; 。&lt;code&gt;DenseMap&lt;/code&gt; 会通过template类型  调用 &lt;code&gt;DenseSet&lt;/code&gt; 的 &lt;code&gt;insert&lt;/code&gt; ，实际是调用 &lt;code&gt;DenseMap&lt;/code&gt; 的 &lt;code&gt;try_emplace&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;DenseMap&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DenseMap&lt;/code&gt; 的原型如下：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/llvm-DenseMap.h#L102&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename KeyT, typename ValueT,
          typename ValueInfoT = DenseMapValueInfo&amp;lt;ValueT&amp;gt;,
          typename KeyInfoT = DenseMapInfo&amp;lt;KeyT&amp;gt;,
          typename BucketT = detail::DenseMapPair&amp;lt;KeyT, ValueT&amp;gt;&amp;gt;
class DenseMap : public DenseMapBase&amp;lt;DenseMap&amp;lt;KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT&amp;gt;,
                                     KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT&amp;gt; {
  friend class DenseMapBase&amp;lt;DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT&amp;gt;;
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构有点复杂，但可以注意到，&lt;code&gt;DenseMap&lt;/code&gt; 是通过 &lt;code&gt;KeyInfoT = DenseMapInfo&amp;lt;KeyT&amp;gt;&lt;/code&gt; 来获取Key的信息的，比如Key的哈希值、两个Key是否相等等。而 &lt;code&gt;DenseMapInfo&amp;lt;const char *&amp;gt;&lt;/code&gt; 是什么呢？&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/llvm-DenseMapInfo.h#L67&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// llvm-DenserMapInfo.h

// Provide DenseMapInfo for cstrings.
template&amp;lt;&amp;gt; struct DenseMapInfo&amp;lt;const char*&amp;gt; {
  static inline const char* getEmptyKey() { 
    return reinterpret_cast&amp;lt;const char *&amp;gt;((intptr_t)-1); 
  }
  static inline const char* getTombstoneKey() { 
    return reinterpret_cast&amp;lt;const char *&amp;gt;((intptr_t)-2); 
  }
  static unsigned getHashValue(const char* const &amp;amp;Val) { 
    return _objc_strhash(Val); 
  }
  static bool isEqual(const char* const &amp;amp;LHS, const char* const &amp;amp;RHS) {
    if (LHS == RHS) {
      return true;
    }
    if (LHS == getEmptyKey() || RHS == getEmptyKey()) {
      return false;
    }
    if (LHS == getTombstoneKey() || RHS == getTombstoneKey()) {
      return false;
    }
    return 0 == strcmp(LHS, RHS);
  }
};

// objc-private.h
static __inline uint32_t _objc_strhash(const char *s) {
    uint32_t hash = 0;
    for (;;) {
    int a = *s++;
    if (0 == a) break;
    hash += (hash &amp;lt;&amp;lt; 8) + a;
    }
    return hash;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;getHashValue&lt;/code&gt; 和 &lt;code&gt;isEqual&lt;/code&gt; 两个方法，说明 &lt;code&gt;DenseSet&amp;lt;const char *&amp;gt;&lt;/code&gt; 是通过字符串本身计算哈希值。所以有两个值相同，但地址不同的字符串存入 &lt;code&gt;DenseSet&amp;lt;const char*&amp;gt;&lt;/code&gt; ，最后只会存一份字符串（是否共享字符串内存取决于 &lt;code&gt;copy&lt;/code&gt; 参数与是否 &lt;code&gt;strdupIfMutable&lt;/code&gt;）。这也能够说明为什么同一个字符串只能对应一个selector。&lt;/p&gt;
&lt;h3&gt;select进入namedSelectors的时机&lt;/h3&gt;
&lt;p&gt;selector &lt;code&gt;sayHi&lt;/code&gt;是什么时候插入进&lt;code&gt;namedSelectors&lt;/code&gt; 的？ &lt;code&gt;namedSelectors&lt;/code&gt; 是什么时候初始化的？&lt;/p&gt;
&lt;p&gt;通过观察，我们可以看到存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map_images → map_images_nolock → sel_init
                         ↓
                     _read_images → sel_registerNameNoLock → namedSelectors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条调用链。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-sel.mm#L31&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/***********************************************************************
* sel_init
* Initialize selector tables and register selectors used internally.
**********************************************************************/
void sel_init(size_t selrefCount)
{
#if SUPPORT_PREOPT
    if (PrintPreopt) {
        _objc_inform(&quot;PREOPTIMIZATION: using dyld selector opt&quot;);
    }
#endif

  namedSelectors.init((unsigned)selrefCount);

    // Register selectors used by libobjc

    mutex_locker_t lock(selLock);

    SEL_cxx_construct = sel_registerNameNoLock(&quot;.cxx_construct&quot;, NO);
    SEL_cxx_destruct = sel_registerNameNoLock(&quot;.cxx_destruct&quot;, NO);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sel_init&lt;/code&gt; 是谁调用的？&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-os.mm#L455&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc-os.mm

void 
map_images_nolock(unsigned mhCount, const struct _dyld_objc_notify_mapped_info infos[],
                  bool *disabledClassROEnforcement,
                  _dyld_objc_mark_image_mutable makeImageMutable)
{
	...
	if (firstTime) {
        sel_init(selrefCount);
        ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而&lt;code&gt;map_images_nolock&lt;/code&gt;是谁调用的呢？&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-runtime-new.mm#L3547&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc-runtime-new.mm

/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const struct _dyld_objc_notify_mapped_info infos[],
           _dyld_objc_mark_image_mutable makeImageMutable)
{
    bool takeEnforcementDisableFault;

    {
        mutex_locker_t lock(runtimeLock);
        map_images_nolock(count, infos, &amp;amp;takeEnforcementDisableFault, makeImageMutable);
    }

    if (takeEnforcementDisableFault) {
        if (DebugClassRXSigning == Fatal)
            _objc_fatal(&quot;class_rx signing mismatch&quot;);

#if TARGET_OS_IPHONE &amp;amp;&amp;amp; !TARGET_OS_SIMULATOR
        if (!DisableClassROFaults)
            _objc_fault(&quot;class_ro_t enforcement disabled&quot;);
#endif
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;map_images_nolock&lt;/code&gt; 由 &lt;code&gt;map_images&lt;/code&gt; 调用。上一章也提到，&lt;code&gt;map_images&lt;/code&gt; 在动态库载入时会被调用，所以在程序初始化时就能够完成 &lt;code&gt;namedSelectors&lt;/code&gt; 的初始化。&lt;/p&gt;
&lt;p&gt;初始化的流程我们理解了，但是问题还没解决：映像 &lt;code&gt;__objc_selrefs&lt;/code&gt; 段里存的selector什么时候转到&lt;code&gt;namedSelectors&lt;/code&gt; 里呢？&lt;/p&gt;
&lt;p&gt;上面的代码可以看到，&lt;code&gt;map_images&lt;/code&gt; 会调用 &lt;code&gt;map_images_nolock&lt;/code&gt; 。而在 &lt;code&gt;map_images_nolock&lt;/code&gt; 里会调用一个函数 &lt;code&gt;_read_images&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void 
map_images_nolock(unsigned mhCount, const struct _dyld_objc_notify_mapped_info infos[],
                  bool *disabledClassROEnforcement,
                  _dyld_objc_mark_image_mutable makeImageMutable)
{
...
	    if (hCount &amp;gt; 0) {
	        _read_images(mappedInfos, hCount, totalClasses, unoptimizedTotalClasses, makeImageMutable);
	    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看看里面的作用&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-runtime-new.mm&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
// objc-runtime-new.mm

/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked
* list beginning with headerList.
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(mapped_image_info infosParam[], uint32_t hCount, int totalClasses, int unoptimizedTotalClasses,
                  _dyld_objc_mark_image_mutable makeImageMutable)
{
	...
 static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (auto&amp;amp; info : infos) {
            if (info.dyldObjCRefsOptimized()) continue;

            bool isBundle = info.hi-&amp;gt;isBundle();
            SEL *sels = info.hi-&amp;gt;selrefs(&amp;amp;count);
            UnfixedSelectors += count;
            for (i = 0; i &amp;lt; count; i++) {
                const char *name = sel_cname(sels[i]);
                SEL sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    // The infos array is reversed, but dyld expects the original index
                    const uint32_t infoIndex = (hCount - 1) - infos.index(&amp;amp;info);

                    makeImageMutable(infoIndex);
                    withMutableSharedCache(info.tproEnabled(), [&amp;amp;] {
                        sels[i] = sel;
                    });
                }
            }
        }
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里调用了 &lt;code&gt;sel_registerNameNoLock&lt;/code&gt; ，实际就是将映像里的selector一个个地存进&lt;code&gt;namedSelectors&lt;/code&gt; 里。所以能够说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@selector(...)&lt;/code&gt; 与 &lt;code&gt;NSSelectorFromString(...)&lt;/code&gt; 会得到同一个 selector&lt;/li&gt;
&lt;li&gt;因为 selector 已在加载期完成注册/驻留&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;发送消息&lt;/h2&gt;
&lt;p&gt;发送消息部分，就是Objective-C的精髓。&lt;/p&gt;
&lt;h3&gt;猜测&lt;/h3&gt;
&lt;p&gt;已知，对象的isa里存着&lt;code&gt;baseMethods&lt;/code&gt; ，实际是一个数组，每一项是&lt;code&gt;{SEL &amp;amp; type（传参类型） &amp;amp; 跳转地址 }&lt;/code&gt;。所以我们可以猜测：&lt;/p&gt;
&lt;p&gt;每次调用&lt;code&gt;objc_msgSend&lt;/code&gt;，都会去对象的 &lt;code&gt;baseMethods&lt;/code&gt; 里查跳转地址并执行跳转。&lt;/p&gt;
&lt;p&gt;但是 &lt;code&gt;baseMethods&lt;/code&gt; 是一个数组，每调一次 &lt;code&gt;objc_msgSend&lt;/code&gt; 都需要去遍历数组查找实现，能不能把 &lt;code&gt;baseMethods&lt;/code&gt; 存在一个map里，这样查找的时间复杂度就下来了？所以：&lt;/p&gt;
&lt;p&gt;每个isa里存在一个map缓存，key为selector。调用 &lt;code&gt;objc_msgSend&lt;/code&gt; 时会先去这个缓存查找，如果没找到再去 &lt;code&gt;baseMethods&lt;/code&gt; 里查找。&lt;/p&gt;
&lt;p&gt;确实，Apple也是这么做的。&lt;/p&gt;
&lt;h3&gt;深入汇编&lt;/h3&gt;
&lt;p&gt;因为&lt;code&gt;objc_msgSend&lt;/code&gt; 的调用频次很高。Apple为了提升效率，&lt;strong&gt;特意&lt;/strong&gt;用汇编来实现，属实良心。&lt;/p&gt;
&lt;p&gt;不同CPU架构下的汇编指令也会不同。Apple甚至对不同的CPU做了指令差异处理，太良心了。&lt;/p&gt;
&lt;p&gt;不过，核心逻辑是大致相同的。我们这里以arm64架构为例，先来看看 &lt;code&gt;objc_msgSend&lt;/code&gt; 的具体实现：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/Messengers.subproj/objc-msg-arm64.s&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	MSG_ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p14, [x0]		// p14 = raw isa
	GetClassFromIsa_p16 p14, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;objc_msgSend&lt;/code&gt; 的关键步骤可以概括为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;nil / tagged pointer 检查&lt;/li&gt;
&lt;li&gt;从对象取出 isa&lt;/li&gt;
&lt;li&gt;在 class cache 中查找 selector → IMP&lt;/li&gt;
&lt;li&gt;命中则跳转；未命中则进入慢路径&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我让G老师写了一段伪代码，方便理解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IMP objc_msgSend(id receiver, SEL sel, ...) {
    if (receiver == nil) return 0;

    cls = decode_isa(receiver-&amp;gt;isa);
    imp = cache_lookup(cls, sel);

    if (imp == NULL) {
        imp = __objc_msgSend_uncached(cls, sel);
    }

    return imp(receiver, sel, ...);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;LNilOrTagged&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LNilOrTagged&lt;/code&gt; 是什么呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果传入的对象为nil，就直接跳到&lt;code&gt;LReturnZero&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;否则解析Tagged Pointer拿到class 指针，存在&lt;code&gt;x16&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里也能够说明，为什么Objective-C里能够对空指针发消息。&lt;/p&gt;
&lt;p&gt;拿到了&lt;code&gt;isa&lt;/code&gt;，就到了缓存查找部分：&lt;code&gt;CacheLookup&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;CacheLookup&lt;/h3&gt;
&lt;p&gt;缓存是什么？回顾上一章，我们复习一下&lt;code&gt;isa&lt;/code&gt; 的结构：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-runtime-new.h#L2635&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct objc_class : objc_object {
  objc_class(const objc_class&amp;amp;) = delete;
  objc_class(objc_class&amp;amp;&amp;amp;) = delete;
  void operator=(const objc_class&amp;amp;) = delete;
  void operator=(objc_class&amp;amp;&amp;amp;) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cache_t&lt;/code&gt;是什么？&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-runtime-new.h#L337&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct cache_t {
private:
    explicit_atomic&amp;lt;uintptr_t&amp;gt; _bucketsAndMaybeMask;
    union {
        // Note: _flags on ARM64 needs to line up with the unused bits of
        // _originalPreoptCache because we access some flags (specifically
        // FAST_CACHE_HAS_DEFAULT_CORE and FAST_CACHE_HAS_DEFAULT_AWZ) on
        // unrealized classes with the assumption that they will start out
        // as 0.
        struct {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED &amp;amp;&amp;amp; !__LP64__
            // Outlined cache mask storage, 32-bit, we have mask and occupied.
            explicit_atomic&amp;lt;mask_t&amp;gt;    _mask;
            uint16_t                   _occupied;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED &amp;amp;&amp;amp; __LP64__
            // Outlined cache mask storage, 64-bit, we have mask, occupied, flags.
            explicit_atomic&amp;lt;mask_t&amp;gt;    _mask;
            uint16_t                   _occupied;
            uint16_t                   _flags;
#   define CACHE_T_HAS_FLAGS 1
#elif __LP64__
            // Inline cache mask storage, 64-bit, we have occupied, flags, and
            // empty space to line up flags with originalPreoptCache.
            //
            // Note: the assembly code for objc_release_xN knows about the
            // location of _flags and the
            // FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION flag within. Any changes
            // must be applied there as well.
            uint32_t                   _disguisedPreoptCacheSignature;
            uint16_t                   _occupied;
            uint16_t                   _flags;
#   define CACHE_T_HAS_FLAGS 1
#else
            // Inline cache mask storage, 32-bit, we have occupied, flags.
            uint16_t                   _occupied;
            uint16_t                   _flags;
#   define CACHE_T_HAS_FLAGS 1
#endif

        };
        explicit_atomic&amp;lt;preopt_cache_t *, PTRAUTH_STR(originalPreoptCache, ptrauth_key_process_independent_data)&amp;gt; _originalPreoptCache;
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_bucketsAndMaybeMask&lt;/code&gt; 是什么呢？我们上一章讲过，指针被mask是为了安全。其实，这里buckets的实际内存布局是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ bucket0 ][ bucket1 ][ bucket2 ][ bucket3 ] ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bucket&lt;/code&gt;是什么呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic&amp;lt;uintptr_t&amp;gt; _imp;
    explicit_atomic&amp;lt;SEL&amp;gt; _sel;
#else
    explicit_atomic&amp;lt;SEL&amp;gt; _sel;
    explicit_atomic&amp;lt;uintptr_t&amp;gt; _imp;
#endif

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说白了就是imp+sel的紧凑结构。&lt;/p&gt;
&lt;p&gt;现在先来一个思考题，已知isa的地址，怎么获取bucket的基地？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1️⃣ decode isa（mask 取 class pointer）
Class cls = raw_isa &amp;amp; ISA_MASK;

// 2️⃣ cache_t 在 class 内的偏移
cache_t *cache = (cache_t *)((uint8_t*)cls + CACHE_OFFSET);

// 3️⃣ buckets 在 cache_t 内的偏移
bucket_t *buckets = *(bucket_t **)((uint8_t*)cache + BUCKETS_OFFSET);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CACHE_OFFSET&lt;/code&gt; = 0x10&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BUCKETS_OFFSET&lt;/code&gt; = 0x00&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;我们开始看&lt;code&gt;CacheLookUp&lt;/code&gt;的汇编代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP &amp;lt;function&amp;gt; MissLabelDynamic MissLabelConstant
 *
 * MissLabelConstant is only used for the GETIMP variant.
 *
 * Locate the implementation for a selector in a class method cache.
 *
 * When this is used in a function that doesn&apos;t hold the runtime lock,
 * this represents the critical section that may access dead memory.
 * If the kernel causes one of these functions to go down the recovery
 * path, we pretend the lookup failed by jumping the JumpMiss branch.
 *
 * Takes:
 *	 x1 = selector
 *	 x16 = class to be searched
 *
 * Kills:
 * 	 x9,x10,x11,x12,x13,x15,x17
 *
 * Untouched:
 * 	 x14
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *                  In LOOKUP mode, the two low bits are set to 0x3
 *                  if we hit a constant cache (used in objc_trace)
 *          (not found) jumps to LCacheMiss
 *                  with x15 = class
 *                  For constant caches in LOOKUP mode, the low bit
 *                  of x16 is set to 0x1 to indicate we had to fallback.
 *          In addition, when LCacheMiss is __objc_msgSend_uncached or
 *          __objc_msgLookup_uncached, 0x2 will be set in x16
 *          to remember we took the slowpath.
 *          So the two low bits of x16 on exit mean:
 *            0: dynamic hit
 *            1: fallback to the parent class, when there is a preoptimized cache
 *            2: slowpath
 *            3: preoptimized cache hit
 *
 ********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f					// don&apos;t ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16, x17	// authenticate imp and re-sign as IMP
9:	ret						// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don&apos;t care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16, x10	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we&apos;re past the LLookupStart\Function label we may have
	//   loaded an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we&apos;re past LLookupEnd\Function,
	//   then our PC will be reset to LLookupRecover\Function which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//

	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd &amp;amp; mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd &amp;gt;&amp;gt; 7)) &amp;amp; mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd &amp;amp; mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11			// p11 = mask = 0xffff &amp;gt;&amp;gt; p11
	and	p12, p1, p11			// x12 = _cmd &amp;amp; mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd &amp;amp; mask) &amp;lt;&amp;lt; (1+PTRSHIFT))

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket &amp;gt;= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd &amp;amp; mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask &amp;lt;&amp;lt; 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask &amp;lt;&amp;lt; 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask &amp;lt;&amp;lt; 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &amp;amp;&amp;amp;
	ccmp	p13, p12, #0, ne		//     bucket &amp;gt; first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) &amp;gt;&amp;gt; hash_shift &amp;amp; hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// &amp;gt;&amp;gt;= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff &amp;gt;&amp;gt; mask_bits)
	and	x9, x9, x11			// &amp;amp;= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// &amp;gt;&amp;gt;= shift
	and	x9, x9, x11, LSR #53		// &amp;amp;=  mask
#endif

	// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
	// keep the remaining 38 bits for the IMP offset, which may need to reach
	// across the shared cache. This offset needs to be shifted &amp;lt;&amp;lt; 2. We did this
	// to give it even more reach, given the alignment of source (the class data)
	// and destination (the IMP)
	ldr	x17, [x10, x9, LSL #3]		// x17 == (sel_offs &amp;lt;&amp;lt; 38) | imp_offs
	cmp	x12, x17, LSR #38

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] &amp;lt;&amp;lt; 2
	sub	x0, x16, x17        		// imp = isa - imp_offs
	SignAsImp x0, x17
	ret
.else
	b.ne	5f				        // cache miss
	sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] &amp;lt;&amp;lt; 2
	sub x17, x16, x17               // imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17, x10
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldur	x9, [x10, #-16]			// offset -16 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码非常多。其实，这个代码一共分成三个阶段：&lt;/p&gt;
&lt;h4&gt;准备数据&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd &amp;amp; mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd &amp;gt;&amp;gt; 7)) &amp;amp; mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd &amp;amp; mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11			// p11 = mask = 0xffff &amp;gt;&amp;gt; p11
	and	p12, p1, p11			// x12 = _cmd &amp;amp; mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，外部固定会传入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x1&lt;/code&gt;: sel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x16&lt;/code&gt;: isa&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接着，会从&lt;code&gt;isa+${#CACHE}&lt;/code&gt;取值并存到x10里。而CACHE是什么呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define CACHE            (2 * __SIZEOF_POINTER__)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，x10实际是&lt;code&gt;buckets&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再者，x12 = _cmd &amp;amp; mask。mask是什么？mask=缓存的大小-1。所以，x12就是sel计算后的缓存索引位置。用hashmap的话来讲，是哈希值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[bucket0][bucket1][bucket2][bucket3][bucket4][bucket5][bucket6][bucket7]
                                   ^
                                   │
                                  x12   ← (_cmd &amp;amp; mask) 得到的 index
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;从中往前遍历&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd &amp;amp; mask) &amp;lt;&amp;lt; (1+PTRSHIFT))

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket &amp;gt;= buckets)
	b.hs	1b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从bucket[x13]遍历到bucket[0]。如果找到&lt;code&gt;sel==cmd&lt;/code&gt;，则跳转到&lt;code&gt;CacheHit&lt;/code&gt; （传入的参数），否则&lt;code&gt;x13--&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;该轮遍历结束后，x13位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[bucket0][bucket1][bucket2][bucket3][bucket4][bucket5][bucket6][bucket7]
     ^
     │
    x13
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;从后往中遍历&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd &amp;amp; mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask &amp;lt;&amp;lt; 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask &amp;lt;&amp;lt; 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask &amp;lt;&amp;lt; 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &amp;amp;&amp;amp;
	ccmp	p13, p12, #0, ne		//     bucket &amp;gt; first_probed)
	b.hi	4b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，x13指向buckets最后一个元素：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[bucket0][bucket1][bucket2][bucket3][bucket4][bucket5][bucket6][bucket7]
                                                                ^
                                                                │
                                                               x13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后向前遍历，查找&lt;code&gt;sel==_cmd&lt;/code&gt;。有就跳到&lt;code&gt;CacheHit&lt;/code&gt;，否则&lt;code&gt;x13--&lt;/code&gt;，直到&lt;code&gt;x13==x12&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;结果&lt;/h4&gt;
&lt;p&gt;如果找不到，就跳到&lt;code&gt;MissLabelDynamic&lt;/code&gt;。对于&lt;code&gt;objc_msgSend&lt;/code&gt;来说，是&lt;code&gt;objc_msgSend_uncached&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而如果跳转成功呢？&lt;code&gt;CacheHit&lt;/code&gt;会根据传入的参数分为三种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f					// don&apos;t ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16, x17	// authenticate imp and re-sign as IMP
9:	ret						// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don&apos;t care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16, x10	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，&lt;code&gt;objc_msgSend&lt;/code&gt;调用的是&lt;code&gt;NORMAL&lt;/code&gt;类型的，实际是跳转到&lt;code&gt;TailCallCachedImp&lt;/code&gt;。&lt;code&gt;TailCallCachedImp&lt;/code&gt; 的作用，实际是传入imp、SEL并执行跳转。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$0, $0, $3
.ifndef LTailCallCachedImpIndirectBranch
LTailCallCachedImpIndirectBranch:
.endif
	br	$0
.endmacro
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;objc_msgSend_uncached&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.macro TailCallFunctionPointer
	// $0 = function pointer value
	br	$0
.endmacro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑很简单，就是走到&lt;code&gt;MethodTableLookup&lt;/code&gt; 这个宏，然后再跳到&lt;code&gt;x17&lt;/code&gt;的地址上。&lt;/p&gt;
&lt;h4&gt;lookUpImpOrForward&lt;/h4&gt;
&lt;p&gt;这个方法是用C写的：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/objc4/blob/fb265098298302243cd7eeaa1f63f0ba7786dd9a/runtime/objc-runtime-new.mm#L4787&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    lockdebug::assert_unlocked(&amp;amp;runtimeLock.get());

    if (slowpath(!cls-&amp;gt;isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn&apos;t realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don&apos;t want people to be able to craft a binary blob that looks like
    // a class but really isn&apos;t one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    checkIsKnownClass(cls);

    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior &amp;amp; LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    lockdebug::assert_locked(&amp;amp;runtimeLock.get());
    curClass = cls;

    // The code used to lookup the class&apos;s cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    // Has this class been disabled? Act like a message to nil.
    if (!cls || !cls-&amp;gt;ISA()) {
#if __arm64__
        imp = _objc_returnNil;
        goto done;
#elif __x86_64
        if (behavior &amp;amp; LOOKUP_FPRET)
            imp = _objc_msgNil_fpret;
        else if (behavior &amp;amp; LOOKUP_FP2RET)
            imp = _objc_msgNil_fp2ret;
        else
            imp = _objc_msgNil;

        // We can&apos;t cache these on x86, in case some other caller tries sending
        // this selector with a different return type. If we con&apos;t cache then we
        // always come back here, and always choose the correct IMP for the
        // caller&apos;s expected return type.
        behavior |= LOOKUP_NOCACHE;

        goto done;
#else
#error Don&apos;t know how to handle messages to disabled classes on this target.
#endif
    }

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass-&amp;gt;cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass-&amp;gt;cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth-&amp;gt;imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass-&amp;gt;getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn&apos;t help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal(&quot;Memory corruption in class list.&quot;);
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don&apos;t cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior &amp;amp; LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior &amp;amp; LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls-&amp;gt;cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls-&amp;gt;cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
#if CONFIG_USE_PREOPT_CACHES
 done_unlock:
#endif
    runtimeLock.unlock();
    if (slowpath((behavior &amp;amp; LOOKUP_NIL) &amp;amp;&amp;amp; imp == forward_imp)) {
        return nil;
    }
    return imp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实，可以分为三部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;类初始化。类信息可能还在只读区域里，需要把这些信息挪到可读可写的区域。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从类信息查找selector对应的函数指针IMP。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缓存SEL和IMP。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;类初始化&lt;/h4&gt;
&lt;p&gt;调用关系：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;realizeAndInitializeIfNeeded_locked&lt;/code&gt; -&amp;gt; &lt;code&gt;realizeClassMaybeSwiftAndLeaveLocked&lt;/code&gt; -&amp;gt; &lt;code&gt;realizeClassMaybeSwiftMaybeRelock&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/***********************************************************************
* realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked)
* Realize a class that might be a Swift class.
* Returns the real class structure for the class.
* Locking:
*   runtimeLock must be held on entry
*   runtimeLock may be dropped during execution
*   ...AndUnlock function leaves runtimeLock unlocked on exit
*   ...AndLeaveLocked re-acquires runtimeLock if it was dropped
* This complication avoids repeated lock transitions in some cases.
**********************************************************************/
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t&amp;amp; lock, bool leaveLocked)
{
    lockdebug::assert_locked(&amp;amp;lock);

    if (!cls-&amp;gt;isSwiftStable_ButAllowLegacyForNow()) {
        // Non-Swift class. Realize it now with the lock still held.
        // fixme wrong in the future for objc subclasses of swift classes
        cls = realizeClassWithoutSwift(cls, nil);
        if (!leaveLocked) lock.unlock();
    } else {
        // Swift class. We need to drop locks and call the Swift
        // runtime to initialize it.
        lock.unlock();
        cls = realizeSwiftClass(cls);
        ASSERT(cls-&amp;gt;isRealized());    // callback must have provoked realization
        if (leaveLocked) lock.lock();
    }

    return cls;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里分为两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对objc类初始化&lt;/li&gt;
&lt;li&gt;对swift类初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;对objc类初始化&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    lockdebug::assert_locked(&amp;amp;runtimeLock.get());

    class_rw_t *rw;
    Class supercls;
    Class metacls;

    if (!cls) return nil;
    if (cls-&amp;gt;isRealized()) {
        validateAlreadyRealizedClass(cls);
        return cls;
    }
    ASSERT(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    auto ro = cls-&amp;gt;safe_ro();
    auto isMeta = ro-&amp;gt;flags &amp;amp; RO_META;
    if (ro-&amp;gt;flags &amp;amp; RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls-&amp;gt;data();
        ro = cls-&amp;gt;data()-&amp;gt;ro();
        ASSERT(!isMeta);
        cls-&amp;gt;changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc&amp;lt;class_rw_t&amp;gt;();
        rw-&amp;gt;set_ro(ro);
        rw-&amp;gt;flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls-&amp;gt;setData(rw);
    }

    cls-&amp;gt;cache.initializeToEmptyOrPreoptimizedInDisguise();

#if FAST_CACHE_META
    if (isMeta) cls-&amp;gt;cache.setBit(FAST_CACHE_META);
#endif

    // Choose an index for this class.
    // Sets cls-&amp;gt;instancesRequireRawIsa if indexes no more indexes are available
    cls-&amp;gt;chooseClassArrayIndex();

    if (PrintConnecting) {
        _objc_inform(&quot;CLASS: realizing class &apos;%s&apos;%s %p %p #%u %s%s&quot;,
                     cls-&amp;gt;nameForLogging(), isMeta ? &quot; (meta)&quot; : &quot;&quot;,
                     (void*)cls, ro, cls-&amp;gt;classArrayIndex(),
                     cls-&amp;gt;isSwiftStable() ? &quot;(swift)&quot; : &quot;&quot;,
                     cls-&amp;gt;isSwiftLegacy() ? &quot;(pre-stable swift)&quot; : &quot;&quot;);
    }

    // Realize superclass and metaclass, if they aren&apos;t already.
    // This needs to be done after RW_REALIZED is set above, for root classes.
    // This needs to be done after class index is chosen, for root metaclasses.
    // This assumes that none of those classes have Swift contents,
    //   or that Swift&apos;s initializers have already been called.
    //   fixme that assumption will be wrong if we add support
    //   for ObjC subclasses of Swift classes.
    supercls = realizeClassWithoutSwift(remapClass(cls-&amp;gt;getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls-&amp;gt;ISA()), nil);

    // If there&apos;s no superclass and this is not a root class, then we have a
    // missing weak superclass. Disable the class and return.
    if (!supercls &amp;amp;&amp;amp; !(cls-&amp;gt;safe_ro()-&amp;gt;flags &amp;amp; RO_ROOT)) {
        if (PrintConnecting)
            _objc_inform(&quot;CLASS: &apos;%s&apos;%s %p has missing weak superclass, disabling.&quot;,
                         cls-&amp;gt;nameForLogging(), isMeta ? &quot; (meta)&quot; : &quot;&quot;, (void *)cls);
        addRemappedClass(cls, nil);

        // Set the metaclass to nil to signal that this class is disabled.
        // Root classes have a nil superclass, but all (non-disabled) classes
        // have a non-nil isa pointer, so this can be used as a quick check for
        // disabled classes.
        cls-&amp;gt;initIsa(nil);

        return nil;
    }

#if SUPPORT_NONPOINTER_ISA
    if (isMeta) {
        // Metaclasses do not need any features from non pointer ISA
        // This allows for a faspath for classes in objc_retain/objc_release.
        cls-&amp;gt;setInstancesRequireRawIsa();
    } else {
        // Disable non-pointer isa for some classes and/or platforms.
        // Set instancesRequireRawIsa.
        bool instancesRequireRawIsa = cls-&amp;gt;instancesRequireRawIsa();
        bool rawIsaIsInherited = false;
        static bool hackedDispatch = false;
        const char *name;

        if (DisableNonpointerIsa) {
            // Non-pointer isa disabled by environment or app SDK version
            instancesRequireRawIsa = true;
        }
        else if (!hackedDispatch
                 &amp;amp;&amp;amp; (name = ro-&amp;gt;getName()) // Yes, we mean to assign here
                 &amp;amp;&amp;amp; 0 == strcmp(name, &quot;OS_object&quot;))
        {
            // hack for libdispatch et al - isa also acts as vtable pointer
            hackedDispatch = true;
            instancesRequireRawIsa = true;
        }
        else if (supercls  &amp;amp;&amp;amp;  supercls-&amp;gt;getSuperclass()  &amp;amp;&amp;amp;
                 supercls-&amp;gt;instancesRequireRawIsa())
        {
            // This is also propagated by addSubclass()
            // but nonpointer isa setup needs it earlier.
            // Special case: instancesRequireRawIsa does not propagate
            // from root class to root metaclass
            instancesRequireRawIsa = true;
            rawIsaIsInherited = true;
        }

        if (instancesRequireRawIsa) {
            cls-&amp;gt;setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
        }
    }
// SUPPORT_NONPOINTER_ISA
#endif

    // Update superclass and metaclass in case of remapping
    cls-&amp;gt;setSuperclass(supercls);
    cls-&amp;gt;initClassIsa(metacls);

    // Reconcile instance variable offsets / layout.
    // This may reallocate class_ro_t, updating our ro variable.
    if (supercls  &amp;amp;&amp;amp;  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Set fastInstanceSize if it wasn&apos;t set already.
    cls-&amp;gt;setInstanceSize(ro-&amp;gt;instanceSize);

    // Copy some flags from ro to rw
    if (ro-&amp;gt;flags &amp;amp; RO_HAS_CXX_STRUCTORS) {
        cls-&amp;gt;setHasCxxDtor();
        if (! (ro-&amp;gt;flags &amp;amp; RO_HAS_CXX_DTOR_ONLY)) {
            cls-&amp;gt;setHasCxxCtor();
        }
    }

    // Propagate the associated objects forbidden flag from ro or from
    // the superclass.
    if ((ro-&amp;gt;flags &amp;amp; RO_FORBIDS_ASSOCIATED_OBJECTS) ||
        (supercls &amp;amp;&amp;amp; supercls-&amp;gt;forbidsAssociatedObjects()))
    {
        rw-&amp;gt;flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;
    }

    // Connect this class to its superclass&apos;s subclass lists
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    // Attach categories
    methodizeClass(cls, previously);

    return cls;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体做了什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分配类的读写空间（&lt;code&gt;rwdata&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;初始化缓存&lt;/li&gt;
&lt;li&gt;初始化&lt;code&gt;superclass&lt;/code&gt;、&lt;code&gt;metaclass&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tagged Pointer优化&lt;/li&gt;
&lt;li&gt;rwdata初始化（&lt;code&gt;methodizeClass&lt;/code&gt;）、初始化方法表
&lt;ul&gt;
&lt;li&gt;安装 class 自己的方法&lt;/li&gt;
&lt;li&gt;处理 preoptimized method lists&lt;/li&gt;
&lt;li&gt;root metaclass 处理&lt;/li&gt;
&lt;li&gt;attach categories&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;对swift类初始化&lt;/h5&gt;
&lt;p&gt;实现逻辑在&lt;code&gt;realizeSwiftClass&lt;/code&gt;里&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/***********************************************************************
* realizeSwiftClass
* Performs first-time initialization on class cls,
* including allocating its read-write data,
* and any Swift-side initialization.
* Returns the real class structure for the class.
* Locking: acquires runtimeLock indirectly
**********************************************************************/
static Class realizeSwiftClass(Class cls)
{
    lockdebug::assert_unlocked(&amp;amp;runtimeLock.get());

    // Some assumptions:
    // * Metaclasses never have a Swift initializer.
    // * Root classes never have a Swift initializer.
    //   (These two together avoid initialization order problems at the root.)
    // * Unrealized non-Swift classes have no Swift ancestry.
    // * Unrealized Swift classes with no initializer have no ancestry that
    //   does have the initializer.
    //   (These two together mean we don&apos;t need to scan superclasses here
    //   and we don&apos;t need to worry about Swift superclasses inside
    //   realizeClassWithoutSwift()).

    // fixme some of these assumptions will be wrong
    // if we add support for ObjC sublasses of Swift classes.

#if DEBUG
    runtimeLock.lock();
    ASSERT(remapClass(cls) == cls);
    ASSERT(cls-&amp;gt;isSwiftStable_ButAllowLegacyForNow());
    ASSERT(!cls-&amp;gt;isMetaClassMaybeUnrealized());
    ASSERT(cls-&amp;gt;getSuperclass());
    runtimeLock.unlock();
#endif

    // Look for a Swift metadata initialization function
    // installed on the class. If it is present we call it.
    // That function in turn initializes the Swift metadata,
    // prepares the &quot;compiler-generated&quot; ObjC metadata if not
    // already present, and calls _objc_realizeSwiftClass() to finish
    // our own initialization.

    if (auto init = cls-&amp;gt;swiftMetadataInitializer()) {
        if (PrintConnecting) {
            _objc_inform(&quot;CLASS: calling Swift metadata initializer &quot;
                         &quot;for class &apos;%s&apos; (%p)&quot;, cls-&amp;gt;nameForLogging(), cls);
        }

        Class newcls = init(cls, nil);

        if (cls != newcls) {
            mutex_locker_t lock(runtimeLock);
            addRemappedClass(cls, newcls);
        }

        return newcls;
    }
    else {
        // No Swift-side initialization callback.
        // Perform our own realization directly.
        mutex_locker_t lock(runtimeLock);
        return realizeClassWithoutSwift(cls, nil);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上会调用swift runtime的初始化函数。&lt;/p&gt;
&lt;h4&gt;查找IMP&lt;/h4&gt;
&lt;p&gt;这一步的逻辑很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass-&amp;gt;cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass-&amp;gt;cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth-&amp;gt;imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass-&amp;gt;getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn&apos;t help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal(&quot;Memory corruption in class list.&quot;);
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don&apos;t cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;isConstantOptimizedCache&lt;/code&gt; -&amp;gt; dyld塞的prebuild  buckets。如果有，尝试去读取&lt;/li&gt;
&lt;li&gt;否则，调用&lt;code&gt;getMethodNoSuper_nolock&lt;/code&gt; ，去查方法表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/***********************************************************************
 * getMethodNoSuper_nolock
 * fixme
 * Locking: runtimeLock must be read- or write-locked by the caller
 **********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    lockdebug::assert_locked(&amp;amp;runtimeLock.get());

    ASSERT(cls-&amp;gt;isRealized());
    // fixme nil cls?
    // fixme nil sel?

    auto alternates = cls-&amp;gt;data()-&amp;gt;methodAlternates();

    if (auto *relativeList = alternates.relativeList)
        return getMethodFromRelativeList(relativeList, sel);

    if (alternates.list)
        return getMethodFromListArray(&amp;amp;alternates.list, 1, sel);

    if (auto *array = alternates.array) {
        auto listAlternates = array-&amp;gt;listAlternates();
        if (listAlternates.oneList)
            return getMethodFromListArray(&amp;amp;listAlternates.oneList, 1, sel);
        if (auto innerArray = listAlternates.array)
            return getMethodFromListArray(innerArray, listAlternates.arrayCount, sel);
        if (auto *relativeList = listAlternates.listList)
            return getMethodFromRelativeList(relativeList, sel);
    }

    return nil;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cls-&amp;gt;data()&lt;/code&gt; 对应的就是class的rwdata。而&lt;code&gt;methodAlternative&lt;/code&gt;的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Get the class&apos;s method lists without wrapping the different
    // representations in a method_array_t. This allows the caller to directly
    // access the underlying representations and have separate code for them,
    // rather than relying on the iterator abstraction being sufficiently
    // optimized. This exists for getMethodNoSuper_nolock to call, other callers
    // should be able to just use methods().
    ALWAYS_INLINE
    MethodListAlternates methodAlternates() const {
        MethodListAlternates result = {};
        auto v = get_ro_or_rwe();
        if (v.is&amp;lt;class_rw_ext_t *&amp;gt;()) {
            result.array = &amp;amp;v.get&amp;lt;class_rw_ext_t *&amp;gt;(&amp;amp;ro_or_rw_ext)-&amp;gt;methods;
        } else {
            auto &amp;amp;baseMethods = v.get&amp;lt;const class_ro_t *&amp;gt;(&amp;amp;ro_or_rw_ext)-&amp;gt;baseMethods;
            result.list = baseMethods.dyn_cast&amp;lt;method_list_t *&amp;gt;();
            result.relativeList = baseMethods.dyn_cast&amp;lt;relative_list_list_t&amp;lt;method_list_t&amp;gt; *&amp;gt;();
        }
        return result;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;功能为获取class的方法表&lt;/p&gt;
&lt;h4&gt;写入缓存&lt;/h4&gt;
&lt;p&gt;最后，将拿到的IMP写入到cache里，并返回IMP，为了方便下次做查询。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;done:
    if (fastpath((behavior &amp;amp; LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls-&amp;gt;cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls-&amp;gt;cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
#if CONFIG_USE_PREOPT_CACHES
 done_unlock:
#endif
    runtimeLock.unlock();
    if (slowpath((behavior &amp;amp; LOOKUP_NIL) &amp;amp;&amp;amp; imp == forward_imp)) {
        return nil;
    }
    return imp;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled.
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled &amp;amp;&amp;amp; implementer)) {
        bool cacheIt = logMessageSend(implementer-&amp;gt;isMetaClass(),
                                      cls-&amp;gt;nameForLogging(),
                                      implementer-&amp;gt;nameForLogging(),
                                      sel);
        if (!cacheIt) return;
    }
#endif
    if (slowpath(msgSendCacheMissHook.isSet())) {
        auto hook = msgSendCacheMissHook.get();
        hook(cls, receiver, sel, imp);
    }

    cls-&amp;gt;cache.insert(sel, imp, receiver);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;收尾&lt;/h4&gt;
&lt;p&gt;最后，执行栈回到&lt;code&gt;TailCallFunctionPointer x17&lt;/code&gt;上，跳转到IMP对应的地址上，完成一次消息调用。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文稍微深入得讲了下&lt;code&gt;objc_msgSend&lt;/code&gt;的原理。我本来以为，&lt;code&gt;objc_msgSend&lt;/code&gt;无非就是一个类似于消息中心一样的东西，没想到Apple针对这个做了很多非常细致的性能优化。&lt;/p&gt;
&lt;p&gt;目前为止，Objective-C运行时消息派发的部分已经全部完结了🤮。接下来，终于可以回到Kotlin Native部分。&lt;/p&gt;
</content:encoded></item><item><title>再见WordPress</title><link>https://blog.nowcent.cn/posts/goodbye-wordpress/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/goodbye-wordpress/</guid><pubDate>Tue, 10 Feb 2026 02:30:00 GMT</pubDate><content:encoded>&lt;p&gt;不知不觉，距离我写下第一篇博客，已经过去快6年了。这六年间，我的博客一直是用WordPress搭的。经过一番思索后，我决定将博客平台迁移至Astro。&lt;/p&gt;
&lt;h2&gt;相识WordPress&lt;/h2&gt;
&lt;p&gt;我的第一篇博客写于 2020 年 6 月 24 日凌晨 2 点。当时为什么突然萌生写博客的想法？我早就忘了。&lt;/p&gt;
&lt;p&gt;也许是因为狂神？当时我沉迷于看狂神Java的视频，狂神本人也一直在提倡大家要开一个自己的博客，去记录所学的知识。&lt;/p&gt;
&lt;p&gt;也许和摄影有关？我最早的几篇文章都跟摄影有关，那段时间我刚拥有人生中的第一台相机，也开始尝试每天扫街拍摄。所以或许是想通过博客来记录这些瞬间？&lt;/p&gt;
&lt;p&gt;也可能与当时做 Jenkins CI 有关？当时刚开始学习 Jenkins 和流水线时，那种自动化带来的震撼让我印象深刻。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;那为什么选择WordPress呢？我也忘了。可能是因为听了狂神的课程，加上当时对博客平台了解不多，于是直接选择了 WordPress。&lt;/p&gt;
&lt;p&gt;印象中，那天一直折腾 WordPress 到凌晨 2 点，直到写完第一篇文章后才去睡觉。第二天醒来，又继续折腾，并把前几天拍的照片作为封面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img/Screenshot_2020-06-24-23-24-28-522_com.quark.browser.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为什么不接着用WordPress呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;我的服务器资源不足。并且服务器要💰，用来跑博客太浪费。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我的服务器出口宽带只有1M，在WordPress写文章会非常卡。并且外部访问也会很卡，尽管已经做了许多CDN/缓存优化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用WordPress写Markdown很别扭。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;升级WordPress风险很大。我曾有过好几次因为WordPress升级失败，需要自己去数据库删脏数据。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而使用Astro有什么优势呢：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;天生支持Markdown。&lt;/li&gt;
&lt;li&gt;在客户端上编写文章体验更好，并且不用担心编写过程会卡顿。&lt;/li&gt;
&lt;li&gt;部署在Github Page上，无需考虑出口宽带问题。&lt;/li&gt;
&lt;li&gt;静态站点无需维护数据库，数据安全性更高，也减少了维护成本。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;文章迁移怎么做&lt;/h2&gt;
&lt;p&gt;其实，Astro文档上就有迁移教程&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://docs.astro.build/en/guides/migrate-to-astro/from-wordpress/&quot;}&lt;/p&gt;
&lt;p&gt;但是，迁移这块我还绕了弯路，因为一开始我想用hexo来搭建，当时用了&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;hexojs/hexo-migrator-wordpress&quot;}&lt;/p&gt;
&lt;p&gt;这个插件将WordPress里的XML数据转换到Markdown&lt;/p&gt;
&lt;p&gt;但转换后的Markdown仍存在一定的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mermaid图表缺失&lt;/li&gt;
&lt;li&gt;部分数学公式会多添加反斜杆&lt;code&gt;\&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;图片仍然引用远程地址&lt;/li&gt;
&lt;li&gt;文章标签丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好在有codex的帮助，这些问题都解决了。&lt;/p&gt;
&lt;p&gt;顺带一提，我的博客基于以下主题进行了魔改&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;saicaca/fuwari&quot;}&lt;/p&gt;
&lt;p&gt;主题非常漂亮，也非常感谢作者的付出 🙏&lt;/p&gt;
</content:encoded></item><item><title>2025总结</title><link>https://blog.nowcent.cn/posts/2025-year-in-review/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/2025-year-in-review/</guid><pubDate>Wed, 31 Dec 2025 21:47:46 GMT</pubDate><content:encoded>&lt;p&gt;居然又到跨年了，最近时间真的过得越来越快了。距上次写年度总结，我感觉才过去了几个月。不管了，先把今年的年度总结写下来吧。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;2025年度的计划&lt;/h2&gt;
&lt;h3&gt;计算机原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;自然语言处理初步学习（80%）&lt;/li&gt;
&lt;li&gt;初步理解LLM（100%）（上班摸鱼学的）&lt;/li&gt;
&lt;li&gt;深入学习编译原理（60%，啃不下去）&lt;/li&gt;
&lt;li&gt;现代操作系统与CPU的关系（完成）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工程相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;学会Rust（完成）&lt;/li&gt;
&lt;li&gt;使用Go及周围技术栈上线一个项目（完成）&lt;/li&gt;
&lt;li&gt;使用React及周围技术栈完整上线一个项目（完成）&lt;/li&gt;
&lt;li&gt;espressif开发（未开始）&lt;/li&gt;
&lt;li&gt;源码学习
&lt;ul&gt;
&lt;li&gt;Kotlin&lt;/li&gt;
&lt;li&gt;Android&lt;/li&gt;
&lt;li&gt;iOS底层全家桶&lt;/li&gt;
&lt;li&gt;Vue&lt;/li&gt;
&lt;li&gt;Jotai&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;额外开始的计划&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;深入理解区块链知识&lt;/li&gt;
&lt;li&gt;初步进行以太坊开发&lt;/li&gt;
&lt;li&gt;硬件密钥与数字证书&lt;/li&gt;
&lt;li&gt;日语&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本年度关键事件&lt;/h2&gt;
&lt;h3&gt;2025.1.1 踏入2025&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/0DC647A0-C268-4F61-87CE-C6F650A677D7_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.1.1 体验深圳地铁11号线二期&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/3C5E8222-1ECF-45E5-A6CE-6F950F78FA7F_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.1.3 给XiaomiHome提的PR被Reject&lt;/h3&gt;
&lt;p&gt;::github-pr{repo=&quot;XiaoMi/ha_xiaomi_home&quot; number=&quot;524&quot;}&lt;/p&gt;
&lt;h3&gt;2025.1.6 后台崩了&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/D915F3C3-11F3-487B-A79D-9110B27FC78F_1_105_c-472x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.1.8 公司年会&lt;/h3&gt;
&lt;h3&gt;2025.1.26 春节&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/16C5F831-CE69-4665-B1D5-C23CB887E325_1_105_c-1024x683.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/8F5A87F5-8591-4636-BF93-D920ED617C25_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.2.1 东莞虎门一日游&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/FAF46363-EC52-4516-A186-905CBD95CC07_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.2.4 Ham v1.6.0&lt;/h3&gt;
&lt;h3&gt;2025.2.12 深大元宵活动&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/DECC04D3-5F66-42C2-9F66-112895D312AC_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.3.1 鲲鹏径第七段徒步&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/3ACEB94F-A488-4AE2-81A3-5462D0A298B6_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.3.8 茶塘梅银徒步&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/17F4A887-85AE-4CA0-86EF-0AA20B0E8A79_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.3.16 茶塘梅银第二次徒步&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/E7C3B04A-4589-4396-88B6-02DE27E0C3EE_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.3 双影奇境&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/4751D287-7F2B-4F99-89E3-9EC4BEDBE156_1_105_c-1024x576.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.3.21-3.24 回了趟学校&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/7BEA55F6-C191-45EB-9A2A-8579CB0E55DB_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.3.28 去翻新好的深圳体育场看中超&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/08CB753D-4EBB-4429-A28A-88738F1A92D1_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/3FCF459C-A32B-4778-A53A-A7AE195FAD81_1_105_c-1024x683.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/975328A3-9304-4B6C-8BDB-81AC8636D03D_1_105_c-1024x683.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/2D32DDB9-EFA5-4260-9110-B1A76F2CA1B4_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.4.4 鲲鹏径第十九段徒步&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/7462E90F-64D5-4F98-8BB6-6AAB0BE8D6EA_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;2025.4.5 中超&lt;/strong&gt;&lt;/h3&gt;
&lt;h3&gt;2025.5.1 中超&lt;/h3&gt;
&lt;h3&gt;2025.5.3-5.4 珠海、澳门&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/7704CCFE-BD02-406F-A8D2-C811A3C96A09_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.6.13-6.15 去西安看VNL&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/37565F29-F918-4812-BE50-DEB1E6997934_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.7.11 看到尤雨溪本人&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/3B5CCA05-4080-42BD-8AAF-96B1F930F993_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.7-2025.8 后台使用Go重写&lt;/h3&gt;
&lt;h3&gt;2025.7.19 香港书展&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/4A32CDAA-9250-40F7-9F15-14CC4EDA636D_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.8.23-8.26 东京&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/398E5AE2-684C-42B9-8B4D-F2BB296A8539_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/CA53D050-D53D-4533-A027-81AFF623E9E2_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/BDF16175-6817-4F3C-9090-9F6315316000_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/7E29D262-4C57-4D38-914A-6EE8C71DEFFA_1_105_c-1024x685.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/AF2130A7-A89C-45B9-9F29-85B6745D7CD3_1_105_c-1024x685.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/4A55CD66-8C6C-4906-B487-D26C2AD37624_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.9.13 海边&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/F74DD2C7-4EEF-42EA-90E0-190B7F6B025D_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.9.30-10.8 日本&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/26F7D544-2F5D-48D0-9358-59234BFA78D4_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.10.31 发了一个NFT&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/43EDF468-4589-4E82-816B-E75E574A720F_1_105_c-472x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.11.9 深圳最后一家漫咖啡即将结业&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/F85BCC78-4423-4E14-817F-377602B710E6_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.12.25 深圳下雪了&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/3E16D5B4-629A-4349-ACD8-4FFBF40079EC_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2025.12.28 看阿凡达3&lt;/h3&gt;
&lt;h3&gt;2025.12.29 Ham v1.7.0&lt;/h3&gt;
&lt;h2&gt;年度反思&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;很多新学的东西没有持之以恒学下去。比如日语，本来计划年底考个级，没想到11-12月期间一点日语都没碰。&lt;/li&gt;
&lt;li&gt;可以用来学习的时间真的太少了。比如周末，除去出去运动、睡觉、吃饭、维护项目的时间，能真正学习的时间几乎没有。&lt;/li&gt;
&lt;li&gt;因为工作原因，个人素质变低。（不过也没办法，工作太让人心累了）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;下一个年度的计划与挑战&lt;/h2&gt;
&lt;h3&gt;暂定的计划&lt;/h3&gt;
&lt;h4&gt;计算机原理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;学习计算机图形学&lt;/li&gt;
&lt;li&gt;学习LLM模型有关著名论文&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;项目&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Android源码学习&lt;/li&gt;
&lt;li&gt;React及周围技术栈源码学习&lt;/li&gt;
&lt;li&gt;使用LLM技术上线一个项目&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;生活&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;日语考过N3&lt;/li&gt;
&lt;li&gt;继续提升英语水平&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;挑战&lt;/h3&gt;
&lt;p&gt;2022 年我拿到 offer 时，曾给自己设定一个目标：工作三年后尽快寻求换岗。如今这个时间节点即将到来，我是继续留在当前岗位呢，还是去勇敢地面对改变？&lt;/p&gt;
&lt;h2&gt;展望&lt;/h2&gt;
&lt;p&gt;借用去年的话来说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;迎接2026年，意味着21世纪20年代已经过半了。无论如何，往事不要再提，人生几多风雨，纵然能回到过去，身边的人早已不在。不如坦然面对现实，调整目光向前看。&lt;/p&gt;
&lt;p&gt;小米曾有句宣传语，当然也是雷老板2022年公开演讲的题目了：&lt;/p&gt;
&lt;p&gt;永远_相信美好的事情即将发生_&lt;/p&gt;
&lt;p&gt;那就让我们相信，2026年有美好的事情即将发生吧！&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>从 mTLS 到冷钱包：私钥安全的全链路分析与硬件信任边界</title><link>https://blog.nowcent.cn/posts/mtls-to-cold-wallet-key-security-and-hardware-trust/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/mtls-to-cold-wallet-key-security-and-hardware-trust/</guid><pubDate>Thu, 30 Oct 2025 15:56:02 GMT</pubDate><content:encoded>&lt;h1&gt;一次 mTLS 认证被攻破的故事&lt;/h1&gt;
&lt;p&gt;曾经我开发了一个客户端接口，为了防止被爬虫，加上了mTLS认证：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://en.wikipedia.org/wiki/Mutual_authentication&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant A as 客户端(Java)
    participant C as 客户端(二进制)
    participant B as 后台

    Note over A, B: 请求证书
    A -&amp;gt;&amp;gt; B: [Server HTTPS] 请求证书
    B -&amp;gt;&amp;gt; B: 验证Header入参及MD5(requestBody)
    B -&amp;gt;&amp;gt; B: 生成客户端私钥，并签发客户端证书
    B -&amp;gt;&amp;gt; A: 返回AES(客户端私钥, key1)、AES(客户端证书, key1）
    A -&amp;gt;&amp;gt; C: 透传服务器返回的内容给native
    C -&amp;gt;&amp;gt; A: 返回客户端本地保存的内容&amp;lt;br&amp;gt;AES(客户端私钥, key2)、AES(客户端证书, key2)
    A -&amp;gt;&amp;gt; A: 客户端保存本地加密的私钥和证书

    Note over A, B: 客户端RPC请求
    A -&amp;gt;&amp;gt; C: 获取客户端私钥和证书
    C -&amp;gt;&amp;gt; A: 返回客户端私钥与证书
    A -&amp;gt;&amp;gt; A: 用客户端私钥、证书创建一个新的AndroidKeyStore
    A -&amp;gt;&amp;gt; A: 让RPC请求使用新的KeyStore
    A -&amp;gt;&amp;gt; B: [mTLS]请求接口
    B -&amp;gt;&amp;gt; B: 验证客户端证书合法性
    B -&amp;gt;&amp;gt; B: 执行接口内部逻辑
    B -&amp;gt;&amp;gt; A: 返回接口结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我本来以为这个认证流程已经相对安全了，直到最近这个接口的访问量触发了告警，并且是同一个用户的行为，说明接口又被非预期访问了。问题出在哪呢？&lt;/p&gt;
&lt;h1&gt;问题分析&lt;/h1&gt;
&lt;p&gt;好在，这名用户非常勇敢，把破解流程放在了Github上：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;攻陷手机设备，使用Frida接管&lt;/li&gt;
&lt;li&gt;使用Frida去hook Android KeyStore有关接口。&lt;/li&gt;
&lt;li&gt;在客户端发送mTLS RPC请求前，需要从Android KeyStore获取私钥去签名。而因为Android KeyStore的接口都已被接管，输入和输出的内容可被用户截获。&lt;/li&gt;
&lt;li&gt;结果：客户端证书私钥泄漏。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以问题就在，私钥就不应该出现在&lt;strong&gt;用户态的内存上，甚至最好都不要出现在内核态里&lt;/strong&gt;。而mTLS的加密通信的核心是客户端私钥签名，又必须用到客户端私钥。有没有什么办法，让这个签名的过程与用户态隔离开来呢？&lt;/p&gt;
&lt;p&gt;有的兄弟，有的。&lt;/p&gt;
&lt;h1&gt;硬件密钥&lt;/h1&gt;
&lt;p&gt;设备上有一个单独的元器件，负责私钥存储，对外提供签名、加密功能。一般而言，除非硬件设备因存在漏洞被攻陷，硬件私钥无法被提取。&lt;/p&gt;
&lt;p&gt;对于iOS来说，整个元器件被称为&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://support.apple.com/guide/security/secure-enclave-sec59b0b31ff/web&quot;}&lt;/p&gt;
&lt;p&gt;Security Enclave（SEP）是单独的元器件，有自己的SoC、内存，甚至有自己的操作系统。iOS内核跟SEP只能通过Mailbox通信，iOS内核无法直接操作SEP。并且在与内核的通信过程，还有很多验证方法防止各种攻击，确保SEP内存空间是可信的。&lt;/p&gt;
&lt;p&gt;对于安卓，现代的安卓设备，一般会有TEE或StrongBox。实现原理和iOS的SEP可能不相同，但大体原理是一致的。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://source.android.com/docs/security/best-practices/hardware?hl=en&quot;}&lt;/p&gt;
&lt;p&gt;那为什么需要硬件密钥呢？硬件密钥是一切生物识别支付的基石。以FaceID支付为例，简单来说，以下是支付过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;后台下发支付信息与Challenge（随机值）。&lt;/li&gt;
&lt;li&gt;客户端调用系统FaceID API，传入Challenge。&lt;/li&gt;
&lt;li&gt;系统开启FaceID硬件，对用户进行生物识别。&lt;/li&gt;
&lt;li&gt;如果识别成功，FaceID API返回Challenge对应的&lt;strong&gt;硬件公钥证书链签名&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;客户端将Challenge证书签名传给后台。&lt;/li&gt;
&lt;li&gt;后台检测Challenge的签名、证书链是否有效。如果有效，则支付成功。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面过程的&lt;strong&gt;硬件公钥证书链签名&lt;/strong&gt;，是由SEP完成的。如果没有SEP，用户设备被攻陷后，攻击者很容易就拿到私钥去伪造签名。正是因为硬件密钥足够安全，生物识别支付技术才在全球流行起来。不然，我们现在要手动输入密码才能完成支付。&lt;/p&gt;
&lt;h1&gt;修改流程&lt;/h1&gt;
&lt;p&gt;既然硬件密钥这么安全，那我们mTLS证书的私钥能不能存在硬件密钥里呢？给攻击者一点颜色瞧瞧！&lt;/p&gt;
&lt;h2&gt;Android&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant A as 客户端(Java)
    participant C as 客户端(KeyStore)
    participant B as 后台

    Note over A, B: 请求Challenge
    A -&amp;gt;&amp;gt; B: [Server HTTPS] 请求Challenge
    B -&amp;gt;&amp;gt; B: 验证Header入参及MD5(requestBody)
    B -&amp;gt;&amp;gt; B: 生成Challenge（C1），扔redis里，时间窗口定5分钟
    B -&amp;gt;&amp;gt; A: 返回C1

    Note over A, B: 申请证书
    A -&amp;gt;&amp;gt; C: 用C1生成硬件密钥对pubK, priK
    C -&amp;gt;&amp;gt; A: 返回密钥对ID，Key1=handle(pubK, priK)
    A -&amp;gt;&amp;gt; C: 获取Key1硬件证明
    C -&amp;gt;&amp;gt; A: 返回硬件证明Proof1&amp;lt;br&amp;gt;Proof是GoogleRootCertificate对PubK1签发的证书链，里面包含C1
    A -&amp;gt;&amp;gt; A: 准备生成证书申请(CSR1)，里面包含pubK
    A -&amp;gt;&amp;gt; C: 使用Key1，请求用priK对CSR1签名
    C -&amp;gt;&amp;gt; A: 返回签名后的证书(CSR2)
    A -&amp;gt;&amp;gt; B: [Server HTTPS] 将Proof、CSR2发送给后台
    B -&amp;gt;&amp;gt; B: 验证Header入参及MD5(requestBody)

    B -&amp;gt;&amp;gt; B: 根据公开的GoogleRootCertificate&amp;lt;br&amp;gt;判断Proof是否合法
    B -&amp;gt;&amp;gt; B: 从Proof里取出Challenge，判定是否有效
    B -&amp;gt;&amp;gt; B: Proof里的公钥跟CSR2里的公钥是否一致
    B -&amp;gt;&amp;gt; B: 签发客户端证书CCRT
    B -&amp;gt;&amp;gt; A: 返回CCRT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺陷：Android有的硬件证书链已泄漏，后台需及时更新证书列表。&lt;/p&gt;
&lt;h2&gt;iOS&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant A as 客户端(Swift)
    participant C as 客户端(Security Enclave)
    participant D as 客户端(DCAppAttestService)
    participant B as 后台

    Note over A, B: 请求Challenge
    A -&amp;gt;&amp;gt; B: [Server HTTPS] 请求Challenge
    B -&amp;gt;&amp;gt; B: 验证Header入参及MD5(requestBody)
    B -&amp;gt;&amp;gt; B: 生成Challenge（C1），扔redis里，时间窗口定5分钟
    B -&amp;gt;&amp;gt; A: 返回C1

    Note over A, B: 申请证书
    A -&amp;gt;&amp;gt; C: 生成硬件私钥
    C -&amp;gt;&amp;gt; A: 返回硬件私钥priK的句柄priKID
    A -&amp;gt;&amp;gt; C: 传入priKID，获取硬件公钥pubK
    C -&amp;gt;&amp;gt; A: 返回硬件公钥pubK
    A -&amp;gt;&amp;gt; D: 传入data=(C1, pubK)，获取设备认证
    D -&amp;gt;&amp;gt; A: 返回设备认证签名结果D=((C1, pubK), sign(C1, pubK))
    A -&amp;gt;&amp;gt; C: 传入priKID，获取证书请求
    C -&amp;gt;&amp;gt; A: 返回证书请求CSR

    A -&amp;gt;&amp;gt; B: [Server HTTPS]发送D、CSR至后台

    B -&amp;gt;&amp;gt; B: 验证Header入参及MD5(requestBody)
    B -&amp;gt;&amp;gt; B: 验证设备认证签名D
    B -&amp;gt;&amp;gt; B: 判断D里的pubK与CSR里的公钥是否一致
    B -&amp;gt;&amp;gt; B: 签发客户端证书CCRT
    B -&amp;gt;&amp;gt; A: 返回CCRT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺陷：SEP并不提供设备认证签名（但是，FaceID API提供），后台无法确保客户端传入的公钥是SE生成的。为了弥补这个缺陷，上述流程用到了AppAttestService，因为AppAttestService提供设备认证签名。&lt;/p&gt;
&lt;h1&gt;回顾区块链&lt;/h1&gt;
&lt;p&gt;::url-card{url=&quot;https://liaoxuefeng.com/books/blockchain/introduction/index.html&quot;}&lt;/p&gt;
&lt;p&gt;假如，Bob要给Alice转账1BTC，那么需要什么呢？&lt;/p&gt;
&lt;p&gt;首先，Bob需要生成&lt;strong&gt;钱包&lt;/strong&gt;，并且需要知道Alice的&lt;strong&gt;地址&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;数学基础&lt;/h2&gt;
&lt;p&gt;::url-card{url=&quot;https://en.wikipedia.org/wiki/Elliptic_curve&quot;}&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://en.wikipedia.org/wiki/Elliptic-curve_cryptography&quot;}&lt;/p&gt;
&lt;p&gt;已知，椭圆曲线方程：&lt;/p&gt;
&lt;p&gt;$y^2=x^3+ax+b$&lt;/p&gt;
&lt;h3&gt;加法&lt;/h3&gt;
&lt;p&gt;已知，椭圆曲线是一条连续光滑的曲线。现在有三个点P、Q、R，P、Q在曲线上，有P+Q=R，则定义以下规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若P ≠ Q，则画一条直线连接P、Q，交曲线于第三点R‘，取R’关于X轴轴对称的点，该点定义为R。&lt;/li&gt;
&lt;li&gt;若P = Q，则过P点画一条椭圆曲线的切线，交曲线于第三点R’。R‘关于X轴轴对称的点定义为R。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;应用在密码学里&lt;/h3&gt;
&lt;p&gt;x, y的值域是实数域，无法应用在计算机上。为了转换到有限域上，方程改为：&lt;/p&gt;
&lt;p&gt;$y^2=x^3+ax+b \pmod p$&lt;/p&gt;
&lt;p&gt;曲线上找一点G，定义为&lt;strong&gt;基点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为曲线后面带modp，则必有一点n，使得nG = 0。该点称为&lt;strong&gt;阶&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;公钥与私钥&lt;/h3&gt;
&lt;p&gt;取一正整数d，该整数为&lt;strong&gt;私钥&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;计算Q=dG，Q点为&lt;strong&gt;公钥&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;椭圆曲线的加法必须使用累加法计算。已知d和G，计算公钥时，可以使用&lt;strong&gt;倍点加法&lt;/strong&gt;计算节约计算时间。而只知道Q和G，必须要穷举才能知道私钥d。&lt;/p&gt;
&lt;h3&gt;签名&lt;/h3&gt;
&lt;p&gt;已知消息哈希z：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;取随机正整数k，1≤k&amp;lt;n&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算R=kG&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算R=kG&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取r=Rx mod n&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算签名：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$s=k^{-1}(z+r⋅d)\mod n$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;签名为r、s、v。恢复的公钥包含多个可能值，v指定某个公钥的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;怎么从签名获得公钥Q？&lt;/p&gt;
&lt;p&gt;$$ s=k^{-1}(z+r⋅d)\mod n\ =&amp;gt; sk=z+r·d\ &amp;lt;=&amp;gt; skG=zG+rQ\ &amp;lt;=&amp;gt; Q=r^{-1}(sR-zG) $$&lt;/p&gt;
&lt;p&gt;而&lt;/p&gt;
&lt;p&gt;$$ r=R_{x}\mod n\ =&amp;gt; x_{r}=r \text{或} x_{r}=r+n $$&lt;/p&gt;
&lt;p&gt;带入恢复公式验算，公钥的点必在曲线上，得到准确的xr&lt;/p&gt;
&lt;p&gt;而y有两个解（参考椭圆曲线方程）。后面加了modp，所以有y1+y2=p。因为p是奇质数，会根据v的奇偶性，确定取y1还是y2。&lt;/p&gt;
&lt;p&gt;验证签名：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$ w=s^{-1}\mod n\ u1=z⋅w\mod n\ u2=r⋅w\mod n $$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;取曲线点：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$ (X,Y)=u1G+u2Q $$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;取&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$ v=X\mod n $$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;比较&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$ v == r $$&lt;/p&gt;
&lt;p&gt;为什么成立？&lt;/p&gt;
&lt;p&gt;$$ s=k^{-1}(z+r⋅d)\mod n\ =&amp;gt; ks=z+r·d \ &amp;lt;=&amp;gt; k=s^{-1}(z+rd) $$&lt;/p&gt;
&lt;p&gt;则&lt;/p&gt;
&lt;p&gt;$$ u1G+u2Q=(zs^{-1})G+(rs^{-1})(dG)=(kG) $$&lt;/p&gt;
&lt;h2&gt;钱包与地址&lt;/h2&gt;
&lt;p&gt;::url-card{url=&quot;https://en.wikipedia.org/wiki/Cryptocurrency_wallet&quot;}&lt;/p&gt;
&lt;p&gt;钱包：由ECDSA（椭圆曲线数字签名算法）生成的私钥集合，每一条链对应一把私钥。在链上交易时，需要用你的私钥签名交易信息。&lt;/p&gt;
&lt;p&gt;Hierarchal Deterministic (HD) Wallet：钱包里只存一个&lt;strong&gt;根私钥&lt;/strong&gt;，由此根私钥可以派生出其它私钥。&lt;strong&gt;助记词&lt;/strong&gt;是&lt;strong&gt;种子&lt;/strong&gt;的一种表现形式，种子可以生成根私钥，并派生其它私钥。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://en.bitcoin.it/wiki/Seed_phrase&quot;}&lt;/p&gt;
&lt;p&gt;地址：公钥的哈希再哈希。&lt;/p&gt;
&lt;p&gt;在一个链里，椭圆曲线的参数（p、n、G）是公开的。例如在比特币主链，用的是&lt;code&gt;secp256k1&lt;/code&gt; 曲线，其参数为：&lt;/p&gt;
&lt;p&gt;$$ y^2≡x^3+7\mod p $$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
Gx=79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy=483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
n=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;区块与区块链&lt;/h2&gt;
&lt;p&gt;区块链上有若干区块，因为每个区块都记录着上一个区块的哈希，每个区块就像被系在一起，因此称为“区块链”。&lt;/p&gt;
&lt;p&gt;区块记录交易信息，而不同的链有不同的交易信息记录方式。比特币用的是UTXO模型，而以太坊用的是账户余额模型。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://en.wikipedia.org/wiki/Unspent_transaction_output&quot;}&lt;/p&gt;
&lt;p&gt;比如，Bob之前收到Tom的1BTC，Mary的1BTC，交易记录分别记录不同的区块，那么现在Bob给Alice转账1BTC，区块的交易信息记录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入：Tom→Bob的区块哈希、Mary→Bob的区块哈希、Tom私钥对该交易信息的签名&lt;/li&gt;
&lt;li&gt;输出：Alice的地址、交易金额&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;记账&lt;/h2&gt;
&lt;p&gt;区块链是去中心化的，换言而之没有一个固定的记账对象，记账对象是一个自由加入的&lt;strong&gt;计算机集群&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在Bob与Alice的交易发生时，会发生：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;该交易被发送至未处理池里（内存池）。&lt;/li&gt;
&lt;li&gt;由链的算法在集群里选择一台机器进行记账。在比特币主链，用的是&lt;strong&gt;工作量证明&lt;/strong&gt;法。&lt;/li&gt;
&lt;li&gt;被选中的机器构造新的区块，并广播给其它所有机器，获得奖励（挖矿）。&lt;/li&gt;
&lt;li&gt;其它机器收到新的区块，验证无误后将这个区块加入到自己的本地区块链里。&lt;/li&gt;
&lt;li&gt;交易完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;安全性&lt;/h1&gt;
&lt;p&gt;区块链本身安全吗？安全。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单个区块需要验证发送者签名才会被加到链上，而签名在公私钥理论上就是安全的，无法被伪造。&lt;/li&gt;
&lt;li&gt;每个区块都记录着上一个区块的哈希值。要想改变链上某个区块的信息，就必须要改变这个区块后面所有的区块。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是，安全分为两种：密码学安全和结构安全，区块链只能保证前者。如果私钥泄漏了，那么就算区块链本身再安全也无能为力。而大部分钱包都是在软件层面生成的。这也就意味着，私钥会出现在用户态的内存环境里。按照第1节讲的，在设备被攻陷、或操作系统出现漏洞时（比如某红色三字APP用的提权漏洞）都会导致私钥被直接提取。&lt;/p&gt;
&lt;p&gt;那能否使用硬件密钥保存钱包私钥呢？很遗憾，目前并不支持。以Security Enclave为例，并不支持硬件私钥用比特币主链采用的&lt;code&gt;secp256k1&lt;/code&gt;曲线进行签名。Android 的 StrongBox 也不支持，仅有三星支持该曲线。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.samsung.com/blockchain/keystore/understanding-keystore/what-makes-keystore-unique.html&quot;}&lt;/p&gt;
&lt;p&gt;既然OS的密钥保护存在天然缺陷，那么安全的答案只能回到硬件层面：冷钱包。&lt;/p&gt;
&lt;h1&gt;回到冷钱包&lt;/h1&gt;
&lt;p&gt;既然Security Enclave不支持特定&lt;code&gt;secp256k1&lt;/code&gt;曲线签名，那能不能开发一个外部硬件来解决？这个硬件在内部生成密钥，并只对外界提供&lt;code&gt;secp256k1&lt;/code&gt;曲线签名功能，通过类似蓝牙、NFC、二维码或者USB等方式把签名发送给设备上完成交易。&lt;/p&gt;
&lt;p&gt;没错，这就是冷钱包。冷钱包属于&lt;strong&gt;硬件私钥签名设备&lt;/strong&gt;。除了冷钱包，U盾、FIFO2设备都属于这类。&lt;/p&gt;
&lt;p&gt;但是，冷钱包就一定能够确保私钥安全了？U盾之所以安全，是因为银行既是U盾的硬件厂商，又是交易场所，本身就有保证交易安全的义务，在U盾上开后门没意义。而对于FIFO2，它仅仅是一个&lt;strong&gt;认证设备&lt;/strong&gt;，并不理解用户具体的认证场所，所以硬件厂商不知道私钥的具体含义，在上面做手脚也没意义。&lt;/p&gt;
&lt;p&gt;但是对于冷钱包而言，冷钱包厂商知道硬件里的私钥是钱包。并且它也没义务去确保交易的安全，因为这是区块链本身该做的。所以冷钱包里是否存在厂商添加的后门，全凭厂商道德自觉。更可怕的是，一般的冷钱包只能连接硬件厂商提供的APP，那么冷钱包有没有可能把私钥传输到APP里，APP再把私钥传到后台呢？用户不得而知。&lt;/p&gt;
</content:encoded></item><item><title>Kotlin Native编译原理02 - 简单「深入」理解Objective-C运行时（一）</title><link>https://blog.nowcent.cn/posts/kotlin-native-compiler-02-objective-c-runtime-1/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/kotlin-native-compiler-02-objective-c-runtime-1/</guid><pubDate>Thu, 29 May 2025 23:00:02 GMT</pubDate><content:encoded>&lt;p&gt;KMP（Kotlin Multiplatform）的前身是KMM（Kotlin Multiplatform Mobile）。该项目的首要目标，是让同一套 Kotlin 代码能够同时运行在 Android 和 iOS 平台上。那么，跨端框架的代码是如何在不同平台上运行的？从使用方式来看，各家的实现大同小异：通常是跨端框架打出一个平台包出来，导入到各自的平台项目中，最后通过平台自带的构建工具（如AGP/Xcode）生成最终产物。&lt;/p&gt;
&lt;p&gt;对于Android平台来说这一项相对简单。Android首选开发语言就是Kotlin，原生工程本身就支持导入Kotlin包，KMM只要适配好AGP即可。&lt;/p&gt;
&lt;p&gt;但对于iOS平台来说，情况就复杂了：Kotlin 源代码究竟要编译成什么形式，才能被 iOS 工程所引入？正好，Kotlin Native在KMM之前一年立项，JetBrains 提供了巧妙的方案：既然iOS工程需要二进制产物，而Kotlin Native也支持将Kotlin代码编译成二进制库，那么只要把这部分二进制产物“封装”成iOS Framework，不就可以导入到iOS项目里了？&lt;/p&gt;
&lt;p&gt;好，这个问题解决了。但还有更多的问题等着解决，比如iOS工程如何调用Kotlin的逻辑？解法也很简单：当时iOS的主流语言是Objective-C，所以JetBrains 为 Kotlin Native 增加了导出 Objective-C 符号的功能，这样iOS项目就能直接调用Kotlin逻辑了。&lt;/p&gt;
&lt;p&gt;不过，从性能、安全性和通用性角度来看，还有诸多问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kotlin工程怎么引入iOS库，并使用iOS代码？&lt;/li&gt;
&lt;li&gt;Kotlin Native不依赖JVM了，内存管理如何实现？&lt;/li&gt;
&lt;li&gt;Objective-C和Kotlin互操作时怎么规避内存泄漏和野指针的问题？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都是关键性问题，轻则导致内存泄漏，重则引发应用崩溃。要是不解决好，谁敢在实际工程里用Kotlin Native？所以，目前Kotlin Native项目里充斥着许多跟Objective-C有关的桥接代码。要真正理解这些代码的作用，就要了解Objective-C的运行流程，也就是Objective-C运行时。&lt;/p&gt;
&lt;h2&gt;何为Objective-C运行时？&lt;/h2&gt;
&lt;p&gt;运行时的概念很广泛，G老师对这个问题也是一头雾水。但通常来说运行时可统称为三个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指程序运行的阶段。对应：编译时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;造句：Rust比C++更容易发现运行时的错误&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指程序运行的环境。对应：JVM，NodeJS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;造句：Java程序的运行时是JVM&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指程序依赖的动态库。对应：libc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;造句：printf需要依赖运行时&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;按照上面的概念，Objective-C一般指的是&lt;code&gt;libObjc&lt;/code&gt;这个动态库，Objective-C的特性都需要这个库发挥作用。但实际上，编译器、链接器、系统库等各个环节也对Objective-C做了大量支持。没有这些支持，编译后的Objective-C程序无法正常运行。&lt;/p&gt;
&lt;p&gt;我们先上个程序，very simple：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// test.m
#include &amp;lt;stdio.h&amp;gt;

@implementation MyClass
- (void)sayHello {
    printf(&quot;Hello world!&quot;);
}
@end

int main() {
    MyClass *obj = [MyClass alloc];
    obj = [obj init];
    [obj sayHello];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们直接编译运行试试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clang -x objective-c -o test -w test.m
Undefined symbols for architecture arm64:
  &quot;__objc_empty_cache&quot;, referenced from:
      _OBJC_CLASS_$_MyClass in test-5eaf60.o
      _OBJC_METACLASS_$_MyClass in test-5eaf60.o
  &quot;_objc_alloc&quot;, referenced from:
      _main in test-5eaf60.o
  &quot;_objc_msgSend&quot;, referenced from:
       in objc-stubs-file
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;-x objective-c&lt;/code&gt; 是告诉编译器，按照Objective-C语法进行编译。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;编译果然报错。把libObjc库加入链接，重新编译试试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clang -x objective-c -o test -lobjc -w test.m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译成功了。说明在整个编译过程里，肯定是clang or 汇编器 or 链接器往我们的程序塞了Objective-C的符号。但这些符号是一用Objective-C方式编译就会偷偷加上吗？我们再看看下面的C程序，very simple&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
int main() {
printf(&quot;Hello world!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试用Objective-C的方式编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clang -x objective-c -o test -w test.m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能够编译成功。说明严格来说，Objective-C程序并不一定需要依赖Objective-C运行时，只有我们用到Objective-C的特性时才会依赖到libObjc。但是，这些符号是谁加的呢？&lt;/p&gt;
&lt;p&gt;是clang。&lt;/p&gt;
&lt;p&gt;怎么确定是clang做的？只要把代码编译成汇编，就能看到这些外部符号已经被加进来了。源码参考：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/llvm/llvm-project/blob/main/clang/lib/CodeGen/CGObjCMac.cpp&quot;}&lt;/p&gt;
&lt;p&gt;为什么Objective-C需要完全支持纯C语法呢？因为这是Objective-C的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Objective-C is the primary programming language you use when writing software for OS X and iOS. &lt;strong&gt;It’s a superset of the C programming language and provides object-oriented capabilities and a dynamic runtime.&lt;/strong&gt; Objective-C inherits the syntax, primitive types, and flow control statements of C and adds syntax for defining classes and methods. It also adds language-level support for object graph management and object literals while providing dynamic typing and binding, deferring many responsibilities until runtime.
::url-card{url=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html&quot;}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;objc4代码&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;apple-oss-distributions/objc4&quot;}&lt;/p&gt;
&lt;p&gt;Objective-C是C的&lt;strong&gt;超集&lt;/strong&gt;。换句话说，Objective-C编译器必须有C编译器所有特性，这样才能够完整编译C程序。&lt;/p&gt;
&lt;h2&gt;静态分析&lt;/h2&gt;
&lt;p&gt;回到刚刚的Objective-C代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// test.m
#include &amp;lt;stdio.h&amp;gt;

@implementation MyClass
- (void)sayHello {
    printf(&quot;Hello world!&quot;);
}
@end

int main() {
    MyClass *obj = [MyClass alloc];
    obj = [obj init];
    [obj sayHello];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题来了，这段代码能正常跑起来吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果能正常跑起来，为什么？&lt;/li&gt;
&lt;li&gt;如果不能跑起来，是编译期报错还是运行时报错？并且是在代码第几行报错？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候肯定有同学说了：&lt;strong&gt;你这个MyClass都没继承NSObject，代码怎么可能能编译通过啊&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可是，有谁说过Objective-C里的类必须要依赖NSObject了？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NSObject&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The root class of &lt;strong&gt;most&lt;/strong&gt; Objective-C class hierarchies, from which subclasses inherit a basic interface to the runtime system and the ability to behave as Objective-C objects.&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/documentation/objectivec/nsobject-swift.class&quot;}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然，上面的程序确实在运行时会报错，并且是在&lt;code&gt;[MyClass alloc]&lt;/code&gt; 报的错。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clang -x objective-c -lobjc test.m -o test &amp;amp;&amp;amp; ./test
objc[80371]: +[MyClass alloc]: unrecognized selector sent to instance 0x1024340a0 (no message forward handler is installed)
[1]    80371 abort      ./test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们逐步分析该Objective-C程序的运行原理。&lt;/p&gt;
&lt;h3&gt;苹果的黑魔法1&lt;/h3&gt;
&lt;p&gt;我们把刚才的程序翻译成汇编看看，扒开Objective-C的内幕：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clang -x objective-c -S test.m -o test.s &amp;amp;&amp;amp; cat test.s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.section__TEXT,__text,regular,pure_instructions
.build_version macos, 15, 0sdk_version 15, 4
.p2align2                               ; -- Begin function -[MyClass sayHello]
&quot;-[MyClass sayHello]&quot;:                  ; @&quot;\\01-[MyClass sayHello]&quot;
.cfi_startproc
; %bb.0:
subsp, sp, #32
stpx29, x30, [sp, #16]             ; 16-byte Folded Spill
addx29, sp, #16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
strx0, [sp, #8]
strx1, [sp]
adrpx0, l_.str@PAGE
addx0, x0, l_.str@PAGEOFF
bl_printf
ldpx29, x30, [sp, #16]             ; 16-byte Folded Reload
addsp, sp, #32
ret
.cfi_endproc
                                        ; -- End function
.globl_main                           ; -- Begin function main
.p2align2
_main:                                  ; @main
.cfi_startproc
; %bb.0:
subsp, sp, #32
stpx29, x30, [sp, #16]             ; 16-byte Folded Spill
addx29, sp, #16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
                                        ; implicit-def: $x8
adrpx8, _OBJC_CLASSLIST_REFERENCES_$_@PAGE
ldrx0, [x8, _OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF]
bl_objc_alloc
ldrx1, [sp]                        ; 8-byte Folded Reload
strx0, [sp, #8]
ldrx0, [sp, #8]
bl_objc_msgSend$init
ldrx1, [sp]                        ; 8-byte Folded Reload
strx0, [sp, #8]
ldrx0, [sp, #8]
bl_objc_msgSend$sayHello
movw0, #0                          ; =0x0
ldpx29, x30, [sp, #16]             ; 16-byte Folded Reload
addsp, sp, #32
ret
.cfi_endproc
                                        ; -- End function
.section__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
.asciz&quot;Hello world!&quot;

.section__DATA,__objc_data
.globl_OBJC_CLASS_$_MyClass           ; @&quot;OBJC_CLASS_$_MyClass&quot;
.p2align3, 0x0
_OBJC_CLASS_$_MyClass:
.quad_OBJC_METACLASS_$_MyClass
.quad0
.quad__objc_empty_cache
.quad0
.quad__OBJC_CLASS_RO_$_MyClass

.globl_OBJC_METACLASS_$_MyClass       ; @&quot;OBJC_METACLASS_$_MyClass&quot;
.p2align3, 0x0
_OBJC_METACLASS_$_MyClass:
.quad_OBJC_METACLASS_$_MyClass
.quad_OBJC_CLASS_$_MyClass
.quad__objc_empty_cache
.quad0
.quad__OBJC_METACLASS_RO_$_MyClass

.section__TEXT,__objc_classname,cstring_literals
l_OBJC_CLASS_NAME_:                     ; @OBJC_CLASS_NAME_
.asciz&quot;MyClass&quot;

.section__DATA,__objc_const
.p2align3, 0x0                          ; @&quot;_OBJC_METACLASS_RO_$_MyClass&quot;
__OBJC_METACLASS_RO_$_MyClass:
.long3                               ; 0x3
.long40                              ; 0x28
.long40                              ; 0x28
.space4
.quad0
.quadl_OBJC_CLASS_NAME_
.quad0
.quad0
.quad0
.quad0
.quad0

.section__TEXT,__objc_methname,cstring_literals
l_OBJC_METH_VAR_NAME_:                  ; @OBJC_METH_VAR_NAME_
.asciz&quot;sayHello&quot;

.section__TEXT,__objc_methtype,cstring_literals
l_OBJC_METH_VAR_TYPE_:                  ; @OBJC_METH_VAR_TYPE_
.asciz&quot;v16@0:8&quot;

.section__DATA,__objc_const
.p2align3, 0x0                          ; @&quot;_OBJC_$_INSTANCE_METHODS_MyClass&quot;
__OBJC_$_INSTANCE_METHODS_MyClass:
.long24                              ; 0x18
.long1                               ; 0x1
.quadl_OBJC_METH_VAR_NAME_
.quadl_OBJC_METH_VAR_TYPE_
.quad&quot;-[MyClass sayHello]&quot;

.p2align3, 0x0                          ; @&quot;_OBJC_CLASS_RO_$_MyClass&quot;
__OBJC_CLASS_RO_$_MyClass:
.long2                               ; 0x2
.long0                               ; 0x0
.long0                               ; 0x0
.space4
.quad0
.quadl_OBJC_CLASS_NAME_
.quad__OBJC_$_INSTANCE_METHODS_MyClass
.quad0
.quad0
.quad0
.quad0

.section__DATA,__objc_classrefs,regular,no_dead_strip
.p2align3, 0x0                          ; @&quot;OBJC_CLASSLIST_REFERENCES_$_&quot;
_OBJC_CLASSLIST_REFERENCES_$_:
.quad_OBJC_CLASS_$_MyClass

.section__DATA,__objc_classlist,regular,no_dead_strip
.p2align3, 0x0                          ; @&quot;OBJC_LABEL_CLASS_$&quot;
l_OBJC_LABEL_CLASS_$:
.quad_OBJC_CLASS_$_MyClass

.section__DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long0
.long64

.subsections_via_symbols

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;着重关注下&lt;code&gt;[MyClass alloc]&lt;/code&gt; 和&lt;code&gt;[obj init]&lt;/code&gt;的逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[MyClass alloc] ：调用&lt;code&gt;_objc_alloc&lt;/code&gt; ，参数为&lt;code&gt;_OBJC_CLASSLIST_REFERENCES&lt;/code&gt; 符号值，实际就是&lt;code&gt;_OBJC_CLASS_$_MyClass符号&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[obj init]：调用&lt;code&gt;_objc_msgSend$init&lt;/code&gt;，参数为&lt;code&gt;[MyClass alloc]&lt;/code&gt; 创建的对象地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候有意思的地方就来了：&lt;/p&gt;
&lt;p&gt;对于第一个，我们可以直接判断&lt;code&gt;_objc_alloc&lt;/code&gt; 原型就在objc4里。至于传参具体是什么，我们可以通过objc4的源码分析；&lt;/p&gt;
&lt;p&gt;对于第二个，汇编代码里没有&lt;code&gt;_objc_msgSend$init&lt;/code&gt; 符号，所以最后是怎么通过编译的？并且，我们从前面的编译结果可以得知，&lt;code&gt;_objc_msgSend$init&lt;/code&gt; 并不是外部符号，那么这个符号是从哪冒出来的？&lt;/p&gt;
&lt;p&gt;恭喜你，发现了&lt;strong&gt;Improve app size and runtime performance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/videos/play/wwdc2022/110363&quot;}&lt;/p&gt;
&lt;p&gt;这个优化具体做了什么？还是以&lt;code&gt;[objc sayHello]&lt;/code&gt; 为例。在先前的clang版本，这个语句会被直接编译成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
adrp x1, [selector sayHello地址]
ldr x1, [x1, selector sayHello地址]
bl _objc_msgSend
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apple觉得这个可以优化成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
bl _objc_msgSend$sayHello
...

_objc_msgSend$sayHello:
adrp x1, [selector sayHello地址]
ldr x1, [x1, selector sayHello地址]
bl _objc_msgSend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样如果&lt;code&gt;[objc sayHello]&lt;/code&gt; 被多次调用，按照原来的方式就需要执行3*n条指令，而按照新的方式只需执行3+n条指令，获得了3倍的性能提升！当然，这个优化需要前端跟链接器一起做。前端负责把&lt;code&gt;[objc sayHello]&lt;/code&gt; 编译成&lt;code&gt;bl _objc_msgSend$sayHello&lt;/code&gt; ，链接器负责生成&lt;code&gt;_objc_msgSend$sayHello&lt;/code&gt; 的跳板代码。&lt;/p&gt;
&lt;p&gt;所以代价是什么？代价是作为coder，我们不能使用类似&lt;code&gt;_objc_msgSend$xxxx&lt;/code&gt; 这样的符号了。&lt;/p&gt;
&lt;p&gt;链接器新增objc_stubs具体代码：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/ld64/blob/main/src/ld/passes/objc_stubs.cpp&quot;}&lt;/p&gt;
&lt;p&gt;那么，&lt;code&gt;_objc_msgSend$sayHello&lt;/code&gt;实际是什么呢？我们接着往下看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# objdump -d test
...
00000001000008c0 &amp;lt;_objc_msgSend$sayHello&amp;gt;:
1000008c0: 90000041    adrpx1, 0x100008000 &amp;lt;__OBJC_METACLASS_RO_$_MyClass&amp;gt;
1000008c4: f9404c21    ldrx1, [x1, #0x98]
1000008c8: 90000030    adrpx16, 0x100004000 &amp;lt;_printf+0x100004000&amp;gt;
1000008cc: f9400610    ldrx16, [x16, #0x8]
1000008d0: d61f0200    brx16
1000008d4: d4200020    brk#0x1
1000008d8: d4200020    brk#0x1
1000008dc: d4200020    brk#0x1
...
# otool -IvV test
test:
Indirect symbols for (__TEXT,__stubs) 2 entries
address            index name
0x0000000100000874    11 _objc_alloc
0x0000000100000880    13 _printf
Indirect symbols for (__DATA_CONST,__got) 3 entries
address            index name
0x0000000100004000    11 _objc_alloc
0x0000000100004008    12 _objc_msgSend
0x0000000100004010    13 _printf

# objdump -d test
...
Contents of section __TEXT,__objc_methname:
 100000911 696e6974 00736179 48656c6c 6f00      init.sayHello.
...
Contents of section __DATA,__objc_selrefs:
 100008090 11090000 00001000 16090000 00001000  ................
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，在&lt;code&gt;_objc_msgSend&lt;/code&gt; 被调用时，x0为外部传入的对象地址。通过分析数据段可知，x1指向&lt;code&gt;__objc_selrefs&lt;/code&gt; 节的一段数据，而这个数据刚好是&lt;code&gt;__objc_methname&lt;/code&gt; 节里sayHello字符串的地址。&lt;/p&gt;
&lt;h3&gt;启动objc4&lt;/h3&gt;
&lt;p&gt;现在我们打开objc4源代码，看下_objc_msgSend的原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// message.h
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来，iOS开发里所谓的SEL/方法选择子，实际是方法名指针&lt;/p&gt;
&lt;p&gt;接着，我们再来看下_objc_alloc的原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc-internal.h
OBJC_EXPORT id _Nullable
objc_alloc(Class _Nullable cls)
    OBJC_AVAILABLE(10.9, 7.0, 9.0, 1.0, 2.0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Class&lt;/code&gt; 和 &lt;code&gt;id&lt;/code&gt; 是什么呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc-private.h
typedef struct objc_class *Class;
typedef struct objc_object *id;

// objc-runtime-new.h
struct objc_class : objc_object {
// Class ISA;
Class superclass;
  cache_t cache;             // formerly cache pointer and vtable
  class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
  ...
}

struct objc_object {
private:
    char isa_storage[sizeof(isa_t)];
    ...
}    

union isa_t {
uintptr_t bits;
private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic&amp;lt;const char *&amp;gt; name;
    objc::PointerUnion&amp;lt;method_list_t, relative_list_list_t&amp;lt;method_list_t&amp;gt;, method_list_t::Ptrauth, method_list_t::Ptrauth&amp;gt; baseMethods;
    objc::PointerUnion&amp;lt;protocol_list_t, relative_list_list_t&amp;lt;protocol_list_t&amp;gt;, PtrauthRaw, PtrauthRaw&amp;gt; baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    objc::PointerUnion&amp;lt;property_list_t, relative_list_list_t&amp;lt;property_list_t&amp;gt;, PtrauthRaw, PtrauthRaw&amp;gt; baseProperties;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哦，&lt;code&gt;objc_alloc&lt;/code&gt; 返回的是一个&lt;code&gt;objc_object&lt;/code&gt;。除此之外，你还可以得到以下结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_OBJC_CLASS_$_MyClass/_OBJC_METACLASS_$_MyClass&lt;/code&gt; = &lt;code&gt;objc_class&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__OBJC_CLASS_RO_$_MyClass/__OBJC_METACLASS_RO_$_MyClass&lt;/code&gt; = &lt;code&gt;class_ro_t&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__OBJC$_INSTANCE_METHODS_MyClass&lt;/code&gt; = &lt;code&gt;method_list_t&lt;/code&gt; （方法跳转地址）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并且程序加载后，&lt;code&gt;objc_class&lt;/code&gt;存在可写区域，而&lt;code&gt;class_ro_t&lt;/code&gt;存在只读区域。&lt;/p&gt;
&lt;p&gt;并且可以确定在文件映像里，这几个结构体存在以下持有关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  objc_object --&amp;gt; objc_class
  objc_class --&amp;gt; class_ro_t
  class_ro_t --&amp;gt; method_list_t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且，&lt;code&gt;class_ro_t&lt;/code&gt;里存着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超类&lt;/li&gt;
&lt;li&gt;类方法表（method_list_t）&lt;/li&gt;
&lt;li&gt;成员变量偏移表（ivars）&lt;/li&gt;
&lt;li&gt;实现协议表（protocol_list_t）&lt;/li&gt;
&lt;li&gt;属性表（property_list_t）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;[obj sayHello]&lt;/code&gt; 只调用了&lt;code&gt;bl _objc_msgSend$sayHello&lt;/code&gt; 就能跳到&lt;code&gt;-[MyClass sayHello]&lt;/code&gt; 符号上。所以很大可能，obj_msgSend里会先读取&lt;code&gt;objc_object&lt;/code&gt;里的&lt;code&gt;isa&lt;/code&gt;，从&lt;code&gt;method_list_t&lt;/code&gt; 里找到&lt;code&gt;sayHello&lt;/code&gt; 方法的实际跳转地址。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;objc_class&lt;/code&gt; 位于可写空间。所以有没有一种可能，在运行时我们把&lt;code&gt;objc_class&lt;/code&gt;里的&lt;code&gt;class_ro_t&lt;/code&gt; 整个换掉，这样就可以在运行时调整一个类的方法实现了，并且可以给一个类动态添加属性、实现协议？&lt;/p&gt;
&lt;p&gt;恭喜你，发现了&lt;strong&gt;Objective-C动态特性&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;苹果的黑魔法2&lt;/h3&gt;
&lt;p&gt;我们看下&lt;code&gt;__OBJC$_INSTANCE_METHODS_MyClass&lt;/code&gt; 符号的汇编代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.section__DATA,__objc_const
.p2align3, 0x0                          ; @&quot;_OBJC_$_INSTANCE_METHODS_MyClass&quot;
__OBJC_$_INSTANCE_METHODS_MyClass:
.long24                              ; 0x18
.long1                               ; 0x1
.quadl_OBJC_METH_VAR_NAME_
.quadl_OBJC_METH_VAR_TYPE_
.quad&quot;-[MyClass sayHello]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，我们查看下二进制反编译，又发现了神奇的东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Contents of section __TEXT,__objc_methlist:
 1000008e0 0c000080 01000000 b0770000 1d000000  .........w......
 1000008f0 10ffffff                             ....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的方法表不应该在__objc_const节么，为什么编译后就飞到__objc_methlist节了呢？&lt;/p&gt;
&lt;p&gt;并且，原先&lt;code&gt;__OBJC_$_INSTANCE_METHODS_MyClass&lt;/code&gt; 只有32个字节，而在二进制里怎么就只有20字节了呢？&lt;/p&gt;
&lt;p&gt;苹果&lt;strong&gt;又偷偷加魔法&lt;/strong&gt;了？还真是。恭喜你，发现了&lt;strong&gt;相对方法表（relative method list）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/videos/play/wwdc2020/10163/?time=1054&quot;}&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/ld64/blob/main/src/ld/passes/objc.cpp&quot;}&lt;/p&gt;
&lt;p&gt;相对方法表其实很容易理解。原先方法表里的每一项结构为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct bigSigned {
        SEL __ptrauth_objc_sel name;
        const char * ptrauth_method_list_types types;
        MethodListIMP imp;
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct small {
        // The name field either refers to a selector (in the shared
        // cache) or a selref (everywhere else).
        RelativePointer&amp;lt;const void *, /*isNullable*/false&amp;gt; name;
        RelativePointer&amp;lt;const char *&amp;gt; types;
        RelativePointer&amp;lt;IMP, /*isNullable*/false&amp;gt; imp;
    };
    
struct RelativePointer: nocopy_t {
    int32_t offset;
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;name、types和imp的地址改成地址偏移值，每项用一个字去存储。原本方法表里的每一项需要24字节，这次调整后只需要12字节，得到了2倍的性能提升！&lt;/p&gt;
&lt;p&gt;而方法表是会存在硬盘文件里的，说明这可以“大幅度”减少可执行文件的体积大小。&lt;/p&gt;
&lt;p&gt;万事俱备，我们总算入门了Objective-C可执行文件的排列结构，接着我们可以步入到程序加载阶段了！&lt;/p&gt;
&lt;h2&gt;程序加载&lt;/h2&gt;
&lt;p&gt;先让G老师帮我们过下程序加载的过程：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;简单来说，程序加载过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内核加载程序&lt;/strong&gt;：用 &lt;code&gt;execve&lt;/code&gt; 映射可执行文件到内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动 dyld&lt;/strong&gt;：动态链接器 &lt;code&gt;dyld&lt;/code&gt; 加载并解析依赖库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载动态库&lt;/strong&gt;：用 &lt;code&gt;mmap&lt;/code&gt; 加载 &lt;code&gt;.dylib&lt;/code&gt;，重定位地址，绑定符号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行初始化函数&lt;/strong&gt;：执行 C++/ObjC 初始化函数，比如 &lt;code&gt;_objc_init&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转到 main()&lt;/strong&gt;：开始执行程序主逻辑。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;1是由内核完成的，负责把映像映射到内存上。接着内核把控制权移交给dyld，dyld负责加载动态库并完成内存映像上的地址重定位。所有工作完成后，把控制权转交到程序本身，跳转到程序的main函数上。&lt;/p&gt;
&lt;h3&gt;只读内存&lt;/h3&gt;
&lt;p&gt;先来一个小测试：文件映像里DATA_CONST段是只读的。这个段映射到内存后，谁去保证这段内存是只读的？如果程序非法写入/执行内存，那么是谁去阻止的？&lt;/p&gt;
&lt;p&gt;有人觉得是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
  A[程序执行: br 0x12345678] --1.执行--&amp;gt; B[CPU]
  B --2.询问地址是否合法--&amp;gt; C[内核]
  C --3.1地址合法--&amp;gt; B
  B --4.执行跳转--&amp;gt; D[结束]
  C --3.2地址不合法--&amp;gt; E[关闭程序并报错]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图里涉及到内核，说明必然存在用户态→内核态→用户态的切换过程。内核↔用户态切换需要保存和恢复上下文，开销非常大。考虑到一个程序里有成千上万条的内存相关指令，如果每次操作内存都需要切换用户/内核态，&lt;strong&gt;那电脑岂不就会卡到爆炸&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;得，那就干脆不切换到内核态？但有个新问题，CPU去哪查某个内存地址的读写权限呢？比如要写xxx内存，得有个表告诉CPU能不能写吧？&lt;/p&gt;
&lt;p&gt;这个“权限表”存哪呢？内存断电就清空，显然表明这个表最好放内存上。那CPU怎么知道这个表的内存地址？那就在CPU里加一个寄存器，专门去存这张表的地址不就行了？&lt;/p&gt;
&lt;p&gt;最后，CPU恍然大悟，这不就是&lt;strong&gt;页表&lt;/strong&gt;吗？我们把读写权限存在页表上。这样，在MMU做内存地址转换时，可以顺便去判断地址的读写权限。既然缺页会造成中断，那么同样地，如果MMU判断到权限异常，也跳一个中断，让操作系统去处理。&lt;/p&gt;
&lt;p&gt;果然，ARM也是这么实现的：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.arm.com/documentation/102376/0200/Describing-memory-in-AArch64&quot;}&lt;/p&gt;
&lt;p&gt;具体来说，由&lt;strong&gt;AP&lt;/strong&gt;这个比特来控制页面的读写权限。这也解释了，某块内存上的读写权限是由CPU去保证的。某块只读内存，除非切到内核态（比如内核存在提权漏洞），或者环境辐射比较大导致内存颗粒发生了结构性变化，不然不可能往里面写入数据。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;DATA_CONST&lt;/code&gt;段又比较特殊，它在载入内存时是可读可写的。我们先用otool看下，会发现&lt;code&gt;DATA_CONST&lt;/code&gt;的权限是&lt;code&gt;rw-&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# otool -lv test
...
Load command 2
      cmd LC_SEGMENT_64
  cmdsize 312
  segname __DATA_CONST
   vmaddr 0x0000000100004000
   vmsize 0x0000000000004000
  fileoff 16384
 filesize 16384
  maxprot rw-
 initprot rw-
   nsects 3
    flags SG_READ_ONLY
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不在载入时就设置只读呢？我们往下看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Section
  sectname __got
   segname __DATA_CONST
      addr 0x0000000100004000
      size 0x0000000000000018
    offset 16384
     align 2^3 (8)
    reloff 0
    nreloc 0
      type S_NON_LAZY_SYMBOL_POINTERS
attributes (none)
 reserved1 2 (index into indirect symbol table)
 reserved2 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来got节也在&lt;code&gt;DATA_CONST&lt;/code&gt;段里，这也就解释了为什么在程序装载时&lt;code&gt;__DATA_CONST&lt;/code&gt;段的内存需可读可写，因为在链接阶段dyld会修改这片内存。这片内存什么时候变成只读的呢？就在dyld源码里，调用&lt;code&gt;mprotect&lt;/code&gt; 会更改该段的内存权限。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/dyld/blob/main/dyld/dyldMain.cpp#L1241&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// make __DATA_CONST read-only (kernel maps it r/w)
    const Header* dyldMH = (const Header*)dyldMA;
    dyldMH-&amp;gt;forEachSegment(^(const Header::SegmentInfo&amp;amp; segInfo, bool&amp;amp; stop) {
        if ( segInfo.readOnlyData() ) {
            const uint8_t* start = (uint8_t*)(segInfo.vmaddr + slide);
            size_t         size  = (size_t)segInfo.vmsize;
            sSyscallDelegate.mprotect((void*)start, size, PROT_READ);
        }
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mprotect&lt;/code&gt; 的实现在Darwin核心的&lt;code&gt;libsystem_kernel.dylib&lt;/code&gt; 里。该库没有开源，看下该符号的反汇编：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00000001804339bc &amp;lt;_mprotect&amp;gt;:
1804339bc: d2800950    movx16, #0x4a              ; =74
1804339c0: d4001001    svc#0x80
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，&lt;code&gt;mprotect&lt;/code&gt;实际是一个系统调用，调用号为&lt;code&gt;74&lt;/code&gt; 。在执行该调用时，程序会从用户态切换到内核态。&lt;/p&gt;
&lt;p&gt;暂停一下，看xnu内核前，我们不妨先了解下xnu的层级及架构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/2/26/The_XNU_Kernel_Graphic.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接着，我们打开xnu内核源码，查找内核号&lt;code&gt;74&lt;/code&gt; 对应实现，发现刚好就是XNU内核BSD内核里的&lt;code&gt;mprotect&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/syscalls.master#L132&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;74AUE_MPROTECTALL{ int mprotect(caddr_ut addr, size_ut len, int prot) NO_SYSCALL_STUB; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BSD层的&lt;code&gt;mprotect&lt;/code&gt; 主要做一些参数校验，接着跳到OSMFK内核的&lt;code&gt;mach_vm_protect&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/kern_mman.c#L1187&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// kern_mman.c
int
mprotect(__unused proc_t p, struct mprotect_args *uap, __unused int32_t *retval)
{
...
#if CONFIG_MACF
/*
 * The MAC check for mprotect is of limited use for 2 reasons:
 * Without mmap revocation, the caller could have asked for the max
 * protections initially instead of a reduced set, so a mprotect
 * check would offer no new security.
 * It is not possible to extract the vnode from the pager object(s)
 * of the target memory range.
 * However, the MAC check may be used to prevent a process from,
 * e.g., making the stack executable.
 */
error = mac_proc_check_mprotect(p, user_addr,
    user_size, prot);
if (error) {
return error;
}
#endif

...
prot &amp;amp;= ~VM_PROT_TRUSTED;

result = mach_vm_protect(user_map, user_addr, user_size,
    false, prot);
switch (result) {
case KERN_SUCCESS:
return 0;
case KERN_PROTECTION_FAILURE:
return EACCES;
case KERN_INVALID_ADDRESS:
/* UNIX SPEC: for an invalid address range, return ENOMEM */
return ENOMEM;
}
return EINVAL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而OSMFK层的&lt;code&gt;mach_vm_protect&lt;/code&gt; ，也只是做一些参数校验，然后跳到主角&lt;code&gt;vm_map_protect&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;mac_proc_check_mprotect&lt;/code&gt; 用来检查mmap的权限。iOS上程序无法通过mmap获取可执行内存，就是在这里判断的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/vm/vm_user.c#L292&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vm_user.c
kern_return_t
mach_vm_protect(
vm_map_t                map,
mach_vm_address_ut      start_u,
mach_vm_size_ut         size_u,
boolean_t               set_maximum,
vm_prot_ut              new_protection_u)
{
if (map == VM_MAP_NULL) {
return KERN_INVALID_ARGUMENT;
}

if (VM_SANITIZE_UNSAFE_IS_ZERO(size_u)) {
return KERN_SUCCESS;
}

return vm_map_protect(map,
           start_u,
           vm_sanitize_compute_ut_end(start_u, size_u),
           set_maximum,
           new_protection_u);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;vm_map_protect&lt;/code&gt; 做了什么呢？首先找到指定的&lt;code&gt;vm_map_entry&lt;/code&gt;（描述某块内存的作用），并检验入参合法性，如有需要合并/分裂&lt;code&gt;vm_map_entry&lt;/code&gt;，然后调用&lt;code&gt;pmap_protect&lt;/code&gt; 刷新页表权限&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/vm/vm_map.c#L5641&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 *vm_map_protect:
 *
 *Sets the protection of the specified address
 *region in the target map.  If &quot;set_max&quot; is
 *specified, the maximum protection is to be set;
 *otherwise, only the current protection is affected.
 */
kern_return_t
vm_map_protect(
vm_map_t                map,
vm_map_offset_ut        start_u,
vm_map_offset_ut        end_u,
boolean_t               set_max,
vm_prot_ut              new_prot_u)
{
...
if (current-&amp;gt;is_sub_map &amp;amp;&amp;amp; current-&amp;gt;use_pmap) {
pmap_protect(VME_SUBMAP(current)-&amp;gt;pmap,
    current-&amp;gt;vme_start,
    current-&amp;gt;vme_end,
    prot);
} else {
pmap_protect_options(map-&amp;gt;pmap,
    current-&amp;gt;vme_start,
    current-&amp;gt;vme_end,
    prot,
    pmap_options,
    NULL);
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pmap_protect&lt;/code&gt; 和 &lt;code&gt;pmap_protect_options&lt;/code&gt; 最终会跳到 &lt;code&gt;pmap_protect_options_internal&lt;/code&gt; 上，这就是操作PTE的关键函数。&lt;code&gt;pmap_protect_options_internal&lt;/code&gt; 先判断要改写的权限值，并最终调用&lt;code&gt;write_pte_fast&lt;/code&gt; 改写PTE&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/arm/pmap/pmap.c#L5413&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MARK_AS_PMAP_TEXT vm_map_address_t
pmap_protect_options_internal(
pmap_t pmap,
vm_map_address_t start,
vm_map_address_t end,
vm_prot_t prot,
unsigned int options,
__unused void *args)
{
...
{
/* Determine the new protection. */
switch (prot) {
case VM_PROT_EXECUTE:
set_XO = TRUE;
OS_FALLTHROUGH;
case VM_PROT_READ:
case VM_PROT_READ  VM_PROT_EXECUTE:
break;
case VM_PROT_READ  VM_PROT_WRITE:
case VM_PROT_ALL:
return end;         /* nothing to do */
default:
should_have_removed = TRUE;
}
}
...
/*
 * XXX Removing &quot;NX&quot; would
 * grant &quot;execute&quot; access
 * immediately, bypassing any
 * checks VM might want to do
 * in its soft fault path.
 * pmap_protect() and co. are
 * not allowed to increase
 * access permissions.
 */
if (set_NX) {
tmplate = pt_attr_leaf_xn(pt_attr);
} else {
if (pmap == kernel_pmap) {
/* do NOT clear &quot;PNX&quot;! */
tmplate = ARM_PTE_NX;
} else {
/* do NOT clear &quot;NX&quot;! */
tmplate = pt_attr_leaf_x(pt_attr);
if (set_XO) {
tmplate &amp;amp;= ~ARM_PTE_APMASK;
tmplate = pt_attr_leaf_rona(pt_attr);
}
}
}
...
write_pte_fast(pte_p, tmplate);
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;pt_attr_leaf_rona&lt;/code&gt; 实际为 &lt;code&gt;pt_attr-&amp;gt;ap_rona&lt;/code&gt; ，而&lt;code&gt;ap_rona&lt;/code&gt;是什么呢？它的定义就在&lt;code&gt;pmap.c&lt;/code&gt;文件的上方&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.ap_rona = ARM_PTE_AP(AP_RONA)

// arm64/../proc_reg.h
#define ARM_PTE_AP(x)              ((x) &amp;lt;&amp;lt; 6)            /* access protections */
#define AP_RONA 0x2 /* priv=read-only, user=no-access */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;巧了，ARM把L3 PTE的第 6-7位定为AP字段。至此，dyld 终于把 &lt;code&gt;DATA_CONST&lt;/code&gt; 段设置成了只读内存。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上，xnu 对于读写权限的处理逻辑非常复杂。本文的目的并不是深入探讨 xnu 的实现细节，这里只是简要介绍一下从用户态设置内存读写权限到内核操作页表的大致流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;objc初始化&lt;/h3&gt;
&lt;p&gt;上一节结束后，dyld把动态库加载到了内存上，并给只读段设置好了只读权限，那么接下来会发生什么呢？&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NSObject&lt;/code&gt;一个&lt;code&gt;load&lt;/code&gt;方法，按文档说的是在该类载入运行时会被调用（翻译：该类初始化时），调用时机比main还要前，是怎么做到的呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;load()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/documentation/objectivec/nsobject-swift.class/load(&quot;}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;简单分析&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们写一个程序看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#import &amp;lt;Foundation/Foundation.h&amp;gt;

@implementation MyClass: NSObject
+ (void)load {
    printf(&quot;loaded!&quot;);
}
@end

int main() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译程序，把断点打到load符号上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(lldb) br set -r &quot;\\+\[MyClass load\]&quot;
Breakpoint 2: where = test`+[MyClass load], address = 0x0000000100000868
(lldb) r
(lldb) bt
* thread #1, queue = &apos;com.apple.main-thread&apos;, stop reason = breakpoint 2.1
  * frame #0: 0x0000000100000868 test`+[MyClass load]
    frame #1: 0x0000000198877910 libobjc.A.dylib`load_images + 716
    frame #2: 0x00000001988d8200 dyld`dyld4::RuntimeState::notifyObjCInit(dyld4::Loader const*) + 456
    frame #3: 0x00000001988e3160 dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&amp;amp;, dyld3::Array&amp;lt;dyld4::Loader const*&amp;gt;&amp;amp;, dyld3::Array&amp;lt;dyld4::Loader const*&amp;gt;&amp;amp;) const + 296
    frame #4: 0x00000001988e78dc dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&amp;amp;) const::$_0::operator()() const + 180
    frame #5: 0x00000001988e3478 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&amp;amp;) const + 700
    frame #6: 0x0000000198904288 dyld`dyld4::APIs::runAllInitializersForMain() + 392
    frame #7: 0x00000001988c7dac dyld`dyld4::prepare(dyld4::APIs&amp;amp;, mach_o::Header const*) + 3092
    frame #8: 0x00000001988c7184 dyld`dyld4::start(dyld4::KernelArgs*, void*, void*)::$_0::operator()() const + 236
    frame #9: 0x00000001988c6b00 dyld`start + 5924
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用链从dyld到objc4的&lt;code&gt;load_images&lt;/code&gt;。为什么dyld会调&lt;code&gt;load_images&lt;/code&gt;呢？终于，我们在_objc_init找到一处可疑的地方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc4
// objc-os.mm
...
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    locks_init();
    environ_init();
    runtime_tls_init();
    _objc_sync_init();
    accessors_init();
    side_tables_init();
    static_init();
    runtime_init();
    exception_init();
    cache_t::init();

#if !TARGET_OS_EXCLAVEKIT
    _imp_implementationWithBlock_init();
#endif

    _dyld_objc_callbacks_v4 callbacks = {
        4, // version
        map_images,
        load_images,
        unmap_image,
        _objc_patch_root_of_class,
    };
    _dyld_objc_register_callbacks((_dyld_objc_callbacks*)&amp;amp;callbacks);

    didCallDyldNotifyRegister = true;
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_dyld_objc_register_callbacks&lt;/code&gt; 是dyld提供的API，是dyld提供给objc运行时初始化的钩子。但问题是，&lt;code&gt;_objc_init&lt;/code&gt; 是谁去调用的？&lt;/p&gt;
&lt;p&gt;这次，我们把断点打到&lt;code&gt;_objc_init&lt;/code&gt;上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(lldb) bt
* thread #1, queue = &apos;com.apple.main-thread&apos;, stop reason = breakpoint 3.1
  * frame #0: 0x000000019886d400 libobjc.A.dylib`_objc_init
    frame #1: 0x0000000198aae8cc libdispatch.dylib`_os_object_init + 24
    frame #2: 0x0000000198ae36cc libdispatch.dylib`libdispatch_init + 480
    frame #3: 0x00000001a6bc0308 libSystem.B.dylib`libSystem_initializer + 244
    frame #4: 0x00000001988e2c18 dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&amp;amp;) const + 444
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现一条调用链路：libSystem → libdispatch → libobjc#_objc_init的调用链。我们打开libSystem的源码，查看libSystem_initializer函数，果然：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// init.c
// libsyscall_initializer() initializes all of libSystem.dylib
// &amp;lt;rdar://problem/4892197&amp;gt;
__attribute__((constructor))
static void
libSystem_initializer(int argc,
      const char* argv[],
      const char* envp[],
      const char* apple[],
      const struct ProgramVars* vars)
{
...
libdispatch_init();
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，&lt;code&gt;_objc_init&lt;/code&gt;的实际调用时机是libSystem载入时。而&lt;code&gt;load_image&lt;/code&gt; 呢？我们接着分析，在dyld的&lt;code&gt;Loader::runInitializersBottomUp&lt;/code&gt; 方法里会逐个调用动态库的初始化函数 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// This recusively walks the image graph.  There is the potential for cycles.  To break cycles, if the image is delayed, we
// use the visitedDelayed set to track if the image was already visited.  If the image is not delayed, we use
// beginInitializers() to mark the image visited.
// We have to recurse into delayed dylibs because they may need to be initialized because they have weak-defs or interposing tuples.
void Loader::runInitializersBottomUp(RuntimeState&amp;amp; state, Array&amp;lt;const Loader*&amp;gt;&amp;amp; danglingUpwards, Array&amp;lt;const Loader*&amp;gt;&amp;amp; visitedDelayed) const
{
    // don&apos;t run initializers in images that are in delayInit state
    // but continue down graph and run initializers in children if needed
    const bool delayed = this-&amp;gt;isDelayInit(state) ;

    // do nothing if already visited
    if ( delayed ) {
        if ( visitedDelayed.contains(this) )
            return;
        // use &apos;visitedDelayed&apos; to mark we have already handled his image
        visitedDelayed.push_back(this);
    }
    else {
        // marks visited
        if ( (const_cast&amp;lt;Loader*&amp;gt;(this))-&amp;gt;beginInitializers(state) )
            return;
    }

    //state.log(&quot;runInitializersBottomUp(%s)\\n&quot;, this-&amp;gt;path());

    // make sure everything below this image is initialized before running my initializers
    const uint32_t depCount = this-&amp;gt;dependentCount();
    for ( uint32_t i = 0; i &amp;lt; depCount; ++i ) {
        LinkedDylibAttributes childAttrs;
        if ( Loader* child = this-&amp;gt;dependent(state, i, &amp;amp;childAttrs) ) {
            if ( childAttrs.upward ) {
                // add upwards to list to process later
                if ( !danglingUpwards.contains(child) )
                    danglingUpwards.push_back(child);
            }
            else {
                child-&amp;gt;runInitializersBottomUp(state, danglingUpwards, visitedDelayed);
            }
        }
    }

    if ( !delayed ) {
        // tell objc to run any +load methods in this image (done before C++ initializers)
        state.notifyObjCInit(this);

        // run initializers for this image
        this-&amp;gt;runInitializers(state);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;runInitializersBottomUp&lt;/code&gt; 的作用是从底向上调用动态库的初始化方法。一旦有动态库加载完成 ，就会调用&lt;code&gt;RuntimeState::notifyObjCInit&lt;/code&gt; 。注意，这个&lt;code&gt;notifyObjCInit&lt;/code&gt;并不会调用&lt;code&gt;_objc_init&lt;/code&gt; ，里面会有一系列魔法，最终调用&lt;code&gt;load_image&lt;/code&gt; 。在&lt;code&gt;notifyObjCInit&lt;/code&gt; 执行完毕后，才会走到&lt;code&gt;this-&amp;gt;runInitializers(state)&lt;/code&gt; 调用动态库初始化函数，最终调用&lt;code&gt;_objc_init&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;这个函数里是先调&lt;code&gt;notifyObjCInit&lt;/code&gt; 再调用对应动态库的初始化函数，所以有没有可能&lt;code&gt;notifyObjCInit&lt;/code&gt; 比&lt;code&gt;_objc_init&lt;/code&gt;先调用？而实际上，libSystem初始化函数执行时才会注册dyld钩子，所以能确保&lt;code&gt;_objc_init&lt;/code&gt;在&lt;code&gt;load_image&lt;/code&gt;函数前执行。&lt;/p&gt;
&lt;p&gt;剩下的逻辑就简单啦：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc4
// objc-runtime-new.mm
void
load_images(const struct _dyld_objc_notify_mapped_info* info)
{
    if (slowpath(PrintImages)) {
        _objc_inform(&quot;IMAGES: calling +load methods in %s\\n&quot;, info-&amp;gt;path ? info-&amp;gt;path : &quot;&amp;lt;null&amp;gt;&quot;);
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)info-&amp;gt;mh, info-&amp;gt;sectionLocationMetadata)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Load all pending categories if they haven&apos;t been loaded yet, and discover
    // load methods.
    {
        mutex_locker_t lock2(runtimeLock);
        loadAllCategoriesIfNeeded();
        prepare_load_methods((const headerType *)info-&amp;gt;mh, info-&amp;gt;sectionLocationMetadata);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;prepare_load_methods: 提取当前映像（动态库）所有类的load方法（IMP）到缓存里。当然，这里会第一次初始化（realize）所有Category的父类。&lt;/li&gt;
&lt;li&gt;call_load_methods: 遍历调用缓存里的load方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;_objc_init&lt;/code&gt;做了什么呢？逻辑不难，让G老师帮我们总结下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;_objc_init&lt;/code&gt; 做了这些事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只初始化一次（多线程安全）&lt;/li&gt;
&lt;li&gt;初始化 Objective-C 运行时的各种全局表和结构（类表、方法表、SEL表、缓存等）&lt;/li&gt;
&lt;li&gt;注册 dyld 回调，让后续动态库加载时能自动发现和注册新的类/分类&lt;/li&gt;
&lt;li&gt;准备自动释放池、弱引用、关联对象等机制&lt;/li&gt;
&lt;li&gt;注册与 Foundation、libdispatch 等系统的协作回调&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本质作用：让 Objective-C 运行时做好一切准备，为所有 ObjC 代码运行打好基础。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;至此，Objective-C运行时构建及初始化的工作就介绍完毕了。这么看下来，libobjc其实是一个&lt;strong&gt;不能自理&lt;/strong&gt;的动态库，编译器、链接器和系统库都必须参与到objc的运行体系，而很少有别的动态库能享受到如此高等的待遇。整体来说，Objective-C运行时更像是操作系统提供的一个&lt;strong&gt;特殊框架&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;程序运行&lt;/h2&gt;
&lt;p&gt;Objective-C初始化结束后，dyld终于把程序的控制权转移到了main函数。为了提高运行时效率及安全程度，Apple在Objective-C运行时里又添加了许多黑魔法。&lt;/p&gt;
&lt;h3&gt;指针认证（PA）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;PA是ARMv8.3-A引入的指针高位加密签名机制，用于防止指针篡改。苹果自A12芯片（iPhone XS系列，iOS 12）开始支持，用于保护返回地址、isa指针等关键指针安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/lelegard/arm-cpusysregs/blob/main/docs/arm64e-on-macos.md&quot;}&lt;/p&gt;
&lt;p&gt;PA是arm64e提供的功能。简单来说，指针认证是指CPU有一个硬件，能够对传入的指针进行签名和验签，签名后的信息被存储在指针的高位中（指针标记）。指针签名的私钥由CPU保管，甚至内核都拿不到，确保了PA「一定」安全性。&lt;/p&gt;
&lt;p&gt;paciza:&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.arm.com/documentation/dui0801/g/A64-General-Instructions/PACIA--PACIZA--PACIA1716--PACIASP--PACIAZ&quot;}&lt;/p&gt;
&lt;p&gt;autiza:&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.arm.com/documentation/dui0801/g/A64-General-Instructions/AUTIA--AUTIZA--AUTIA1716--AUTIASP--AUTIAZ&quot;}&lt;/p&gt;
&lt;p&gt;来看段very simple的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;ptrauth.h&amp;gt;

static int a = 99999;
int main(int argc, const char * argv[]) {
    void *raw_var_ptr = &amp;amp;a;
    void *signed_ptr = ptrauth_sign_unauthenticated(raw_var_ptr, ptrauth_key_process_dependent_data, 10);
// signed_ptr = rawVar;
    void *authed_ptr = ptrauth_auth_data(signed_ptr, ptrauth_key_process_dependent_data, 10);
    printf(&quot;raw_var_ptr=%p signed_ptr=%p authed_ptr=%p&quot;, raw_var_ptr, signed_ptr, authed_ptr);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;raw_var_ptr=0x102e61428 signed_ptr=0x77230102e61428 authed_ptr=0x102e61428
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，一定需要编译为arm64e，只有arm64e才支持这两个拓展指令。&lt;/p&gt;
&lt;p&gt;编译并运行程序。正如结果所示，指针签名后，CPU把签名结果存在了指针高位。而如果编译为arm64，会发现&lt;code&gt;raw_var_ptr==signed_ptr&lt;/code&gt;，这是因为arm64下的&lt;code&gt;ptrauth_sign_unauthenticated&lt;/code&gt;为空实现。&lt;/p&gt;
&lt;p&gt;如果取消掉&lt;code&gt;// signed_ptr = rawVar;&lt;/code&gt; 的注释，在&lt;code&gt;ptrauth_auth_data&lt;/code&gt; 会验签失败，后续触发&lt;code&gt;brk #0xc473&lt;/code&gt; 造成异常。&lt;/p&gt;
&lt;p&gt;指针认证有什么用呢？我们来看一个例子，来模拟对象的isa指针被强行修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

@implementation HackClass: NSObject
- (void)sayHello {
    printf(&quot;hacker sayHello&quot;);
}
@end

@implementation SuperClass: NSObject
- (void)sayHello {
    printf(&quot;super sayHello&quot;);
}
@end

@implementation MyClass: SuperClass
- (void)sayHello {
    printf(&quot;sayHello&quot;);
}
@end

struct isa_raw {
    Class isa;
};

int main(int argc, const char * argv[]) {
    MyClass *obj = [[MyClass alloc] init];
    struct isa_raw *obj2 = (__bridge struct isa_raw *)obj;
    // 模拟isa指针变化
    obj2-&amp;gt;isa = HackClass.class;
    [obj sayHello];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;isa指针受指针认证保护，所以在arm64e架构中运行程序会导致崩溃。但在不支持PA的架构中运行该程序（比如arm64），程序会输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hacker sayHello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个技术有什么用呢？可以有效保护xnu内核安全性，&lt;strong&gt;主要是防止UAF（Use-After-Free）漏洞&lt;/strong&gt;。在没有PA保护下，假如xnu内核存在一个漏洞，存在一个指向某个&lt;code&gt;objc_object&lt;/code&gt;垂悬指针，那么攻击者会：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在用户态堆喷，创建大量对象，并想办法“骗”内核把创建的对象复制进内核态，目的让垂悬指针指向自己精心构造的&lt;code&gt;objc_object&lt;/code&gt; ，这个精心构造对象的isa也是自己构造的，里面方法表的方法指向gadget（指令片段）。&lt;/li&gt;
&lt;li&gt;在用户态多次调用内核函数，让内核对垂悬指针指向的对象发消息。这样攻击者构造的gadget在内核态一个个执行，从而让攻击者&lt;strong&gt;获得内核态权限&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面的攻击方式实际是COOP（&lt;strong&gt;Counterfeit Object-oriented Programming&lt;/strong&gt;）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然实际攻击过程并没有这么简单。比如现代操作系统在载入程序时都会做ASLR（地址空间布局随机化），会导致同一个gadget，每次运行程序时的地址都不一样。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而iOS的越狱很多都是基于UAF实现的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CVE-2016-4655 (IOHIDFamily UAF)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;::url-card{url=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-4655&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CVE-2017-13861（IOKit UAF）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;::url-card{url=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-13861&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CVE-2019-8605（sock_puppet）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;::url-card{url=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-8605&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;开启PA后，UAF的难度直线上升，所以在Objective-C运行时里，Apple把PA技术用到了&lt;strong&gt;能用到的几乎所有地方（包括ISA指针、method_list等）&lt;/strong&gt;。不过，PA并不是绝对安全的，本身也会遭受侧信道攻击，详见这篇MIT的论文：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://dl.acm.org/doi/pdf/10.1145/3470496.3527429&quot;}&lt;/p&gt;
&lt;p&gt;并且PA还有一堆的缺陷待解决，每次解决导致了PA的ABI不稳定，因此尽管苹果发了&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/documentation/security/preparing-your-app-to-work-with-pointer-authentication&quot;}&lt;/p&gt;
&lt;p&gt;这篇文章推荐开发者们积极适配PA，自身系统的应用/库都完成了arm64e的适配，但截止至今日，第三方arm64e应用仍无法在macOS上运行。iOS第三方应用虽然可选arm64e架构，但Xcode默认构建arm64。并且Xcode16还存在一个bug，你构建的arm64e应用无法复制到iOS设备上并运行。&lt;/p&gt;
&lt;h3&gt;Tagged Pointer&lt;/h3&gt;
&lt;p&gt;先来看一段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    NSNumber *num1 = [NSNumber numberWithInt:15];
    NSNumber *num2 = [[NSNumber alloc] initWithInt:15];
    NSNumber *num3 = @(15);
    __asm__(&quot;brk 0x0&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;num1 == num2&lt;/code&gt; 是否成立？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;num2 == num3&lt;/code&gt; 是否成立？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;num1 == num3&lt;/code&gt; 是否成立？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把断点打到&lt;code&gt;brk 0x0&lt;/code&gt; 并把指针混淆关掉（环境变量&lt;code&gt;OBJC_DISABLE_TAG_OBFUSCATION&lt;/code&gt; 设为&lt;code&gt;true&lt;/code&gt;），然后查看&lt;code&gt;num1&lt;/code&gt; &lt;code&gt;num2&lt;/code&gt; &lt;code&gt;num3&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(lldb) p/t num1
(__NSCFNumber *) 0b1000000000000000000000000000000000000000000000000000011110010011 (int)15
(lldb) p/t num2
(__NSCFNumber *) 0b1000000000000000000000000000000000000000000000000000011110010011 (int)15
(lldb) p/t num3
(NSConstantIntegerNumber *) 0b0000000000000000000000000000000100000000000000000100000000111000 (int)15
(lldb) p num1-&amp;gt;isa
error: Couldn&apos;t apply expression side effects : Couldn&apos;t dematerialize a result variable: couldn&apos;t read its memory
(lldb) p num3-&amp;gt;isa
(Class) NSConstantIntegerNumber
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;num1和num2最高位为1，显然这不是正常的内存地址。为什么呢？因为Apple觉得变量里要存的数据量太小了，按照正常的指针存法大概率需要在堆上开辟8字节空间，简直是&lt;strong&gt;暴殄天物&lt;/strong&gt;，所以干脆把数据存在指针本身。这种技术称为&lt;strong&gt;Tagged Pointer&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在现在，ARM64下Tagged Pointer的结构如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;64位：Tagged Pointer标记，是Tagged Pointer就设为1&lt;/li&gt;
&lt;li&gt;63-8/4位：数据本体&lt;/li&gt;
&lt;li&gt;4-7位：拓展数据类型（可有可无，根据数据类型判断）&lt;/li&gt;
&lt;li&gt;1-3位：数据类型&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;怎么获取Tagged Pointer？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建支持Tagged Pointer的类，如果数据量小，会返回Tagged Pointer（比如&lt;code&gt;NSNumber&lt;/code&gt;的&lt;code&gt;numberWithInt&lt;/code&gt;和&lt;code&gt;initWithInt&lt;/code&gt;）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;怎么判断我拿到的指针是不是Tagged Pointer？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;开发者无需关心。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不关心那怎么通过指针取值？比如&lt;code&gt;NSNumber&lt;/code&gt;，如何获取里面某个ivar的值？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;NSNumber&lt;/code&gt;里不存在ivar。取值需通过&lt;code&gt;NSNumber&lt;/code&gt;的API获取，发消息函数&lt;code&gt;objc_msgSend&lt;/code&gt;里会对Tagged Pointer进行特殊处理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不关心怎么行？那我怎么通过内存布局取&lt;code&gt;NSNumber&lt;/code&gt;的实际值？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;NSNumber&lt;/code&gt; 是个接口（类簇），具体实现很多，怎么通过内存布局取值？请试用API。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;开发者能否自定义Tagged Pointer类型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不行。&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;@(15)&lt;/code&gt; 结果为什么指向一个真实的指针呢？因为这个值在编译期就已经确定了，这里指向的是文件上&lt;code&gt;DATA_CONST&lt;/code&gt;段的某个值。&lt;/p&gt;
&lt;p&gt;所以可以引出下面著名代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    __weak NSNumber *num;
    __weak NSMutableArray *array;
    @autoreleasepool {
        num = [NSNumber numberWithInt:15];
        array = [NSMutableArray array];
    }
    NSLog(@&quot;num=%@, array=%@&quot;, num, array);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;num=15, array=(null)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有这个，注意需开启ARC：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 以下代码会有Runtime Error
#include &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    __block NSString *name;
    for (int i = 0; i &amp;lt; 1000; ++i) {
        dispatch_async(q, ^{
            name = [NSString stringWithFormat:@&quot;FFFFFFFFFFFFF&quot;];
        });
    }
}

// 以下代码不会有Runtime Error
#include &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    __block NSString *name;
    for (int i = 0; i &amp;lt; 1000; ++i) {
        dispatch_async(q, ^{
            name = [NSString stringWithFormat:@&quot;FF&quot;];
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么上半部分的代码会有问题？原因在于ARC下给name赋值的代码并不是原子的，实际调用链是&lt;code&gt;objc_release&lt;/code&gt; → &lt;code&gt;assign value&lt;/code&gt; → &lt;code&gt;objc_retain&lt;/code&gt; 。对对象指针调用&lt;code&gt;objc_release&lt;/code&gt; ，如果此时的引用计数为0，就会释放对象。而这里明显存在race，如果&lt;code&gt;+[NSString stringWithFormat:]&lt;/code&gt; 返回的是一个对象，那么这个对象可能会被多次释放，进而造成程序崩溃。&lt;/p&gt;
&lt;p&gt;而对Tagged Pointer调用&lt;code&gt;objc_release&lt;/code&gt; ，在函数开头就被guard return掉了。所以如果&lt;code&gt;+[NSString stringWithFormat:]&lt;/code&gt; 返回的是Tagged Pointer，那么虽然这里存在data race，但异常只局限在操作内存这一逻辑操作，并不会导致程序出现异常。&lt;/p&gt;
&lt;h2&gt;objc_msgSend&lt;/h2&gt;
&lt;p&gt;下一章讲&lt;/p&gt;
</content:encoded></item><item><title>Kotlin Native编译原理01 - 符号与链接</title><link>https://blog.nowcent.cn/posts/kotlin-native-compiler-01-symbols-and-linking/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/kotlin-native-compiler-01-symbols-and-linking/</guid><pubDate>Mon, 12 May 2025 18:57:50 GMT</pubDate><content:encoded>&lt;p&gt;先让G老师带我们过一下编译和链接：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;编译&lt;/strong&gt;：将你写的源代码（通常是 &lt;code&gt;.c&lt;/code&gt;、&lt;code&gt;.cpp&lt;/code&gt; 等文件）转换成计算机能理解的机器代码（通常是 &lt;code&gt;.obj&lt;/code&gt; 或 &lt;code&gt;.o&lt;/code&gt; 文件）。编译器检查代码的语法，生成中间代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们来实操一下？&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;首先，我们有一份C语言的源代码，very simple：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// test.c

#include &amp;lt;stdio.h&amp;gt;

void printSomething() {
printf(&quot;hello world!&quot;);
}

int main() {
   printSomething();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地的编译环境是ARM64 MacOS。编译看看，得到中间产物&lt;code&gt;test.o&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clang -c test.c -o test.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，我们执行命令链接，得到最终可执行产物，然后程序就跑起来啦😊&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clang test.o -o test
./test

hello world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然了，这两步可以合成一个命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clang test.c -o test &amp;amp;&amp;amp; ./test
hello world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那问题来了，你从头到尾都没写过printf函数的实现，那代码为什么能够编译成功呢？从编译到程序跑起来的这段过程，编译器、系统帮我们做了什么工作呢？&lt;/p&gt;
&lt;h2&gt;编译&lt;/h2&gt;
&lt;p&gt;代码是怎么编译的？&lt;/p&gt;
&lt;p&gt;编译过程其实很复杂，如果感兴趣可以看下黑皮书《编译原理》，这里简单过一下编译的过程。&lt;/p&gt;
&lt;p&gt;我们把看看源代码的汇编表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clang -E test.c -o test.i # 预编译
clang -S test.i -o test.s # 编译到汇编
cat test.s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;        .section        __TEXT,__text,regular,pure_instructions
        .build_version macos, 15, 0     sdk_version 15, 2
        .globl  _printSomething                 ; -- Begin function printSomething
        .p2align        2
_printSomething:                        ; @printSomething
        .cfi_startproc
; %bb.0:
        stp     x29, x30, [sp, #-16]!           ; 16-byte Folded Spill
        mov     x29, sp
        .cfi_def_cfa w29, 16
        .cfi_offset w30, -8
        .cfi_offset w29, -16
        adrp    x0, l_.str@PAGE
        add     x0, x0, l_.str@PAGEOFF
        bl      _printf
        ldp     x29, x30, [sp], #16             ; 16-byte Folded Reload
        ret
        .cfi_endproc
                                        ; -- End function
        .globl  _main                           ; -- Begin function main
        .p2align        2
_main:                                  ; @main
        .cfi_startproc
; %bb.0:
        stp     x29, x30, [sp, #-16]!           ; 16-byte Folded Spill
        mov     x29, sp
        .cfi_def_cfa w29, 16
        .cfi_offset w30, -8
        .cfi_offset w29, -16
        bl      _printSomething
        mov     w0, #0                          ; =0x0
        ldp     x29, x30, [sp], #16             ; 16-byte Folded Reload
        ret
        .cfi_endproc
                                        ; -- End function
        .section        __TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
        .asciz  &quot;hello world!&quot;

.subsections_via_symbols
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以很清楚地看到，我们test.c里的函数名，实际变成了汇编里的tag。&lt;/p&gt;
&lt;p&gt;重点在两个&lt;code&gt;bl&lt;/code&gt; 命令里，可参见ARM文档&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.arm.com/documentation/dui0379/e/arm-and-thumb-instructions/bl&quot;}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;BL: Branch with Link.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;bl&lt;/code&gt; 是无条件跳转。在编译过程中，会先解析各个函数的地址，再替换掉bl语句里的“函数名”。&lt;/p&gt;
&lt;p&gt;我们使用objdump查看test的反汇编：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;objdump -d test

0000000100003f58 &amp;lt;_printSomething&amp;gt;:
100003f58: a9bf7bfd     stp     x29, x30, [sp, #-0x10]!
100003f5c: 910003fd     mov     x29, sp
100003f60: 90000000     adrp    x0, 0x100003000 &amp;lt;_printf+0x100003000&amp;gt;
100003f64: 913e6000     add     x0, x0, #0xf98
100003f68: 94000009     bl      0x100003f8c &amp;lt;_printf+0x100003f8c&amp;gt;
100003f6c: a8c17bfd     ldp     x29, x30, [sp], #0x10
100003f70: d65f03c0     ret

0000000100003f74 &amp;lt;_main&amp;gt;:
100003f74: a9bf7bfd     stp     x29, x30, [sp, #-0x10]!
100003f78: 910003fd     mov     x29, sp
100003f7c: 97fffff7     bl      0x100003f58 &amp;lt;_printSomething&amp;gt;
100003f80: 52800000     mov     w0, #0x0                ; =0
100003f84: a8c17bfd     ldp     x29, x30, [sp], #0x10
100003f88: d65f03c0     ret

Disassembly of section __TEXT,__stubs:

0000000100003f8c &amp;lt;__stubs&amp;gt;:
100003f8c: b0000010     adrp    x16, 0x100004000 &amp;lt;_printf+0x100004000&amp;gt;
100003f90: f9400210     ldr     x16, [x16]
100003f94: d61f0200     br      x16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，&lt;code&gt;bl _printSomething&lt;/code&gt;已经被替换为&lt;code&gt;bl 0x100003f58&lt;/code&gt; 。而&lt;code&gt;0x100003f58&lt;/code&gt; 刚好就是printSomething函数的地址。这个替换是什么时候做的呢？实际是编译器在编译汇编程序时，帮你做好了从tag到实际地址的转化。&lt;/p&gt;
&lt;p&gt;细心的你可能发现，&lt;code&gt;printSomething&lt;/code&gt; 函数里调用&lt;code&gt;printf&lt;/code&gt; ，对应的是汇编里的&lt;code&gt;bl 0x100003f8c&lt;/code&gt;。位于&lt;code&gt;0x100003f8c&lt;/code&gt; 函数的符号名为&lt;code&gt;__stubs&lt;/code&gt; ，包含如下逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000100003f8c &amp;lt;__stubs&amp;gt;:
100003f8c: b0000010     adrp    x16, 0x100004000 &amp;lt;_printf+0x100004000&amp;gt;
100003f90: f9400210     ldr     x16, [x16]
100003f94: d61f0200     br      x16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完蛋，这肯定不是&lt;code&gt;printf&lt;/code&gt; 的具体逻辑啊，因为&lt;code&gt;printf&lt;/code&gt; 肯定有向显存（内核）输送数据的过程，那么这三行是什么呢？实际是跳转到&lt;code&gt;0x100004000&lt;/code&gt; 地址的值上。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0x100004000&lt;/code&gt; 地址里面有什么呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;objdump  -s -d test

...
Contents of section __TEXT,__cstring:
 1000004a0 68656c6c 6f20776f 726c6421 00        hello world!.
Contents of section __TEXT,__unwind_info:
 1000004b0 01000000 1c000000 00000000 1c000000  ................
 1000004c0 00000000 1c000000 02000000 60040000  ............`...
 1000004d0 40000000 40000000 94040000 00000000  @...@...........
 1000004e0 40000000 00000000 00000000 00000000  @...............
 1000004f0 03000000 0c000100 10000100 00000000  ................
 100000500 00000004 00000000                    ........
Contents of section __DATA_CONST,__got:
 100004000 00000000 00000080                    ........

Disassembly of section __TEXT,__text:
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;0x100004000&lt;/code&gt; 里取出的值是&lt;code&gt;0x00000000 00000080&lt;/code&gt;。那么继续看上面的__stub，x16=0，那么接着会执行br 0，必然会发生segmentation fault。不过我们的程序是正常运行的，这是怎么做到的呢？&lt;/p&gt;
&lt;p&gt;恭喜你，发现了&lt;code&gt;动态链接&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;动态链接&lt;/h1&gt;
&lt;p&gt;先问问G老师什么是动态链接&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;动态链接（Dynamic Linking）&lt;/strong&gt; 是一种在&lt;strong&gt;程序运行时&lt;/strong&gt;将外部库（如系统库或第三方库）&lt;strong&gt;加载并绑定&lt;/strong&gt;到程序中的机制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为什么需要动态链接呢？不能把printf的代码复制进我们的程序里吗？当然可以，不过有个问题，如果程序一用到printf就把printf的代码复制进自己的程序里，那我们写五个需要用到printf的程序，每个程序里都有自己的printf。如果程序多起来，岂不是非常占硬盘空间？所以能不能把printf的程序抽出来，放在系统内核里，在程序需要时再去调系统内核里的实现？&lt;/p&gt;
&lt;p&gt;恭喜你，发现了&lt;code&gt;动态库&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;动态库（Dynamic Library）是指在程序运行时由操作系统加载的共享库文件。它包含了一组可以被多个程序共享使用的函数、变量或对象代码，不在编译时被直接嵌入进可执行文件中，而是在运行时加载并链接&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;简单来说，在程序准备运行（载入）时，内核里的动态链接程序会将你程序里声明的动态库载入到内存中，然后再对程序做地址重定向，接着才会真正启动你的程序。&lt;/p&gt;
&lt;p&gt;上面这段话有几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;程序是怎么告诉内核要加载哪些动态库的？&lt;/li&gt;
&lt;li&gt;动态库是怎么载入到内存里的？&lt;/li&gt;
&lt;li&gt;地址重定向是什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在了解上述问题前，我们先来了解下「符号」和「符号表」&lt;/p&gt;
&lt;h1&gt;符号与符号表&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;符号（symbol）就是程序中有名字的东西&lt;/strong&gt;，比如：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;哈哈，G老师对符号的解释真的是言简意赅呢。符号即是字符串对虚拟地址/地址偏移的映射，符号表则是多个符号的集合，会集中存在程序文件里。比如我们在反汇编里看到的_printSomething，实际是符号的一种。&lt;/p&gt;
&lt;p&gt;不同文件类型对符号的存法不同。对于MACH-O，符号表存在LoadCommand区里，对应Command为LC_SYMTAB。我们使用otool可轻松看到文件里符号表的位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;otool -l test  grep -A 5 LC_SYMTAB           
     cmd LC_SYMTAB
 cmdsize 24
  symoff 32944
   nsyms 4
  stroff 33016
 strsize 56
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MACH-O里，符号表又分为符号信息表和字符串表两部分（Linux里的ELF也是一样的）。符号信息的结构如下：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/nlist.h&quot;}&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/documentation/kernel/nlist_64&quot;}&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/qyang-nj/llios/blob/main/macho_parser/docs/LC_SYMTAB.md&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see &amp;lt;mach-o/stab.h&amp;gt; */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;n_strx&lt;/code&gt; 即为符号名字符串索引。我们可以通过这个索引，去字符串表拿到对应的字符串，该字符串即为符号名。&lt;/p&gt;
&lt;p&gt;来，我们实战一下，看看test里的符号表吧&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexdump -C -s 32944 -n 200 test                    
000080b0  02 00 00 00 0f 01 10 00  00 00 00 00 01 00 00 00  ................
000080c0  16 00 00 00 0f 01 00 00  7c 04 00 00 01 00 00 00  ...............
000080d0  1c 00 00 00 0f 01 00 00  60 04 00 00 01 00 00 00  ........`.......
000080e0  2c 00 00 00 01 00 00 01  00 00 00 00 00 00 00 00  ,...............
000080f0  03 00 00 00 03 00 00 00  20 00 5f 5f 6d 68 5f 65  ........ .__mh_e
00008100  78 65 63 75 74 65 5f 68  65 61 64 65 72 00 5f 6d  xecute_header._m
00008110  61 69 6e 00 5f 70 72 69  6e 74 53 6f 6d 65 74 68  ain._printSometh
00008120  69 6e 67 00 5f 70 72 69  6e 74 66 00 00 00 00 00  ing._printf.....
00008130  fa de 0c c0 00 00 01 91  00 00 00 01 00 00 00 00  ................
00008140  00 00 00 14 fa de 0c 02  00 00 01 7d 00 02 04 00  ...........}....
00008150  00 02 00 02 00 00 00 5d  00 00 00 58 00 00 00 00  .......]...X....
00008160  00 00 00 09 00 00 81 30  20 02 00 0c 00 00 00 00  .......0 .......
00008170  00 00 00 00 00 00 00 00                           ........
00008178
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一顿解析可知，_printSomething符号的地址为&lt;code&gt;0x000080d0&lt;/code&gt; ，符号名索引为&lt;code&gt;1c&lt;/code&gt;。查字符串表，&lt;code&gt;1c&lt;/code&gt; 位置的字符串正好是&lt;code&gt;_printSomething&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是，_print符号的位置位于哪里呢。我们先来看LC_DYSYMTAB的结构，该结构在动态链接时会用到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;otool -l test grep -A 20 LC_DYSYMTAB
            cmd LC_DYSYMTAB
        cmdsize 80
      ilocalsym 0
      nlocalsym 0
     iextdefsym 0
     nextdefsym 3
      iundefsym 3
      nundefsym 1
         tocoff 0
           ntoc 0
      modtaboff 0
        nmodtab 0
   extrefsymoff 0
    nextrefsyms 0
 indirectsymoff 33008
  nindirectsyms 2
      extreloff 0
        nextrel 0
      locreloff 0
        nlocrel 0
Load command 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可看到动态符号表位于符号表偏移&lt;code&gt;33008&lt;/code&gt; 的位置上，并且有2项。我们看看这个位置上的数据是什么&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xxd -s 33008 -l $((2 * 4)) test
000080f0: 0300 0000 0300 0000                      ........
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;03&lt;/code&gt; 为下标，对应的是符号信息的第三项（即&lt;code&gt;0x000080e0&lt;/code&gt;），而该项的符号名正好是&lt;code&gt;_printf&lt;/code&gt; 。但是为什么有两个非直接符号呢？我们用otool再看下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;otool -Iv  test
test:
Indirect symbols for (__TEXT,__stubs) 1 entries
address            index name
0x0000000100000494     3 _printf
Indirect symbols for (__DATA_CONST,__got) 1 entries
address            index name
0x0000000100004000     3 _printf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;果然，有两个地方用到了这个非直接符号。&lt;/p&gt;
&lt;h1&gt;再谈动态链接&lt;/h1&gt;
&lt;p&gt;我们再回到动态链接。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;程序是怎么声明动态库的？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实程序要用到的动态库，就包含在LoadCommand里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;otool -l test  grep -A 5 LC_LOAD_DYLIB
          cmd LC_LOAD_DYLIB
      cmdsize 56
         name /usr/lib/libSystem.B.dylib (offset 24)
   time stamp 2 Thu Jan  1 08:00:02 1970
      current version 1351.0.0
compatibility version 1.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动态链接程序（dyld）在链接时会读取程序头和LoadCommand。这条LoadCommand的意思，就是告诉动态链接程序，自己要依赖的动态库在&lt;code&gt;/usr/lib/libSystem.B.dylib&lt;/code&gt; 位置。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;但是，系统内并没有&lt;code&gt;/usr/lib/libSystem.B.dylib&lt;/code&gt;？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为苹果为了系统安全，隐藏了这个动态库，把这个库存在了dyld缓存里。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;动态库是怎么加载进内存里的？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;dyld读取LoadCommand，获取程序需依赖的动态库。&lt;/li&gt;
&lt;li&gt;加载未加载过的动态库。（比如系统库已经在内存里了，就无需重复加载）&lt;/li&gt;
&lt;li&gt;对程序用到的地址做重定位。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;怎么做地址重定位？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的程序在执行printf时，会直接跳到&lt;code&gt;0x100004000&lt;/code&gt; 地址对应的值上。而这里的data段有一个特殊的名字：&lt;strong&gt;GOT（Global Offset Table，全局偏移表）&lt;/strong&gt;，所有动态链接符号的地址都会存在这里。&lt;/p&gt;
&lt;p&gt;很好！但是&lt;code&gt;0x100004000&lt;/code&gt; 地址上的值不是 &lt;code&gt;0x00000000 00000080&lt;/code&gt; 么？在程序启动后GOT的值什么时候怎么替换成真实的地址呢？&lt;/p&gt;
&lt;p&gt;恭喜你，发现了&lt;code&gt;非延迟绑定&lt;/code&gt; 与 &lt;code&gt;延迟绑定&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;非延迟绑定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序启动时&lt;/strong&gt;就将所有外部符号（如动态库函数）解析并绑定地址。&lt;/li&gt;
&lt;li&gt;所有依赖的符号都会被查找并修正为实际地址，写入 GOT（Global Offset Table）等数据结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;延迟绑定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;程序启动时&lt;strong&gt;不解析所有符号地址&lt;/strong&gt;，只设置一个跳板机制。&lt;/li&gt;
&lt;li&gt;当第一次调用某个动态库函数时，才解析并绑定地址。&lt;/li&gt;
&lt;li&gt;此后调用该函数将不再重复解析。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回到我们的程序上，我们看看运行时的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DYLD_PRINT_BINDINGS=1 ./test

dyld[43349]: &amp;lt;test/bind#0&amp;gt; -&amp;gt; 0x19e4e7bec (libsystem_c.dylib/_printf)
dyld[43349]: Setting up kernel page-in linking for /Users/orangeboy/Downloads/untitled folder/test
dyld[43349]:   __DATA_CONST (rw.) 0x000104168000-&amp;gt;0x00010416C000 (fileOffset=0x4000, size=16KB)

lldb test
(lldb) target create &quot;test&quot;
Current executable set to &apos;/Users/orangeboy/Downloads/untitled folder/test&apos; (arm64).
(lldb) br set -r printSomething
Breakpoint 1: where = test`printSomething, address = 0x0000000100003f58
(lldb) run
Process 44312 launched: &apos;/Users/orangeboy/Downloads/untitled folder/test&apos; (arm64)
Process 44312 stopped
* thread #1, queue = &apos;com.apple.main-thread&apos;, stop reason = breakpoint 1.1
    frame #0: 0x0000000100003f58 test`printSomething
test`printSomething:
-&amp;gt;  0x100003f58 &amp;lt;+0&amp;gt;:  stp    x29, x30, [sp, #-0x10]!
    0x100003f5c &amp;lt;+4&amp;gt;:  mov    x29, sp
    0x100003f60 &amp;lt;+8&amp;gt;:  adrp   x0, 0
    0x100003f64 &amp;lt;+12&amp;gt;: add    x0, x0, #0xf98 ; &quot;hello world!&quot;
Target 0: (test) stopped.
(lldb) x/gx 0x100004000
0x100004000: 0x000000019e4e7bec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可看到，在程序执行前，&lt;code&gt;0x100004000&lt;/code&gt; 就被填充为&lt;code&gt;0x19e4e7bec&lt;/code&gt; 。我们在执行printf函数前，GOT里就已经有printf的真实地址了。说明printf是非延迟绑定的。&lt;/p&gt;
&lt;p&gt;至于为什么clang没有启用延迟绑定，我也不知道，按道理来说默认是开启的。&lt;/p&gt;
&lt;p&gt;那么非延迟绑定的过程是怎么样的呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;读取__got段的LoadCommand&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;otool -l test  grep -A 10 __got         
  sectname __got
   segname __DATA_CONST
      addr 0x0000000100004000
      size 0x0000000000000008
    offset 16384
     align 2^3 (8)
    reloff 0
    nreloc 0
     flags 0x00000006
 reserved1 1 (index into indirect symbol table)
 reserved2 0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;计算符号名&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;symbol = symbol_table[indirect_symbol_table[ reserved1 + index ]]&lt;/p&gt;
&lt;p&gt;以&lt;code&gt;0x100004000&lt;/code&gt; 为例：&lt;/p&gt;
&lt;p&gt;symbol = symbol_table[indirect_symbol_table[1 + 0]] = symbol_table[3]，对应符号表第3项，该项的符号名正好是&lt;code&gt;_printf&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;通过一系列魔法（链接器复杂实现），得到非直接符号的真实地址，填充回__got里&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再来看下延迟绑定。我无法开启clang的延迟绑定，因此转移到X64 Linux上进行操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gcc test.c -o test &amp;amp;&amp;amp; objdump -d test


test:     file format elf64-x86-64

...

Disassembly of section .plt:

0000000000401020 &amp;lt;.plt&amp;gt;:
  401020:ff 35 e2 2f 00 00    pushq  0x2fe2(%rip)        # 404008 &amp;lt;_GLOBAL_OFFSET_TABLE_+0x8&amp;gt;
  401026:ff 25 e4 2f 00 00    jmpq   *0x2fe4(%rip)        # 404010 &amp;lt;_GLOBAL_OFFSET_TABLE_+0x10&amp;gt;
  40102c:0f 1f 40 00          nopl   0x0(%rax)

0000000000401030 &amp;lt;printf@plt&amp;gt;:
  401030:ff 25 e2 2f 00 00    jmpq   *0x2fe2(%rip)        # 404018 &amp;lt;printf@GLIBC_2.2.5&amp;gt;
  401036:68 00 00 00 00       pushq  $0x0
  40103b:e9 e0 ff ff ff       jmpq   401020 &amp;lt;.plt&amp;gt;
  
...

0000000000401126 &amp;lt;printSomething&amp;gt;:
  401126:55                   push   %rbp
  401127:48 89 e5             mov    %rsp,%rbp
  40112a:bf 10 20 40 00       mov    $0x402010,%edi
  40112f:b8 00 00 00 00       mov    $0x0,%eax
  401134:e8 f7 fe ff ff       callq  401030 &amp;lt;printf@plt&amp;gt;
  401139:90                   nop
  40113a:5d                   pop    %rbp
  40113b:c3                   retq   

000000000040113c &amp;lt;main&amp;gt;:
  40113c:55                   push   %rbp
  40113d:48 89 e5             mov    %rsp,%rbp
  401140:b8 00 00 00 00       mov    $0x0,%eax
  401145:e8 dc ff ff ff       callq  401126 &amp;lt;printSomething&amp;gt;
  40114a:b8 00 00 00 00       mov    $0x0,%eax
  40114f:5d                   pop    %rbp
  401150:c3                   retq   
  401151:66 2e 0f 1f 84 00 00 nopw   %cs:0x0(%rax,%rax,1)
  401158:00 00 00 
  40115b:0f 1f 44 00 00       nopl   0x0(%rax,%rax,1)
...


# objdump -s -j .got test

test:     file format elf64-x86-64

Contents of section .got:
 403fe0 00000000 00000000 00000000 00000000  ................
 403ff0 00000000 00000000 00000000 00000000  ................
 

# objdump -s -j .got.plt test

test:     file format elf64-x86-64

Contents of section .got.plt:
 404000 103e4000 00000000 00000000 00000000  .&amp;gt;@.............
 404010 00000000 00000000 36104000 00000000  ........6.@.....
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（吐槽：ELF比MACHO程序长很多。。&lt;/p&gt;
&lt;p&gt;简单静态分析一下printSomething调用println的过程（延迟绑定版）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;printSomething里，执行到&lt;code&gt;callq 401030 &amp;lt;printf@plt&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;printf@plt里，执行&lt;code&gt;jmpq *0x2fe2(%rip)&lt;/code&gt;，从.got.plt段的&lt;code&gt;404018&lt;/code&gt; 取值并跳转&lt;/li&gt;
&lt;li&gt;取值是&lt;code&gt;401036&lt;/code&gt; ，跳转回&lt;code&gt;printf@plt&lt;/code&gt;里&lt;/li&gt;
&lt;li&gt;printf@plt里执行&lt;code&gt;jmpq 401020 &amp;lt;.plt&amp;gt;&lt;/code&gt; ，跳转到.plt&lt;/li&gt;
&lt;li&gt;.plt里执行&lt;code&gt;jmpq *0x2fe4(%rip)&lt;/code&gt; 跳转到&lt;code&gt;404010&lt;/code&gt; 的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;404010&lt;/code&gt; 的值是0，所以.plt里执行的指令等价于&lt;code&gt;jmpq 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;报错！segmentation fault&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;404010&lt;/code&gt; 的取值为什么是0呢？就算不是0，那它是什么符号的地址呢？&lt;/p&gt;
&lt;p&gt;我们在运行时看下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gdb test
(gdb) br *0x401030
Breakpoint 1 at 0x401030
(gdb) r
Starting program: /data/workspace/test 

Breakpoint 1, 0x0000000000401030 in printf@plt ()
Missing separate debuginfos, use: dnf debuginfo-install bash-4.4.20-4.tl3.tencentos.x86_64 glibc-2.28-225.tl3.6.x86_64
(gdb) x/gx 0x404010
0x404010:       0x00007ffff7dcfca0
(gdb) info symbol 0x00007ffff7dcfca0
_dl_runtime_resolve_xsavec in section .text of /lib64/ld-linux-x86-64.so.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，运行时&lt;code&gt;0x404010&lt;/code&gt; 有值，指向的是linux的**动态链接程序，**这个地址是非延迟绑定的。&lt;/p&gt;
&lt;p&gt;在回到动态库与静态库上，linux上是支持把libc静态打进程序里的。我们看下程序大小区别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gcc test.c -o test &amp;amp;&amp;amp; ls -lh test
-rwxr-xr-x 1 root root 26K May  9 17:36 test
# gcc test.c -o test -static &amp;amp;&amp;amp; ls -lh test
-rwxr-xr-x 1 root root 1.7M May  9 17:36 test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，动态库程序大小明显比静态库程序有优势。&lt;/p&gt;
&lt;h1&gt;符号修饰&lt;/h1&gt;
&lt;p&gt;奇怪，程序里的&lt;code&gt;printSomething&lt;/code&gt;方法，为什么在Mach-O上的符号是&lt;code&gt;_printSomething&lt;/code&gt;，而在ELF上的是&lt;code&gt;printSomething&lt;/code&gt;？因为，这个是由你的编译器决定的。&lt;/p&gt;
&lt;p&gt;我们改写下程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

void printSomething() {
printf(&quot;hello world!&quot;);
}
void printSomething(int i) {
    printf(&quot;hello world!&quot;);
}
int main() {
    printSomething();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个程序编译会报错，提示error: redefinition of &apos;printSomething’。这是因为两个printSomething函数在C里的符号都是_printSomething，但每个符号只能指向一个程序地址，因此编译不通过。&lt;/p&gt;
&lt;p&gt;那如果用clang++编译呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clang++ test.c -o test &amp;amp;&amp;amp; nm test

00000001000004b4 T __Z14printSomethingi
0000000100000498 T __Z14printSomethingv
0000000100000000 T __mh_execute_header
00000001000004dc T _main
                 U _printf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++里name mangling的规则与C不同。函数名相同的两个函数，如果传参不同，会被认为是两个不同的函数，会生成两个不同的符号，因此可以通过编译。&lt;/p&gt;
</content:encoded></item><item><title>2024年度总结</title><link>https://blog.nowcent.cn/posts/2024-year-in-review/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/2024-year-in-review/</guid><pubDate>Tue, 31 Dec 2024 03:02:09 GMT</pubDate><content:encoded>&lt;p&gt;已经有快一年没有写过文章了。不是不想写，是实在太忙了。忙的倒也不是全部是公司的事情，有时候下班后还要花点时间自我学习，有时还要去折腾点东西。但因为每天都沉浸在上班氛围里，总感觉这一年浑浑噩噩就过去了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;2024总结下来：少有进步，多有遗憾。&lt;/p&gt;
&lt;h2&gt;新年&lt;/h2&gt;
&lt;p&gt;讲句实话，2023是我记忆量爆炸的一年，里面包含各种奇怪且印象深刻的事情。2023年12月好像是个寒冬，也不知道干啥就突然到2024了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6502-scaled.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6501-scaled.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6531-scaled.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6623-scaled.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6629-scaled.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6643-scaled.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6591.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_6677-scaled.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/2174BAF4-4A99-4600-BE04-61DAEB892FD9_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2024元旦&amp;amp;喜迎新年 - 还行，到处都是人&lt;/li&gt;
&lt;li&gt;2024看的第一步电影：Taylor Swift时代巡回演唱会 - 最不像电影的电影&lt;/li&gt;
&lt;li&gt;2024看的第一个巨物：大黄鸭 - 就这？&lt;/li&gt;
&lt;li&gt;2024打的第一场球：龙华场 - 评价：我太菜了&lt;/li&gt;
&lt;li&gt;2024年初：各种年会&amp;amp;花市 - 商业化气息浓厚&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，还有喜闻乐见的“哟西事件”。舆论的力量是多恐怖，不言自喻。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/A592424D-A1CE-4DC1-8428-6BC15E193029-473x1024.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/AEE0B609-2597-4649-813F-D51B9E33AB34_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/3E459F3E-DE7E-46CC-A987-8F46F38AE01C_1_201_a-488x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;春&lt;/h2&gt;
&lt;h3&gt;武汉大学&amp;amp;火炬杯&lt;/h3&gt;
&lt;p&gt;3月趁樱花季悄悄回了波学校，学校的樱花果然是好看的呢qwq（大四的时候因为忙毕设没时间看）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/F72C89BE-8BFC-48FA-8849-367CD74DC6BC_1_102_a-1024x683.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/7F1EA957-0F78-4A94-81DC-A9BB1596C173_4_5005_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/DSC08081-1024x683.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/C258596F-58BD-45C2-B50F-302C27204C08_1_102_a-1024x683.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/270D708E-799A-4F7F-A83E-986897560799_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/B835E858-B75D-448C-A55B-C222D15A895D_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/844EF560-1846-4221-A617-6E49FF6F0B11_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/542A5C65-4AFA-4283-A7DB-640BD4FB2BAB_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/FA51CFDA-9056-401E-B855-F58AA362FED6_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/86023396-8927-4CF1-9EB3-65AF7A111FCE_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/9A3BB089-5C88-49F7-8A7B-15CE87222AC7_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/7FFD7259-7B3D-498C-AC79-9D97C50DD73A_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;打码是为了保护同学肖像权，敬请谅解&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这次的时间有亿点点赶，只请了一天假，周五早上高铁到武汉，周日晚上飞回深圳awa。今年的游客比以往都要多，校园里人人人人。不过好在武汉没下雨，也算没白去了。&lt;/p&gt;
&lt;p&gt;哎，这次不是以学生的身份进入校园，总感觉差了点什么。&lt;/p&gt;
&lt;h3&gt;Ham v1.4.3&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新增课程评论&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;img/2696FC44-EA12-42CC-B229-BAD1CD16EBAE_1_105_c-474x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/F7A0C92E-63E2-4F99-92B5-D4BB34F48DBD_1_105_c-474x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;新学的技术栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GRPC&lt;/li&gt;
&lt;li&gt;mTLS&lt;/li&gt;
&lt;li&gt;没想到的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从功能上看，已经基本完成当初的构想了，以后还不知道要添加什么功能呢qwq&lt;/p&gt;
&lt;h3&gt;扫街or探索&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/9429159E-1C57-497C-B859-3527DA610072_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/CFCAE51A-18C5-4428-978A-6505DE2741D5_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/790895C5-4C47-4DA6-BE76-31B141A91ECA_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/0D598528-9A7F-4EE2-B5D9-06F4234AE34A_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/78B3F776-224C-4B4F-A47C-4BC7A00F4CA5_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/67810BCD-EC71-4FD3-89CF-858B19F01E88_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/5EA62AC9-3887-46D9-9388-5C122B521076_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;麦当劳青铜展 - 就这？&lt;/li&gt;
&lt;li&gt;广州&lt;/li&gt;
&lt;li&gt;蛇口&lt;/li&gt;
&lt;li&gt;龙岗&lt;/li&gt;
&lt;li&gt;龙华&lt;/li&gt;
&lt;li&gt;杭州&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;台盖🈚️了&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/66923A7E-24F9-4C37-AF91-15233CF1B2C6_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/9B773004-39C7-4CC4-B264-F61AA7AB1553_1_105_c-472x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;台盖一直卖着市面上最好喝的柠檬茶。但因为决策层重大失误，打算冲击中式奶茶方向，把好端端的一个品牌做死了。&lt;/p&gt;
&lt;p&gt;哎，可惜。你看现在的奶茶店，有哪家不卖中式奶茶呢？&lt;/p&gt;
&lt;h3&gt;其它&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;背单词&lt;/li&gt;
&lt;li&gt;潜入门深度学习&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;夏&lt;/h2&gt;
&lt;p&gt;2024年夏，总感觉做了啥，又感觉啥也没做&lt;/p&gt;
&lt;h3&gt;HK Credit Card&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/53742677-AA96-496F-8805-E830E1378D1A_1_105_c-472x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这下港卡真的要毕业了awa。还好当时及时上车，现在是想办都办不了。&lt;/p&gt;
&lt;h3&gt;坂本龙一 · 杰作&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/B1F3F2CD-5C57-46AC-9AAA-425AB9B1CE17_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/27C2EB47-04F0-42B9-8CB3-F9918D05F2E6_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/00B2DA0B-B103-443C-82DD-75DF5175741D_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;斯人已逝，乐音不绝&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;6.1早的飞机，5.31晚一下班就赶着去电影院看了。买的杜比影院场，在场只有一个小姐姐，且边看边哭。&lt;/p&gt;
&lt;h3&gt;扫街&amp;amp;探索&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/DCB35E76-C58D-4893-8DE3-D66500D290C8_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/7CD44D62-9136-4C3F-8C53-EEC9ECE2A16C_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/492281B1-8CC5-4E61-A5A2-25910BCD0DC8_1_105_c-683x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/CFA2D94F-DC54-4DF7-A1D1-874EA30280A7_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/87EE988A-4368-4ECB-A88C-015BAC4C700D_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SG&lt;/li&gt;
&lt;li&gt;厦门&amp;amp;汕头&lt;/li&gt;
&lt;li&gt;卓悦中心某个网红店&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;电影&amp;amp;电视&amp;amp;奥运会&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/35AA7A1C-CB8E-4485-B98D-A5F4FBE09B3C_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/587F17F5-B64B-4103-86E0-3C57E9D520C4_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/63095B2C-FEB2-4DD7-9561-F05C4B3DB3F3_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/746CC0EE-69EC-495A-B365-C9F867C6710E_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;暑期上映的电影&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的名字&lt;/li&gt;
&lt;li&gt;死侍与金刚狼&lt;/li&gt;
&lt;li&gt;异形&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;热门赛事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;欧洲杯&lt;/li&gt;
&lt;li&gt;奥运会（CBSB）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你的名字，2016年末有事没看成，2017年底在深图哭着看完这部电影，从此开始了长达7年的遗憾。（当然，爱乐之城也是如此）&lt;/p&gt;
&lt;p&gt;死侍与金刚狼&amp;amp;异形，当时抱着猎奇的心态去看的，没啥特别。&lt;/p&gt;
&lt;h3&gt;游戏&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/3C401BDC-610E-42A6-A1A5-3E4FFF04D7EA_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/ADE629E2-9EE9-45E6-8822-9CE598CA21B2_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/93829561-089C-4B5C-9B21-538517267925_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;黑神话 · 悟空&lt;/li&gt;
&lt;li&gt;宇宙机器人（GOTY）&lt;/li&gt;
&lt;li&gt;动物井&lt;/li&gt;
&lt;li&gt;双人成行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;黑神话 · 悟空，真是个令我又爱又恨的游戏。整体美术风格确实顶，但在PS5上的优化非常烂，我已经遇到好几次闪退了（能在PS5上闪退的游戏本来就没几个）。前几章的剧情确实堪称完美，但后几章却烂尾了。地图设计上，引导不及格，我在第一章浪费了大量的时间在跑图，后面都是边开小地图边玩。打击手感上，PS5手柄输入延迟是真高。后期为了速速通关（顶不住了），已是完全的Boss Rush了，毫无趣味可言。&lt;/p&gt;
&lt;p&gt;宇宙机器人，很耐玩，每天玩两章就刚好。在PS5上有着最出色的手柄适配，Top级别的关卡设计，Max级别的物理引擎。只可惜卖得太贵了，体量又小，导致买的人少。&lt;/p&gt;
&lt;p&gt;动物井，一个解密性质的独立游戏。好玩是好玩，就是有点烧脑和费眼睛。&lt;/p&gt;
&lt;p&gt;双人成行，别骂了，得找人一起玩才行。&lt;/p&gt;
&lt;h3&gt;Ham v1.4.4&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;支持动态修复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新学的技术栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React Next &amp;amp; React Native&lt;/li&gt;
&lt;li&gt;Code Server&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;秋与冬&lt;/h2&gt;
&lt;h3&gt;又回了趟学校&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/923F4769-F59F-44D2-8FC1-DD20F4EE9003_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/IMG_9043-2-768x1024.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/6D793248-0B2B-4E3C-9FF3-6C64194D391E_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/0964DEB7-7961-408B-B536-2076CACDD5C1_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/E1A0AA3F-80E1-45FC-9A97-B278CEEF2308_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/C30318E5-F02F-4DBF-9688-AA8D93516E25_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;大家都回家了，没啥人，这次回去好像还感冒了&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后一次吃三食堂（三食堂在2024年底停业）&lt;/li&gt;
&lt;li&gt;网红景点打卡&lt;/li&gt;
&lt;li&gt;深圳喝不到但可以在学校天天喝的茶颜&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;坪山&amp;amp;去了一个新大学，SZTU&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/EC49683C-A577-42E6-B89A-70F130EE97A2_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/F445BD2C-EC9C-41F8-9F88-7EE91553F1D7_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/93B9C49B-721E-4400-B966-88D61CEF11EE_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/3329FFEB-2CF4-4992-8996-BB53BEFB5CD0_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/93DAC8F3-6F59-4807-B038-9596CD4821BB_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/7257A585-84EE-4631-906B-D9AA6E20BEAF_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;坪山印象：是真的郊区啊qwq，几乎都是工业园&lt;/p&gt;
&lt;p&gt;SZTU印象：嗯，设施不错，就是有点冷清。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我印象里的大学：课余时间里
&lt;ul&gt;
&lt;li&gt;操场里有很多人在运动：在跑道跑步的、在球场踢球的、在场边健身锻炼的。&lt;/li&gt;
&lt;li&gt;操场中间坐着很多人：有团建的、有闲聊的、有拍月亮的、有弹吉他的。&lt;/li&gt;
&lt;li&gt;操场外围也有很多人：有吹口风琴的、有背考研政治的、有背毛概的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SZTU：课余时间里
&lt;ul&gt;
&lt;li&gt;连个人都没有&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;前海一日游&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/7CAE7A76-B395-4CC5-AFF7-A2B5498D9F85_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/A980D7E0-10A0-4F04-A582-A1C60370D29F_1_105_c.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;前海湾地铁站门口堆满了摩的佬，问你去不去“看奶龙”。你若回答不去，他还会羞辱你。&lt;/p&gt;
&lt;p&gt;前海的雪好看，奶龙也好看，但人很多，实际看到的是人。总的来说，不值&lt;/p&gt;
&lt;h3&gt;Ham v1.5.2&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;支持日志导出&lt;/li&gt;
&lt;li&gt;支持Passkey登录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新学的技术栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xlog&lt;/li&gt;
&lt;li&gt;Passkey&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;折腾的东西&lt;/h3&gt;
&lt;h4&gt;触控墨水屏显示今日天气&lt;/h4&gt;
&lt;p&gt;妈妈再也不用担心我出门忘穿衣服了～&lt;/p&gt;
&lt;p&gt;新学的技术栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;esp32&amp;amp;esphome&lt;/li&gt;
&lt;li&gt;窗口事件通知与渲染原理&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;S905X3通刷HAOS&lt;/h4&gt;
&lt;p&gt;小米开源了HomeAssistant集成，这不搞个玩玩？&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;XiaoMi/ha_xiaomi_home&quot;}&lt;/p&gt;
&lt;p&gt;新学的技术栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HomeAssistant开发&lt;/li&gt;
&lt;li&gt;嵌入式系统引导&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结与展望&lt;/h2&gt;
&lt;p&gt;少有进步：学的越深入，未知的东西就越多，而我的学习速度与支持自己学习的时间已远比不上前几年。&lt;/p&gt;
&lt;p&gt;多有遗憾：很多时间其实都在摆，没有在学。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;2024即没有往年的紧张刺激（期末/毕设/球赛），又没有达到往年兴奋阈值（上课实验/自我项目/笼子/团建），总感觉还没开始就结束了。工作后仅有晚上的时间来做自己的事情。&lt;/li&gt;
&lt;li&gt;发现喜欢钻研技术的人越来越少了。学东西，有多少人只是为了某个目的去学，而不是源于习惯，源于”我喜欢“？&lt;/li&gt;
&lt;li&gt;深圳变了，城市整体素质变低了。越来越多高层次人才润走，也有越来越多人来深圳打拼。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;迎接2025年，意味着21世纪20年代已经过半了。无论如何，往事不要再提，人生几多风雨，纵然能回到过去，身边的人早已不在。不如坦然面对现实，调整目光向前看。&lt;/p&gt;
&lt;p&gt;小米曾有句宣传语，当然也是雷老板2022年公开演讲的题目了：&lt;/p&gt;
&lt;p&gt;永远_相信美好的事情即将发生_&lt;/p&gt;
&lt;p&gt;那就让我们相信，2025年有美好的事情即将发生吧！&lt;/p&gt;
</content:encoded></item><item><title>Passkey理论与开发入门</title><link>https://blog.nowcent.cn/posts/passkey-theory-and-development-intro/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/passkey-theory-and-development-intro/</guid><pubDate>Mon, 30 Dec 2024 15:52:36 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;通行密钥比密码更易于使用且更安全。采用通行密钥为用户提供一种简单又安全的方式，让用户无需输入密码就能在各种平台上登录你的App 和网站。——Apple&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241220034443484-10-1024x345.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;传统密码&lt;/h2&gt;
&lt;h3&gt;远古时代：明文传密码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant 用户
    participant 后台
    participant 数据库

    Note over 用户,后台: HTTP
    用户 -&amp;gt;&amp;gt; 后台: 1. [注册] 发送用户名A、密码A&apos;
    后台 -&amp;gt;&amp;gt; 数据库: 2. [注册] 保存A、A&apos;
    数据库 -&amp;gt;&amp;gt; 数据库: 3. [注册] 数据库保存A、A&apos;

    用户 -&amp;gt;&amp;gt; 后台: 1. [登录] 用户名A，密码X
    后台 -&amp;gt;&amp;gt;+ 数据库: 2. [登录] 提供用户名A
    数据库 -&amp;gt;&amp;gt;- 后台: 3. [登录] 返回正确的用户密码A&apos;
    后台 -&amp;gt;&amp;gt; 后台: 4. [登录] 比较A&apos;和X
    后台 -&amp;gt;&amp;gt; 用户: 5. [登录] 登录成功or失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;p&gt;流程直观易懂&lt;/p&gt;
&lt;p&gt;风险：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户$\Leftrightarrow$后台采用明文通信，易遇到中间人攻击&lt;/li&gt;
&lt;li&gt;数据库一旦被拖库，所有用户的密码会立刻泄漏&lt;/li&gt;
&lt;li&gt;后台能看到用户密码（你以为后台不会打日志吗）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;文艺复兴：哈希存密码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant 用户（前端）
    participant 后台
    participant 数据库

    Note right of 用户（前端）: HTTPS
    用户（前端） -&amp;gt;&amp;gt; 后台: 1. [注册] 发送A、A&apos;
    后台 -&amp;gt;&amp;gt; 数据库: 2. [注册] A、B=hash(A&apos;)
    数据库 -&amp;gt;&amp;gt; 数据库: 3. [注册] 数据库保存A、B

    用户（前端） -&amp;gt;&amp;gt; 后台: 1. [登录] 用户名A，X
    后台 -&amp;gt;&amp;gt; 数据库: 2. [登录] 提供用户名A
    数据库 -&amp;gt;&amp;gt; 后台: 3. [登录] 返回B
    后台 -&amp;gt;&amp;gt; 后台: 4. [登录] 比较B和X
    后台 -&amp;gt;&amp;gt; 用户（前端）: 5. [登录] 登录成功or失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户$\Leftrightarrow$后台用HTTPS协议传输，一定程度缓解了中间人攻击的问题&lt;/li&gt;
&lt;li&gt;数据库不明文存用户密码，被拖库后获取用户明文密码存在破解成本，可有效保护用户隐私&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;风险：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据库被拖库后，可用彩虹表推测部分用户密码&lt;/li&gt;
&lt;li&gt;因为哈希值唯一，如果知道用户在其他系统的明文密码，就可以知道用户在该系统中有没有使用相同的密码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;::url-card{url=&quot;https://zh.wikipedia.org/wiki/%E5%BD%A9%E8%99%B9%E8%A1%A8&quot;}&lt;/p&gt;
&lt;h3&gt;流行：加盐存密码&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241219221701841-1024x74.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant 用户（前端）
    participant 后台
    participant 数据库

    Note right of 用户（前端）: HTTPS
    用户（前端） -&amp;gt;&amp;gt; 后台: 1. [注册] 发送用户名A、密码A&apos;
    后台 -&amp;gt;&amp;gt; 后台: 2. [注册] 为新用户生成盐S
    后台 -&amp;gt;&amp;gt; 数据库: 3. [注册] 将A、S存数据库里
    后台 -&amp;gt;&amp;gt; 数据库: 4. [注册] 计算存储密码B=hash(S+A&apos;)
    数据库 -&amp;gt;&amp;gt; 数据库: 3. [注册] 数据库保存A、B

    用户（前端） -&amp;gt;&amp;gt; 后台: 1. [登录] 发送用户名A，密码X
    后台 -&amp;gt;&amp;gt; 数据库: 2. [登录] 提供用户名A
    数据库 -&amp;gt;&amp;gt; 后台: 3. [登录] 返回B、S
    后台 -&amp;gt;&amp;gt; 后台: 4. [登录] 比较hash(S+X)==B
    后台 -&amp;gt;&amp;gt; 用户（前端）: 5. [登录] 登录成功or失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;p&gt;提高用户密码破解成本&lt;/p&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;p&gt;盐一旦泄漏，就可用彩虹表推测用户密码&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结：密码永远不安全&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2FA：双因素认证&lt;/h2&gt;
&lt;p&gt;一般而言，三种因素可以证明用户的身份：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;秘密信息：例如用户名、密码、口令&lt;/li&gt;
&lt;li&gt;个人物品：用户手机、身份证、银行卡&lt;/li&gt;
&lt;li&gt;生理特征：指纹、脸、掌纹、虹膜&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;双因素认证是指需要两个因素证据才能通过认证&lt;/p&gt;
&lt;h3&gt;TOTP（Time-based one-time password）&lt;/h3&gt;
&lt;p&gt;基于时间的一次性密码，属于“个人物品”因素&lt;/p&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;QQ令牌&lt;br /&gt;
&lt;img src=&quot;img/image-20241219223833662-3-1024x628.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;银行密码器&lt;br /&gt;
&lt;img src=&quot;img/Passkey-2-851x1024.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;身份认证器APP（微软的Authenticator、谷歌的Authenticator、苹果的Password）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;认证原理&lt;/h4&gt;
&lt;p&gt;算法（前端、后台都会用到）：&lt;/p&gt;
&lt;p&gt;TC = floor((unixtime(now) − unixtime(T0)) / TS)&lt;br /&gt;
TOTP = HASH(SecretKey, TC)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unixtime(now)&lt;/code&gt; 当前时间（服务器时间）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unixtime(T0)&lt;/code&gt; 约定的起始时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TS&lt;/code&gt; 验证码有效长度（比如30秒）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TC&lt;/code&gt; 时间计数器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HASH&lt;/code&gt; 约定哈希函数，一般是&lt;code&gt;SHA-1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant 用户
    participant 后台

    Note over 用户,后台: 用户（或设备）与后台协商使用同一个密钥K
    用户 --&amp;gt;&amp;gt; 后台: （用户做了一系列的事情）
    后台 -&amp;gt;&amp;gt; 用户: 请求用户认证
    用户 -&amp;gt;&amp;gt; 用户: 用当前时间、密钥K生成TOTP1
    用户 -&amp;gt;&amp;gt; 后台: 发起认证，发送TOTP1
    后台 -&amp;gt;&amp;gt; 后台: 用当前时间、密钥K生成TOTP2
    后台 -&amp;gt;&amp;gt; 后台: 比较TOTP1、TOTP2
    后台 -&amp;gt;&amp;gt; 用户: 返回认证成功/失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于第5、6步，由于客户端时间与服务器时间存在误差，会允许用户使用上n个时间段至下n个时间段的TOTP认证（n由后台决定）&lt;/p&gt;
&lt;h3&gt;邮箱/短信验证码&lt;/h3&gt;
&lt;p&gt;太经典了，基本天天都能用到&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以以安全的名义获取用户隐私&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;要钱&lt;/li&gt;
&lt;li&gt;链路受控（验证码邮件可能会被收件方屏蔽、短信验证码需通过运营商链路发送）&lt;/li&gt;
&lt;li&gt;接入麻烦，国内接入还需审核短信签名与短信内容&lt;/li&gt;
&lt;li&gt;并非每个人都有可以接收验证码的手机号（比如Github的SMS认证仅限美国用户使用）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Passkey理论&lt;/h2&gt;
&lt;h3&gt;非对称加密&lt;/h3&gt;
&lt;p&gt;加密和解密使用不同的密钥&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/6483443-88b8bd40abbb17db.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;端对端加密（iMessage）&lt;/li&gt;
&lt;li&gt;Xlog加密&lt;/li&gt;
&lt;li&gt;数字签名&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数字签名&lt;/h3&gt;
&lt;p&gt;数字签名是一种用于验证数字信息完整性、真实性和抗否认性的加密技术，防止信息在传输过程中被篡改。分为签名、验证两步&lt;/p&gt;
&lt;p&gt;举例：Alice要告诉Bob，今晚8点开会&lt;/p&gt;
&lt;p&gt;不带数字签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    actor Alice
    actor Attacker
    actor Bob

    Alice -&amp;gt;&amp;gt; Attacker: 发送“今晚8点开会”
    Attacker -&amp;gt;&amp;gt; Attacker: 把信息修改为“今早8点开会”
    Attacker -&amp;gt;&amp;gt; Bob: 发送“今早8点开会”
    Note over Bob: 收到信息: 今早8点开会
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attacker的行为称为“中间人攻击”&lt;/p&gt;
&lt;p&gt;带数字签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    actor Alice
    actor Attacker
    actor Bob

    Alice --&amp;gt;&amp;gt; Alice: 生成公钥、私钥
    Alice --&amp;gt;&amp;gt; Bob: 发送公钥，Bob保存公钥
    Alice -&amp;gt;&amp;gt; Attacker: 发送“今晚8点开会”和签名（用私钥对信息签名）
    Attacker -&amp;gt;&amp;gt; Attacker: 把信息修改为“今早8点开会”
    Attacker -&amp;gt;&amp;gt; Bob: 发送信息“今早8点开会”和签名
    Bob -&amp;gt;&amp;gt; Bob: 验证签名（用公钥验证）
    Note over Bob: 签名验证失败，说明有人在中间篡改了信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;签名算法（以RSA为例）：&lt;/p&gt;
&lt;p&gt;$\text{\text{假设公钥}D\text{，私钥}E\text{，信息为}M\text{，则签名}}signature=rsa_encode(E, hash(M))$&lt;/p&gt;
&lt;p&gt;$\text{验证签名，}rsa_decode(D, signature) == hash(M)$&lt;/p&gt;
&lt;h3&gt;RSA详解&lt;/h3&gt;
&lt;p&gt;::url-card{url=&quot;https://zh.wikipedia.org/wiki/RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95&quot;}&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241219232344955-1024x479.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;前置知识&lt;/h4&gt;
&lt;h5&gt;同余&lt;/h5&gt;
&lt;p&gt;::url-card{url=&quot;https://zh.wikipedia.org/wiki/%E5%90%8C%E9%A4%98&quot;}&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241220140449220-1024x699.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;欧拉定理与欧拉函数&lt;/h5&gt;
&lt;p&gt;::url-card{url=&quot;https://oi-wiki.org/math/number-theory/fermat/&quot;}&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241219203348186-1024x443.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241219203651112-1024x564.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;欧拉函数举例：&lt;/p&gt;
&lt;p&gt;$\phi(6)=2$ ，因为1, 2, 3, 4, 5里只有1, 5与6互质&lt;/p&gt;
&lt;p&gt;$\phi(7)=1$，因为7是质数，1-6与7互质&lt;/p&gt;
&lt;p&gt;$\phi(8)=4$，因为1-8里只有1, 3, 5, 7与8互质&lt;/p&gt;
&lt;h5&gt;欧拉定理推广&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;若a与b互质，则有&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$\phi(n) = \phi(a \cdot b) = \phi(a) \cdot \phi(b)$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若n为质数，则有&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$\phi(n) = n - 1$&lt;/p&gt;
&lt;h5&gt;总结：我们现在有的武器（基础理论）&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;欧拉定理：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若 $a$ 和 $n$ 为正整数，且 $a$ 和 $n$ 互质，则&lt;br /&gt;
$$&lt;br /&gt;
a^{\varphi(n)}\equiv 1\pmod{n}&lt;br /&gt;
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;欧拉定理推广1:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若 $a$ 和 $b$ 互质，则&lt;br /&gt;
$$&lt;br /&gt;
\phi(n) = \phi(a \cdot b) = \phi(a) \cdot \phi(b)&lt;br /&gt;
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;欧拉定理推广2:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若n为质数，则&lt;br /&gt;
$$&lt;br /&gt;
\phi(n) = n - 1&lt;br /&gt;
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;费马小定理（实际为欧拉定理推导，可结合 $(1)(3)$ 看 ）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若 $a$ 和 $p$ 为正整数，且 $a$ 与 $p$ 互质，$p$ 为质数，则&lt;br /&gt;
$$&lt;br /&gt;
a^{p-1}\equiv 1\pmod{p}&lt;br /&gt;
$$&lt;/p&gt;
&lt;h4&gt;密钥生成&lt;/h4&gt;
&lt;p&gt;第一步 - 输出 $p$ 、 $q$ 、 $n$、$\phi(n)$&lt;/p&gt;
&lt;p&gt;取一质数 $p$、$q$，结合 $(2)(3)$，有&lt;/p&gt;
&lt;p&gt;$n=pq$&lt;/p&gt;
&lt;p&gt;$\phi(n)=\phi(p \cdot q)=\phi(p) \cdot \phi(q) =(p-1)(q-1)$&lt;/p&gt;
&lt;p&gt;举例：
p=61, q=53
则n=61*53=3233
\phi(n)=60*52=3120&lt;/p&gt;
&lt;p&gt;第二步 - 输出 $e$&lt;/p&gt;
&lt;p&gt;选一整数 $e$，满足 $1 &amp;lt; e &amp;lt; \phi(n)$ ，且 $e$ 与 $\phi(n)$ 互质（计算机里一般取65535）&lt;/p&gt;
&lt;p&gt;举例：
e=17&lt;/p&gt;
&lt;p&gt;第三步 - 输出 $d$&lt;/p&gt;
&lt;p&gt;计算出整数 $d$，使得 $ed \equiv 1 \pmod{\phi(n)}$&lt;/p&gt;
&lt;p&gt;怎么计算？&lt;/p&gt;
&lt;p&gt;$ed \equiv 1 \pmod{\phi(n)}$&lt;/p&gt;
&lt;p&gt;$\iff ed = k\cdot\phi(n)+1\text{，}k\text{为整数且}k&amp;gt;0$&lt;/p&gt;
&lt;p&gt;$\iff d = \frac{k\cdot\phi(n)+1}{e}$&lt;/p&gt;
&lt;p&gt;举例：
d=2753&lt;/p&gt;
&lt;p&gt;第四部 - 汇总&lt;/p&gt;
&lt;p&gt;公钥：$(e, n)$&lt;/p&gt;
&lt;p&gt;私钥：$(d, n)$&lt;/p&gt;
&lt;p&gt;$n$ 的长度：密钥长度（比如RSA1024，1024指的是 $n$ 有1024位）&lt;/p&gt;
&lt;p&gt;举例：
上面的公钥：(17, 3233)，私钥：(2753, 3233)&lt;/p&gt;
&lt;h4&gt;加解密&lt;/h4&gt;
&lt;p&gt;假设原始数据为 $m$，密文为 $c$，则&lt;/p&gt;
&lt;p&gt;公钥加密：$m^{e} \equiv c \pmod{n}$&lt;/p&gt;
&lt;p&gt;私钥解密：$c^{d} \equiv m \pmod{n}$&lt;/p&gt;
&lt;p&gt;注：$m &amp;lt; n$，否则需分段加密&lt;/p&gt;
&lt;h4&gt;安全论证&lt;/h4&gt;
&lt;p&gt;已知公钥，能否知道私钥？&lt;/p&gt;
&lt;p&gt;等价于：已知 $n$，$e$，能否知道 $d$？&lt;/p&gt;
&lt;p&gt;已知：$ed \equiv 1 \pmod{\phi(n)}$，所以需要知道 $\phi(n)$&lt;/p&gt;
&lt;p&gt;已知：$\phi(n)=(p-1)(q-1)$，所以需要知道 $p$，$q$&lt;/p&gt;
&lt;p&gt;已知：$n=p \cdot q$，所以需要对 $n$ 做质因数分解&lt;/p&gt;
&lt;p&gt;如果 $p$，$q$ 取得比较大（比如1000位），那么 $n$ 至少有 $999+999=1998$位，以人类目前的科学水平不可能对如此大的数做质因数分解。&lt;/p&gt;
&lt;h4&gt;正确性证明&lt;/h4&gt;
&lt;p&gt;$\text{已知：}m^{e} \equiv c \pmod{n}……..(a)$&lt;/p&gt;
&lt;p&gt;$\text{求证：}c^{d} \equiv m \pmod{n}……..(b)$&lt;/p&gt;
&lt;p&gt;$\text{解：}$&lt;/p&gt;
&lt;p&gt;$(b)\text{中带入}(a)\text{，有}m \equiv c^{d} \pmod{n} \equiv m ^ {ed} \pmod{n} \equiv m^{k\phi(n) + 1}\pmod{n}$&lt;/p&gt;
&lt;p&gt;$\text{即需证} m \equiv m^{k\phi(n) + 1}\pmod{n}$&lt;/p&gt;
&lt;p&gt;$case1: m\text{、}n\text{互质}$&lt;/p&gt;
&lt;p&gt;$\text{\text{根据欧拉定理}(1)\text{，有}}$&lt;/p&gt;
&lt;p&gt;$m^{\varphi(n)}\equiv 1\pmod{n} \iff (m^{\varphi(n)})^k \cdot m \equiv 1^k \cdot m \pmod{n} \iff m \equiv m^{k\phi(n) + 1}\pmod{n}$&lt;/p&gt;
&lt;p&gt;$\text{秒了}$&lt;/p&gt;
&lt;p&gt;$case2:m\text{、}n\text{不互质}$&lt;/p&gt;
&lt;p&gt;$\text{已知，}n=p \cdot q\text{，而}m\text{、}n\text{不互质。则必有整数}\lambda\text{，使得}m= \lambda p\text{或} m = \lambda q\text{。}p\text{、}q\text{可互换，这里先考虑}m=\lambda p$&lt;/p&gt;
&lt;p&gt;$\text{由欧拉定理}(1)\text{，有}$&lt;/p&gt;
&lt;p&gt;$m^{\phi(q)} \equiv 1 \pmod{q}$&lt;/p&gt;
&lt;p&gt;$\iff (m^{k\phi(q)})^{\phi(p)} \equiv (1^{k\phi(q)})^{\phi(p)} \pmod{q} \equiv 1 \pmod{q}$&lt;/p&gt;
&lt;p&gt;$\text{注意到左边}(m^{k\phi(q)})^{\phi(p)}=m^{k\phi(n)}\text{，说明有}m^{k\phi(n)}\equiv1\pmod{q}\text{，则总有一个正整数}r\text{，使得}m^{k\phi(n)}=rq+1$&lt;/p&gt;
&lt;p&gt;$\text{两边同时乘}m=\lambda p\text{，有}m^{k\phi(n)+1}=\lambda rpq+\lambda p=\lambda rn+\lambda p$&lt;/p&gt;
&lt;p&gt;$\text{等价于}m^{k\phi(n)+1}\equiv \lambda p \pmod{n} \equiv m \pmod{n}$&lt;/p&gt;
&lt;p&gt;$\text{证毕}$&lt;/p&gt;
&lt;h4&gt;大质数的获取&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;随机生成一个大数 $x$&lt;/li&gt;
&lt;li&gt;判断 $x$ 是否为质数，一般选用 $Miller-Rabin\text{素性测试}$ 算法判断&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;::url-card{url=&quot;https://oi-wiki.org/math/number-theory/prime/#millerrabin-%E7%B4%A0%E6%80%A7%E6%B5%8B%E8%AF%95&quot;}&lt;/p&gt;
&lt;h3&gt;Passkey怎么验证用户身份&lt;/h3&gt;
&lt;p&gt;首先，用户创建一对密钥，私钥存本地，公钥发给服务器并存服务器里。&lt;/p&gt;
&lt;p&gt;在登录时，后台会给用户发送&lt;code&gt;挑战（随机字符串）&lt;/code&gt;。用户用自己的私钥对&lt;code&gt;挑战&lt;/code&gt;做数字签名，并将签名发送给后台。后台再拿存储的用户公钥做验证，判断签名是否有效。&lt;/p&gt;
&lt;h2&gt;Passkey实践&lt;/h2&gt;
&lt;p&gt;检测你的设备是否支持Passkey：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://webauthn.io/&quot;}&lt;/p&gt;
&lt;h3&gt;流程：简单版&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;已注册用户添加Passkey&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant 前端
    participant 后台

    前端 -&amp;gt;&amp;gt; 后台: 请求注册Passkey
    后台 -&amp;gt;&amp;gt; 前端: 返回注册challenge、rpId、支持认证类型、用户特征等
    前端 -&amp;gt;&amp;gt; 前端: 数据透传至浏览器，调用JS创建Passkey
    前端 --&amp;gt;&amp;gt; 前端: 用户完成创建Passkey操作，由密码管理器/物理认证器创建私钥并保存
    前端 -&amp;gt;&amp;gt; 后台: JS返回注册结构体，里面包含公钥、凭据id等
    后台 -&amp;gt;&amp;gt; 后台: 检查挑战是否有效
    后台 -&amp;gt;&amp;gt; 后台: 存储用户公钥
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;用Passkey登录&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant 前端
    participant 后台

    前端 -&amp;gt;&amp;gt; 后台: 请求用Passkey登录（可携带用户名）
    后台 -&amp;gt;&amp;gt; 前端: 返回登录challenge、rpId、支持的凭据id（如操作1携带用户名请求）
    前端 -&amp;gt;&amp;gt; 前端: 数据透传至浏览器，调用JS获取Passkey认证数据
    前端 --&amp;gt;&amp;gt; 前端: 用户从密码管理器/物理认证器获取私钥，然后生成认证数据
    前端 -&amp;gt;&amp;gt; 后台: JS返回登录结构体，里面包含对challenge的私钥签名、凭据id等
    后台 -&amp;gt;&amp;gt; 后台: 获取Passkey公钥，判断签名、挑战是否有效
    后台 -&amp;gt;&amp;gt; 前端: 登录结果
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;WebAuthN&lt;/h3&gt;
&lt;p&gt;WebAuthN是W3C标准。客户端很多Passkey的实现也是基于WebAuthN。&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Authentication_API&quot;}&lt;/p&gt;
&lt;h3&gt;数据结构与流程分析&lt;/h3&gt;
&lt;h4&gt;注册Passkey&lt;/h4&gt;
&lt;p&gt;客户端请求注册Passkey（用户已登录）：&lt;/p&gt;
&lt;p&gt;客户端 -&amp;gt; 后台：随意&lt;/p&gt;
&lt;p&gt;后台-&amp;gt;客户端:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{  
  &quot;challenge&quot;: &quot;gVQ2n5FCAcksuEefCEgQRKJB_xfMF4rJMinTXSP72E8&quot;,  
  &quot;rp&quot;: {  
    &quot;name&quot;: &quot;Passkey Example&quot;,  
    &quot;id&quot;: &quot;example.com&quot;  
   },  
  &quot;user&quot;: {  
    &quot;id&quot;: &quot;GOVsRuhMQWNoScmh_cK02QyQwTolHSUSlX5ciH242Y4&quot;,  
    &quot;name&quot;: &quot;Michael&quot;,  
    &quot;displayName&quot;: &quot;Michael&quot;  
   },  
  &quot;pubKeyCredParams&quot;: [  
     {  
      &quot;alg&quot;: -7,  
      &quot;type&quot;: &quot;public-key&quot;  
     }  
   ],  
  &quot;timeout&quot;: 60000,  
  &quot;attestation&quot;: &quot;none&quot;,  
  &quot;excludeCredentials&quot;: [  
   ],  
  &quot;authenticatorSelection&quot;: {  
    &quot;authenticatorAttachment&quot;: &quot;platform&quot;,  
    &quot;requireResidentKey&quot;: true,  
    &quot;residentKey&quot;: &quot;required&quot;  
   },  
  &quot;extensions&quot;: {  
    &quot;credProps&quot;: true  
   }  
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rp.name，rp.id：域信息&lt;/li&gt;
&lt;li&gt;user.id：用户特征&lt;/li&gt;
&lt;li&gt;user.name，user.displayName：Passkey展示在用户密码器上的信息&lt;/li&gt;
&lt;li&gt;pubKeyCredParams：支持算法类型&lt;/li&gt;
&lt;li&gt;timeout：challenge有效期时长&lt;/li&gt;
&lt;li&gt;excludeCredentials：不允许生成的凭据id，通常为用户已注册的凭据id&lt;/li&gt;
&lt;li&gt;authenticatorSelection：认证类型，可选物理设备/平台&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;拿到这些数据可以直接透传给系统/浏览器。系统/浏览器会拉起将这些信息透传给密码管理器，让密码管理器创建Passkey。&lt;/p&gt;
&lt;p&gt;密码管理器创建Passkey后，会返回公钥及基础信息给系统，系统再透传给客户端/浏览器。&lt;/p&gt;
&lt;p&gt;客户端-&amp;gt;后台：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{  
 &quot;id&quot;: &quot;base64url-encoded-credential-id&quot;,  
 &quot;type&quot;: &quot;public-key&quot;,  
 &quot;response&quot;: {  
  &quot;clientDataJSON&quot;: &quot;base64url-encoded-client-data-json&quot;,  
  &quot;attestationObject&quot;: &quot;base64url-encoded-attestation-object&quot;  
  }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id：凭据id&lt;/li&gt;
&lt;li&gt;clientDataJSON：base64编码的json数据，json为：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{  
&quot;type&quot;: &quot;webauthn.create&quot;, // 或 &quot;webauthn.get&quot;  
&quot;challenge&quot;: &quot;base64url-encoded-challenge&quot;,  
&quot;origin&quot;: &quot;https://example.com&quot;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;attestationObject：CBOR格式结构体，包含：
&lt;ul&gt;
&lt;li&gt;authData：凭据信息，CBOR格式的Authenticator data，包含公钥、域信息等&lt;/li&gt;
&lt;li&gt;fmt&lt;/li&gt;
&lt;li&gt;attStmt&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data&quot;}&lt;/p&gt;
&lt;p&gt;后台 -&amp;gt; 客户端：随意&lt;/p&gt;
&lt;h4&gt;登录&lt;/h4&gt;
&lt;p&gt;客户端请求登录：&lt;/p&gt;
&lt;p&gt;客户端 -&amp;gt; 后台：(可选)用户特征&lt;/p&gt;
&lt;p&gt;后台-&amp;gt;客户端:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{  
  &quot;challenge&quot;: &quot;x1wRuShyI4k7BqYJi60kVk-clJWsPnBGgh_7z-W9QYk&quot;,  
  &quot;allowCredentials&quot;: [],  
  &quot;timeout&quot;: 60000,  
  &quot;rpId&quot;: &quot;example.com&quot;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;challenge：后台发的挑战&lt;/li&gt;
&lt;li&gt;allowCredentials：允许使用的凭证id，为空表示由用户自己选择。（如果客户端-&amp;gt;后台有发送用户特征，那么这里的allowCredentials应为用户注册过的Passkey凭证id）&lt;/li&gt;
&lt;li&gt;timeout：挑战过期时间&lt;/li&gt;
&lt;li&gt;rpId：域信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;客户端拿到这些数据，应直接透传给系统。系统会透传给密码管理器，让密码管理器通过rpId选择一个Passkey。如果没有Passkey，会弹窗告诉用户没有已注册的Passkey。&lt;/p&gt;
&lt;p&gt;客户端拿到Passkey认证数据后：&lt;/p&gt;
&lt;p&gt;客户端 -&amp;gt; 后台：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;t2hF9lEjZ-8K5oFIZw1wQA&quot;,
  &quot;rawId&quot;: &quot;dDJoRjlMamp6LTdLNWhGSVp3MXdRQQ&quot;,
  &quot;type&quot;: &quot;public-key&quot;,
  &quot;response&quot;: {
    &quot;clientDataJSON&quot;: &quot;eyJjaGFsbGVuZ2UiOiJjaGFsbGVuZ2VleGFtcGxlIiwib3JpZ2luIjoiaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20iLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0&quot;,
    &quot;authenticatorData&quot;: &quot;YWdhdXRoZW50aWNhdG9yRGF0YS5leGFtcGxl&quot;,
    &quot;signature&quot;: &quot;c2lnbmF0dXJlZXhhbXBsZQ&quot;,
    &quot;userHandle&quot;: &quot;dXNlcklkZXh0cmFhcmVv&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id：凭据id&lt;/li&gt;
&lt;li&gt;response.clientDataJSON，response.authenticatorData和注册Passkey时类似&lt;/li&gt;
&lt;li&gt;signature：用Passkey私钥加密的hash(clientDataJSONauthenticatorData)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后台收到数据后，会先对signature解密，得到B，再判断B==hash(clientDataJSONauthenticatorData)&lt;/p&gt;
&lt;p&gt;后台 -&amp;gt; 客户端：随意&lt;/p&gt;
&lt;h3&gt;iOS实现&lt;/h3&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/documentation/authenticationservices/supporting-passkeys&quot;}&lt;/p&gt;
&lt;p&gt;前置条件：&lt;/p&gt;
&lt;p&gt;配置&lt;code&gt;associated domains&lt;/code&gt;，&lt;code&gt;https://{域名}/.well-known/apple-app-site-association&lt;/code&gt;的内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;webcredentials&quot;: {  
    &quot;apps&quot;: [  
      &quot;{团队ID}.{bundleId}&quot;  
     ]  
   }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;怎么获取团队id？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.apple.com/account&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;img src=&quot;img/image-20241220144703062.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;放上去后：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://m.v.qq.com/.well-known/apple-app-site-association&quot;}&lt;/p&gt;
&lt;p&gt;iPhone启动或安装APP时会去拉一遍数据&lt;/p&gt;
&lt;p&gt;需注意的API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ASAuthorizationPlatformPublicKeyCredentialProvider - Passkey注册、认证都要调他&lt;/li&gt;
&lt;li&gt;ASAuthorizationPlatformPublicKeyCredentialRegistration - Passkey注册结果&lt;/li&gt;
&lt;li&gt;ASAuthorizationPlatformPublicKeyCredentialAssertion - Passkey认证结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解析数据：&lt;/p&gt;
&lt;p&gt;ASAuthorizationPlatformPublicKeyCredentialRegistration&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241220033114134-1024x407.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ASAuthorizationPlatformPublicKeyCredentialAssertion&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241220033202337-1024x437.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Android实现&lt;/h3&gt;
&lt;p&gt;::url-card{url=&quot;https://developer.android.com/identity/sign-in/credential-manager?hl=en&quot;}&lt;/p&gt;
&lt;p&gt;Android与Passkey的操作都需用到Credential Manager&lt;/p&gt;
&lt;p&gt;在Android14以前，安卓的密码管理器只能用谷歌密码管理，而谷歌密码管理是包含在GMS里的。如果系统不带GMS，系统就不会有密码管理器，因此国行系统需要安装谷歌服务才能使用Passkey。&lt;/p&gt;
&lt;p&gt;Android14及以后，用户可以安装并使用其它密码管理器了。用户在设置里选择密码器后，如果密码器支持Passkey操作，就可以使用Passkey。&lt;/p&gt;
&lt;h4&gt;Credential Manager&lt;/h4&gt;
&lt;p&gt;前置条件：&lt;/p&gt;
&lt;p&gt;配置&lt;code&gt;Digital Asset Links&lt;/code&gt;，&lt;code&gt;https://{域名}/.well-known/assetlinks.json&lt;/code&gt;的内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[  
   {  
    &quot;relation&quot;: [  
      &quot;delegate_permission/common.handle_all_urls&quot;,  
    &quot;delegate_permission/common.get_login_creds&quot;  
     ],  
    &quot;target&quot;: {  
      &quot;namespace&quot;: &quot;android_app&quot;,  
      &quot;package_name&quot;: &quot;{包名}&quot;,  
      &quot;sha256_cert_fingerprints&quot;: [  
        &quot;{指纹签名}&quot;  
       ]  
     }  
   }  
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很简单，无论是登录还是注册，将后台传的数据透传给系统即可&lt;/p&gt;
&lt;h4&gt;OPPO设备&lt;/h4&gt;
&lt;p&gt;OPPO自己搞了个SDK：&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://open.oppomobile.com/new/developmentDoc/info?id=12759&quot;}&lt;/p&gt;
&lt;p&gt;前置条件：&lt;/p&gt;
&lt;p&gt;配置&lt;code&gt;fido2-trusted-facets&lt;/code&gt;，&lt;code&gt;https://{域名}/.well-known/fido2-trusted-facets.json&lt;/code&gt;的内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[  
   {  
    &quot;relation&quot;: [  
      &quot;delegate_permission/common.get_login_creds&quot;  
     ],  
    &quot;target&quot;: {  
      &quot;namespace&quot;: &quot;android_app&quot;,  
      &quot;package_name&quot;: &quot;{包名}&quot;,  
      &quot;sha256_cert_fingerprints&quot;: [  
        &quot;{指纹签名}&quot;  
       ]  
     }  
   }  
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OPPO Passkey API和Credential Manager差不多。&lt;/p&gt;
&lt;p&gt;咨询OPPO工程师，该API可能很快会被废除：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-20241220034217366-1024x139.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;鸿蒙&lt;/h3&gt;
&lt;p&gt;不支持，别想了&lt;/p&gt;
&lt;h3&gt;后台实现&lt;/h3&gt;
&lt;p&gt;::url-card{url=&quot;https://developers.yubico.com/java-webauthn-server/&quot;}&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;go-webauthn/webauthn&quot;}&lt;/p&gt;
&lt;p&gt;代码略&lt;/p&gt;
&lt;p&gt;遇到的坑点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iOS clientDataJSON.origin为&lt;code&gt;https://{associated domains}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安卓 clientDataJSON.origin为 &lt;code&gt;android:apk-key-hash:{包签名base64}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OPPO clientDataJSON.origin为 &lt;code&gt;android:apk-key-hash:{包签名base64并做urlEncode}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，如果要支持iOS、安卓和OPPO设备的Passkey登录注册，后台需要设置3个origin&lt;/p&gt;
</content:encoded></item><item><title>让NAS的WebDAV完美支持HTTPS</title><link>https://blog.nowcent.cn/posts/webdav-https-for-nas/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/webdav-https-for-nas/</guid><pubDate>Sun, 31 Dec 2023 20:38:01 GMT</pubDate><content:encoded>&lt;p&gt;正好年底了，有点闲钱整个NAS玩玩。我之前有折腾过轻度服务器，所以选NAS的时候特别注重稳定性，选择了极空间的Z2Pro。但是到手后发现它的操作对新手非常友好，对老手不友好。比如有些事情明明可以打几行命令解决，它非要你在GUI上完成。包括这次配HTTPS也是，特此写篇文章记录下。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;为什么要上HTTPS？&lt;/h2&gt;
&lt;p&gt;我有在公网直连WebDAV的需求，基于安全性的考量，是一定要上HTTPS的。&lt;/p&gt;
&lt;h3&gt;HTTP与HTTPS的区别&lt;/h3&gt;
&lt;p&gt;首先得了解什么是HTTP/HTTPS。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP：明文传输。在传输过程有被中间人攻击的可能，即你传输的东西，可能会被别人看到&lt;/li&gt;
&lt;li&gt;HTTPS：密文传输。在连接建立前通过多次握手确定密钥，后面进行加密传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;::url-card{url=&quot;https://www.runoob.com/w3cnote/http-vs-https.html&quot;}&lt;/p&gt;
&lt;h3&gt;WebDAV&lt;/h3&gt;
&lt;p&gt;WebDAV是基于HTTP1.1的通信协议，通过暴露HTTP restful API让你完成对文件的操作。&lt;/p&gt;
&lt;p&gt;WebDAV在连接的时候会让你输入用户名和密码。这个用户名和密码是怎么传到服务器的呢？其实用的是HTTP的Basic Authorization进行鉴权。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    Web-&amp;gt;&amp;gt;Web: 获取用户名&amp;amp;密码
    Web-&amp;gt;&amp;gt;Nas: 传输用户名密码
    Note over Web, Nas: Header Authorization: basic base64({用户名}:{密码})
    Nas-&amp;gt;&amp;gt;Nas: 鉴权
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你想操作NAS，向NAS发起一个请求，浏览器要求你输入用户名和密码。在你输入密码后，浏览器会构造一个字符串：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;basic base64(${\text{用户名}}:${密码})&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个字符串会带在HTTP请求Header的Authorization里。NAS在收到请求后，会对Header的Authorization进行解析。如果用户名密码正确，则进行下一步操作，否则返回403错误码。&lt;/p&gt;
&lt;p&gt;如果用HTTP请求，会有什么问题呢？&lt;/p&gt;
&lt;p&gt;你发的请求是明文的，被人看到后自然可以拿到里面的Header。因为Authorization的构造方式是固定的，而Base64不是加密算法，那就可以推断出请求里的用户名和密码。如果别有用心的人在你请求NAS的路径中“截胡”，将你要发送的包内容记录下来，然后再原封不动地发给NAS，你的用户名和密码就泄漏了。（中间人攻击：重放）&lt;/p&gt;
&lt;p&gt;HTTPS就可以暂时解决这个问题。中间“截胡”的人，截到的内容都是经加密的，因此可有效避免中间人攻击。当然你肯定想问，如果“截胡”的人知道你的加密密钥呢？emm...，这里可能性不大，涉及“鸡生蛋”的问题，有感兴趣的可以看看《计算机网络》这本书。&lt;/p&gt;
&lt;h2&gt;技术实现&lt;/h2&gt;
&lt;p&gt;已知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;极空间会暴露5005端口来提供WebDAV服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，我这里的方案是，路由器在公网暴露5006端口，并转发至内网极空间5006端口；内网极空间5006端口反向代理至自身5005端口。&lt;/p&gt;
&lt;p&gt;所以，我这里的方案是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    外界-&amp;gt;&amp;gt;路由器-5006端口: HTTPS请求至 {域名}:5006
    路由器-5006端口-&amp;gt;&amp;gt;NAS-5006端口: 转发HTTPS请求至 {NAS的IP}:5006
    NAS-5006端口-&amp;gt;&amp;gt;NAS-5005端口: 反向代理到 {NAS的IP}:5005
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nginx Proxy Manager&lt;/h3&gt;
&lt;p&gt;Nginx是一个很好的反向代理工具，但在代理WebDAV时有个坑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它在反向代理时，会删掉一些没用的HTTP Header&lt;/li&gt;
&lt;li&gt;如果Header中带路径，它会自动忽略最后一个&quot;/&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;经实践，就有一点点小问题了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无法对文件重命名&lt;/li&gt;
&lt;li&gt;无法移动文件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;怎么解决呢？如果你用的是普通服务器，只需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给nginx添加&lt;code&gt;headers-more-nginx-module&lt;/code&gt;模块&lt;/li&gt;
&lt;li&gt;重新编译Nginx&lt;/li&gt;
&lt;li&gt;在Nginx配置文件补齐这些缺失的header&lt;/li&gt;
&lt;li&gt;打开Nginx&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但不幸的是，极空间并不能直接编译nginx（呜呜&lt;/p&gt;
&lt;p&gt;Nginx Proxy Manager其实是Nginx + CertBot + UI，所以Nginx有的问题，它肯定有解决方法。方法也很简单嘛，就是重新构建一个带模块的Nginx Proxy Manager镜像&lt;/p&gt;
&lt;p&gt;查看&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/docker/Dockerfile&quot;}&lt;/p&gt;
&lt;p&gt;会发现Nginx是从jc21/nginx-full:certbot-node镜像获取的，因此我们也要构建带模块的jc21/nginx-full:certbot-node&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-1024x404.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看看&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://github.com/NginxProxyManager/docker-nginx-full/blob/master/docker/Dockerfile&quot;}&lt;/p&gt;
&lt;p&gt;会通过./scripts/build-openrestry脚本去编译nginx&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-2-1024x497.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再看看这个脚本，豁然开朗。可以在这里加我们想要的插件了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-5-1024x522.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在build-openresty里，加入&lt;code&gt;headers-more-nginx-module&lt;/code&gt;模块。&lt;/li&gt;
&lt;li&gt;重新构建jc21/nginx-full:certbot-node&lt;/li&gt;
&lt;li&gt;以jc21/nginx-full:certbot-node为父镜像，重新构建jc21/nginx-proxy-manager&lt;/li&gt;
&lt;li&gt;发到公有docker仓库上（比如阿里云），让极空间下载镜像，然后跑起来！&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里有点繁琐，而且我在第一步就失败了，总是提示ubuntu缺少一些组件。&lt;/p&gt;
&lt;h3&gt;Apache&lt;/h3&gt;
&lt;p&gt;没用过，欢迎补充&lt;/p&gt;
&lt;h3&gt;Traefik&lt;/h3&gt;
&lt;p&gt;::url-card{url=&quot;https://doc.traefik.io/traefik/&quot;}&lt;/p&gt;
&lt;p&gt;因为最近玩云原生比较多，想着既然用Nginx太复杂，那就试试Traefik？&lt;/p&gt;
&lt;p&gt;从官网可以看到，Traefik是建议通过Docker Compose部署的，但极空间不支持嘛，所以用Docker CLI也行。。&lt;/p&gt;
&lt;p&gt;从官网可以看到，Traefik推荐给每个Docker容器配动态参数，然后Traefik会去读这些参数，自动给这些Docker容器做反向代理。但极空间不支持嘛，所以用文件配置也行。。&lt;/p&gt;
&lt;p&gt;所以怎么做呢？&lt;/p&gt;
&lt;h4&gt;拉取镜像&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;img/image-7-1024x755.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 对应Docker命令
docker pull traefik:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建/配置容器&lt;/h4&gt;
&lt;p&gt;用上面的镜像创建容器：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-8-1024x804.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-9-1024x809.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-10-1024x806.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面的&lt;code&gt;ALICLOUD_ACCESS_KEY&lt;/code&gt; / &lt;code&gt;ALICLOUD_SECRET_KEY&lt;/code&gt;填你在阿里云的apiKey信息。这里的apiKey需要带上所有域名权限。因为Traefik在从Let&apos;s Encrypt申请SSL证书时，会给你域名的dns里增加一条记录，来确保域名是你的（DNS Challenge）。&lt;/p&gt;
&lt;p&gt;如果没有域名，可以去阿里云淘一个便宜的域名，一年也就十几块钱。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 对应的Docker命令
docker run traefik -v ... -p ... -e ... -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整理下，这里挂载的Volume为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;/Docker/traefik/config (文件夹，放文件动态配置)&lt;/li&gt;
&lt;li&gt;/Docker/traefik/certs (文件夹，放Traefik自动申请的证书)&lt;/li&gt;
&lt;li&gt;/Docker/traefik/traefik.toml （文件，Traefik配置）&lt;/li&gt;
&lt;li&gt;/Docker/traefik/acme.json （文件，证书请求信息）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里打住，&lt;strong&gt;先不要启动容器&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;配置Traefik&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;在挂在目录下创建traefik.toml文件，内容为&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[global]
  checkNewVersion = true
  sendAnonymousUsage = true

[entryPoints]
  [entryPoints.webdav]
    address = &quot;:5006&quot;

[log]
  level = &quot;DEBUG&quot;

[api]

[ping]

[certificatesresolvers.default.acme]
  email = &quot;你的邮箱&quot;
  storage = &quot;/letsencrypt/acme.json&quot;
  dnschallenge.provider = &quot;alidns&quot;

[providers.file]
  directory = &quot;/etc/traefik/config&quot;
  watch = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面配置了什么呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;打开版本检查/发送匿名信息（默认的）&lt;/li&gt;
&lt;li&gt;重点：新加一个入口（entrypoints），监听端口号5006&lt;/li&gt;
&lt;li&gt;log等级为debug（方便排查错误）&lt;/li&gt;
&lt;li&gt;打开api访问（默认）&lt;/li&gt;
&lt;li&gt;打开ping（默认）&lt;/li&gt;
&lt;li&gt;证书信息（在这里填入你的证书请求信息，包括邮箱，域名服务商）&lt;/li&gt;
&lt;li&gt;打开文件provider（默认的provider是docker，这里我关了，即监听容器内目录/etc/traefik/config下所有配置文件的变化）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;在挂载目录/Docker/traefik/config中，新建webdav.yml（随意起名），里面填入：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;http:
  routers:
    webdav:
      entrypoints:
        - webdav
      rule: Host(`你的域名`)
      tls:
        certResolver: default
      service: svc_webdav
  services:
    svc_webdav:
      loadBalancer:
        servers:
          - url: &quot;http://NAS局域网ip:5005&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建一个空的acme.json，移动到/Docker/traefik/里&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动Traefik容器&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到这里就配置成功了。你以为结束了吗？别急，下面有个小坑&lt;/p&gt;
&lt;h4&gt;设置acme.json权限&lt;/h4&gt;
&lt;p&gt;我们ssh到容器内部（小坑：命令用/bin/sh）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-12-1024x752.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 对应Docker命令
docker exec -u root -it traefik sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入容器内部，找到挂载的acme.json文件，将权限设置为600。因为默认权限是777，Traefik会报错；极空间也不支持改变文件权限，所以只能走弯路啦～&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-13-1024x771.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后关闭窗口，重启容器&lt;/p&gt;
&lt;h4&gt;验证&lt;/h4&gt;
&lt;p&gt;在浏览器输入https://{域名}:5006，看看访问是否正常呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在SSL证书过期前，Traefik会自动更新你的证书，确保你的证书不会过期。从外网访问WebDAV时，创建文件、移动文件、文件重命名都一切正常，说明配置成功啦。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Traefik是一个很好用的反向代理工具。&lt;/p&gt;
</content:encoded></item><item><title>Git的一些奇技淫巧</title><link>https://blog.nowcent.cn/posts/git-tips-and-tricks/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/git-tips-and-tricks/</guid><pubDate>Mon, 23 Oct 2023 10:50:24 GMT</pubDate><content:encoded>&lt;h2&gt;Q：你在branch1上开发，代码没写完（还没commit），这时候来了个紧急需求需要你立刻开发：&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;stash一下branch1工作区的变更&lt;/li&gt;
&lt;li&gt;切到新的分支完成开发&lt;/li&gt;
&lt;li&gt;切回到branch1，用stash pop恢复之前到工作区&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Q：force-push什么情况用安全？&lt;/h2&gt;
&lt;p&gt;先搞清楚为什么force-push不安全，会导致全家火葬场的情况：&lt;/p&gt;
&lt;p&gt;你和A在同一个分支上协作。A有个新的提交提到远程仓库上，但你没有update，所以本地没有这个提交。当你用force-push，会把你本地的代码&lt;strong&gt;强制&lt;/strong&gt;覆盖掉远程仓库的代码，导致A的提交消失。&lt;/p&gt;
&lt;p&gt;什么情况下是安全的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分支只有你一个人在用&lt;/li&gt;
&lt;li&gt;与团队成员商量好后，先update再force-push（避免了你update的时候，团队成员提代码的情况）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Q：需要清除已push的某几项commit：&lt;/h2&gt;
&lt;p&gt;如果是连续几项最新的commit，直接将branch reset到【这几个commit中最老的那一个的前一个commit】，然后force push&lt;/p&gt;
&lt;p&gt;如果是任意几项，先在最新的commit引出一个新的本地分支（比如temp-branch），再将原来的分支reset到【这几个commit中最老的那个的前一个commit】，再将之后需要的commit一个个地cp（cherry-pick）到原来的分支上，然后forcepush，再把temp-branch删掉。（当然，如果你想折腾，确实有个方法可以不开新分支完成上面的操作）&lt;/p&gt;
&lt;p&gt;如果不在意git时间线，可以一个一个revert。&lt;/p&gt;
&lt;h2&gt;Q：有好几个commit已推到远程仓库，但想把这几个合并成一个，怎么操作？&lt;/h2&gt;
&lt;p&gt;如果是最新的那几个，那还好，直接squash再force-push即可&lt;/p&gt;
&lt;p&gt;如果是中间那几个，操作过【需要清除已push的某几项commit】这个，你就知道怎么操作了&lt;/p&gt;
&lt;h2&gt;Q：误操作了，导致本地未提交的代码不见了怎么办？&lt;/h2&gt;
&lt;p&gt;巧了，我今天也遇到这个情况。如果你用的是jetbrains家的IDE，用history功能把代码回退到某个时间点；&lt;/p&gt;
&lt;p&gt;如果没用，自求多福吧～&lt;/p&gt;
</content:encoded></item><item><title>被Xcode15坑麻了</title><link>https://blog.nowcent.cn/posts/xcode15-pitfalls/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/xcode15-pitfalls/</guid><pubDate>Thu, 28 Sep 2023 01:38:51 GMT</pubDate><content:encoded>&lt;p&gt;昨天苹果刚发布Sonoma，我就兴致冲冲去升级了，升级后果不其然，Xcode14不能用了，必须升级到15才能用。&lt;/p&gt;
&lt;p&gt;然后事情就来了：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;无限Crash&lt;/h2&gt;
&lt;p&gt;起因是这样的，有一个在Xcode14可以跑的项目，用Xcode15打开就直接crash，没有任何提示&lt;/p&gt;
&lt;p&gt;尝试过：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除缓存，无果&lt;/li&gt;
&lt;li&gt;删除模拟器记录，无果&lt;/li&gt;
&lt;li&gt;重装Xcode，无果&lt;/li&gt;
&lt;li&gt;删除项目缓存（xcuserdata），无果&lt;/li&gt;
&lt;li&gt;删除Pods，重建，无果&lt;/li&gt;
&lt;li&gt;重建Workplace，无果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正当一筹莫展时，我将代码回退到上一个版本，问题就解决了。&lt;/p&gt;
&lt;p&gt;后来经过验证，是xcsharedata里的*.xcscheme改变导致的。但这里的变更基本都是跟Xcode走的。出现不兼容的原因，目前还不清楚。&lt;/p&gt;
&lt;h2&gt;点build没反应&lt;/h2&gt;
&lt;p&gt;上一个问题解决后，高高兴兴打开Xcode，直接点build。然后Xcode报了个错，等我把错误改完后，就发现build/clean按钮点了没反应。如果再多点几下，Xcode就直接freeze了！等强退再进Xcode，就发现哎哟，只要报了一次错，后面就没办法再build/clean了！&lt;/p&gt;
&lt;p&gt;还好电脑装了AppCode。AppCode就没有只能一次build的问题，通过多次build我就把项目里的全部问题改了，然后再回到Xcode里build。直到Xcode里第一次build成功，问题才基本解决。&lt;/p&gt;
</content:encoded></item><item><title>Vision OS 介绍</title><link>https://blog.nowcent.cn/posts/vision-os-introduction/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/vision-os-introduction/</guid><pubDate>Wed, 27 Sep 2023 15:29:57 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;设备/系统介绍&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;img/image001-1024x575.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;硬件&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Optic - 生物认证&lt;/li&gt;
&lt;li&gt;很多摄像头和传感器 - 手部动作识别&lt;/li&gt;
&lt;li&gt;眼球追踪 - 改变焦点&lt;/li&gt;
&lt;li&gt;机身旋钮 - 开启/关闭沉浸度&lt;/li&gt;
&lt;li&gt;机身两侧喇叭 - 放声音，支持空间音频；机身前方显示屏 - 装饰用。。。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能：M2 + R1&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;交互方式&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/image002-1024x466.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image003.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;窗口/控件/物体&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;要操控窗口/控件/物体，交互方式是：&lt;strong&gt;看着一个物体/控件，然后做手势&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SwiftUI默认支持的动作类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手指捏合一下（点击）&lt;/li&gt;
&lt;li&gt;双手捏合并旋转（旋转）&lt;/li&gt;
&lt;li&gt;手指捏合然后移动（拖拽）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他的动作，需要使用ARKit去自定义手部动作识别。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;输入&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;img/image005-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image007-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟键盘&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（手指点在虚拟按键上完成输入？还是用上面跟控件的交互方式？目前不清楚）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接入苹果支持的键盘使用。候选框在实体键盘附近展示。&lt;/li&gt;
&lt;li&gt;手柄&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;系统&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;VisionOS&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image009-1-1024x762.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image011-1-1024x750.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VisionPro&lt;strong&gt;能感知人体的移动&lt;/strong&gt;，虚拟的物体/窗体像是被固定在现实世界一样&lt;/li&gt;
&lt;li&gt;VisionPro会给房间建模、获取房间的光照信息，以显示虚拟窗体的阴影效果&lt;/li&gt;
&lt;li&gt;同一个视角，可以同时显示多个窗口（当然也可以同时显示多个APP）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;strong&gt;构建一个Vision APP&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;&lt;strong&gt;Scene&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;iOS APP里的View是2D的。为了让View做到3D显示，苹果在View上加了一层Scene的概念，用于定义View的展示效果。&lt;/p&gt;
&lt;p&gt;从展示效果上区分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image013-1-1024x341.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Window：主要用于展示平面元素、2D窗体&lt;/li&gt;
&lt;li&gt;Volume：用于展示三维物体&lt;/li&gt;
&lt;li&gt;Immersive Space：沉浸空间，啥都可以展示&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Window&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;平面窗体类型，展示2D元素为主。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image015-1-1024x490.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image017.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;方向轴（假设用户面朝一个窗体）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image019-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;往右：X轴&lt;/p&gt;
&lt;p&gt;往下：Y轴&lt;/p&gt;
&lt;p&gt;往用户方向：Z轴&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Window有一定的Z轴空间，但不能人为调整Z轴的大小&lt;/li&gt;
&lt;li&gt;Window的大小可随用户调整（可以设置最大/最小的大小）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Volume&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;img/image021.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image023.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Z轴指向用户&lt;/li&gt;
&lt;li&gt;独立的3D窗口，主要用于呈现3D模型，可以同时打开多个&lt;/li&gt;
&lt;li&gt;开发人员可以调整大小&lt;/li&gt;
&lt;li&gt;用户不可以调整大小，只能调整位置&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Immersion Space&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;沉浸式空间。可以放Volume或Window。&lt;/p&gt;
&lt;p&gt;同一时间只能打开一个Immersive Space。打开Immservice Space时，其他APP的内容会被隐藏。&lt;/p&gt;
&lt;p&gt;可以改变的沉浸类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;full - 纯VR展示，用户看到的全是虚拟的内容&lt;/li&gt;
&lt;li&gt;mix - 纯MR展示&lt;/li&gt;
&lt;li&gt;progressive - 半MR半VR，通过旋钮调整程度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;img/image025-1024x526.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image027-1024x717.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原点在用户的脚上。&lt;/li&gt;
&lt;li&gt;每个物体有自身的坐标轴。Swift提供了transform方法，将两种坐标轴统一放在ImmserviceSpace中做转换，解决Window和Volume坐标轴不同的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;img/image029-1024x296.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;相关代码&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;定义Scene&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/image031-1024x654.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image033-1024x452.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在API上，苹果偷偷改了immersionStyle的modifier，现在不再以枚举变量区分沉浸类型，改为类似密封类的形式做区分。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;控制Scene&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;引入相关环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Environment(\.openWindow) var openWindow // 根据id打开窗口
@Environment(\.dismissWindow) var dismissWindow // 根据id关闭窗口
@Environment(\.openImmersiveSpace) var openImmersiveSpace // 根据id打开沉浸空间
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace // 关闭当前打开的沉浸空间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面所有的方法都是异步的。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Ornaments&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;一个modifier，用于在&lt;strong&gt;窗体&lt;/strong&gt;附近显示View&lt;/p&gt;
&lt;p&gt;可用于：播放器进度条、快捷开关、快捷tab切换&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image036-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image038-1024x509.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟overlay的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;overlay作用在view上，ornament作用在window上（view的大小可能会和window不一致）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;一些有用的Modifier&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;作用在WindowGroup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.windowResizability - 设置窗体被调整时的大小范围
&lt;ul&gt;
&lt;li&gt;contentSize - 由View的frame属性确定大小最大值、最小值&lt;/li&gt;
&lt;li&gt;contentMinSize - 由View的frame属性确定大小最小值，不设置最大值&lt;/li&gt;
&lt;li&gt;automatic - 系统默认，settings view用`contentSize`，其他用`contentMinSize`&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;.windowStyle - scene类型，上面有&lt;/li&gt;
&lt;li&gt;.defaultSize - 窗体默认大小，支持调整长、宽、高、单位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用在View：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.padding3D - 支持三维的.padding&lt;/li&gt;
&lt;li&gt;.frame(depth) - 支持三维的frame，设置z轴长度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作用在ImmsersiveSpace:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.immersionStyle - 沉浸程度&lt;/li&gt;
&lt;li&gt;.defaultSize - 默认大小&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;strong&gt;RealityView&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;&lt;strong&gt;基本使用&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;E&lt;/strong&gt; Entity - 一个载入的模型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt; Component - 给Entity添加不同的能力
&lt;ul&gt;
&lt;li&gt;InputTargetComponent - 接受输入&lt;/li&gt;
&lt;li&gt;CollisionComponent - 计算碰撞体积&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S&lt;/strong&gt; System - 整个系统，包含多个Entity，在每一帧显示前会通知外界（外界从这里动态调整Entity的属性）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Entity的初始化在make闭包中完成，每一帧的更新会回掉update闭包。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image040-1024x93.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image042.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举个例子&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image044-1-1024x696.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;加载模型&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;img/image050.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let entity = try? await Entity(named: &quot;test&quot;)
let entity2 = try? await Entity(named: &quot;test2&quot;, in: realityKitContentBundle)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;播放动画&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;流程：定义动画类型 =&amp;gt; 创建动画元素 =&amp;gt; Entity播放动画&lt;/p&gt;
&lt;p&gt;播放模型动画：&lt;/p&gt;
&lt;p&gt;下面Demo为了直观，直接用强制非空取代空判断；实际开发中必须要做判空。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let entity = try! await Entity(named: &quot;test&quot;)
let def = entity.availableAnimations[0].definition  // 定义动画类型
let ani = try! AnimationResource.generate(with: AnimationView(source: def))  // 创建动画元素
entity.playAnimation(ani.repeat(count: 100))  // Entity播放动画
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;播放内置动画：&lt;/p&gt;
&lt;p&gt;内置动画默认有FromToBy、Orbit、Sampled三种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FromToBy =&amp;gt; CABasicAnimation&lt;/li&gt;
&lt;li&gt;Sampled =&amp;gt; CAKeyframeAnimation&lt;/li&gt;
&lt;li&gt;Orbit =&amp;gt; 环绕动画&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;let entity = try! await Entity(named: &quot;test&quot;)
let def = OrbitAnimation(...)    // 改了这行，定义动画类型
let ani = try! AnimationResource.generate(with: AnimationView(source: def))  // 创建动画元素
entity.playAnimation(ani.repeat(count: 100)) // Entity播放动画
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;手势&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;img/image051-1024x534.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用法：.onGesture(...)&lt;/p&gt;
&lt;p&gt;与2D的SwiftUI相比，这里多了个targetedAnyEntity方法，意思是System内的所有Entity都能被这个gesture响应。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;targetedAnyEntity - System内任意Entity都响应该手势&lt;/li&gt;
&lt;li&gt;targetedToEntity(_ entity: Entity) - 指定Entity响应该手势&lt;/li&gt;
&lt;li&gt;targetedToEntity(where query: QueryPredicate&amp;lt;Entity&amp;gt;) - 指定某些Entity进行&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;strong&gt;移植&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;&lt;strong&gt;APP直接在VisionOS上使用&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;iOS上的APP可直接在VisionOS上使用，但无法表现VisionOS特有的UI效果，只能展示成一个普通的平面&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;移植公共逻辑类&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;视图只能用SwiftUI写（是否能用OC，有待探究）&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;不可用的API&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Core Motion services&lt;/li&gt;
&lt;li&gt;Barometer and magnetometer data&lt;/li&gt;
&lt;li&gt;All location services except the standard service&lt;/li&gt;
&lt;li&gt;HealthKit data&lt;/li&gt;
&lt;li&gt;Video or still-photo capture&lt;/li&gt;
&lt;li&gt;Camera features like auto-focus or flash&lt;/li&gt;
&lt;li&gt;Rear-facing (selfie) cameras&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Go初探 (5) – 结构体与接口</title><link>https://blog.nowcent.cn/posts/go-intro-5-structs-and-interfaces/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/go-intro-5-structs-and-interfaces/</guid><pubDate>Fri, 13 Nov 2020 14:43:55 GMT</pubDate><content:encoded>&lt;h4&gt;结构体&lt;/h4&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;Go的结构体与C/C++类似，声明方式如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type 结构体名 struct {
    成员声明...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明时如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Student struct{
    id string
    name string
}

func main(){
    //创建一个结构体
    student1 := Student{&quot;123456&quot;, &quot;张三&quot;}

    //可以使用key-value的形式创建
    student2 := Student{id: &quot;123456&quot;, name: &quot;张三&quot;}

    //忽略的字段为0或空
    student3 := Student{name: &quot;张三&quot;}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要访问结构体的数据成员，可直接通过点访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;student1.id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构体也可以作为参数传递&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func method(student Stdent){
    //方法体
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以用指针指向结构体&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type pointer *Student
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pointer = &amp;amp;student1
pointer.id
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;接口&lt;/h4&gt;
&lt;p&gt;与Java的接口一样，Go可以将不同类型中相同的方法提取出来，作为接口。任何其它类型实现了接口里的方法就是实现了这个接口。 接口的定义如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type 接口名 interface{
    方法名() [返回类型]
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接口的实现示例如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

type Dog interface{
    bark()
}

type BigDog struct{

}

type SmallDog struct{

}

func (bigDog BigDog) bark() {
    fmt.Println(&quot;BigDog is barking.&quot;)
}

func (smallDog SmallDog) bark() {
    fmt.Println(&quot;SmallDog is barking.&quot;)
}

func main(){
    var dog Dog
    dog = new(BigDog)
    dog.bark()

    dog = new(SmallDog)
    dog.bark()
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Go初探 (4) – 数组与指针</title><link>https://blog.nowcent.cn/posts/go-intro-4-arrays-and-pointers/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/go-intro-4-arrays-and-pointers/</guid><pubDate>Fri, 06 Nov 2020 14:32:56 GMT</pubDate><content:encoded>&lt;h3&gt;数组&lt;/h3&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;Go语言中提供了数组支持。 数组声明的形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var 数组名 [元素数量] 类型
var array [10] int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想在声明的同时对数组进行初始化，你可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var 数组名 = [元素数量] 类型 {元素...}
var array = [5]{1, 2, 3, 4, 5} int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想偷懒，你还可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var array = [...]{1, 2, 3, 4, 5} int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想使用数组，或给数组元素赋值，可以直接这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array[1] = 5
var array1 int = array[1]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多维数组&lt;/h3&gt;
&lt;p&gt;多维数组的定义形式为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var 数组名 [元素数量1][元素数量2]...[元素数量n] 类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想访问二维数组，你可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var := a[1][2]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数传递数组&lt;/h3&gt;
&lt;p&gt;在函数参数中传递参数，你可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func a(param [10]int) void
func a(param []int) void
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;指针&lt;/h3&gt;
&lt;p&gt;与C/C++一样，Go语言中也提供了指针。 类似地，&amp;amp;为取址运算符，*为指针。 指针的定义形式如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var 变量名 *变量类型
var a *int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有给指针定义指向性，指针的值默认为nil。 如果你想定义指针数组，你可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var 变量名 [元素数量]*变量类型
var a [10]*int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用时&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a1 := *a[1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者如果你想定义嵌套指针，即指针指向指针，你可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var 变量名 **变量类型
var a **int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在函数中指针作为参数传递，可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func swap(a *int, b *int) void{}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Go初探 (3) – 基础语句</title><link>https://blog.nowcent.cn/posts/go-intro-3-basic-statements/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/go-intro-3-basic-statements/</guid><pubDate>Fri, 30 Oct 2020 15:44:12 GMT</pubDate><content:encoded>&lt;h2&gt;基础语句&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h3&gt;条件语句&lt;/h3&gt;
&lt;h4&gt;if&lt;/h4&gt;
&lt;p&gt;if条件语句与Java基本一致，基础语法为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if 表达式 {
    //你的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
import &quot;fmt&quot;

func main(){
    a := 10
    if a &amp;gt; 0 {
        fmt.Print(&quot;123&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也许已经知道了，表达式可以不需要括号。 推测下去，go的if分支条件语句还有如下写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//if...else...
if 表达式 {
    //你的代码
} else {
    //你的代码
}

//if...else if...
if 表达式 {
    //你的代码
} else if 表达式 {
    //你的代码
}

//嵌套if
if 表达式 {
    if 表达式 {
        //你的代码
    } else {
        //你的代码
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;switch&lt;/h4&gt;
&lt;p&gt;与Java不同，go语言里switch的case分支自带break属性，默认在满足条件的情况下，不会执行接下来的分支。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch 表达式 {
    case val1:
        //你的代码
    case val2:
        //你的代码
    default:
        //你的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，与Java不同的是，你还可以这么干&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch {
    case val1 == val2:
        //你的代码
    case val3 == val4:
        //你的代码
    default:
        //你的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;case后可填入条件表达式。当第一处运行为真时，退出分支。 当然了，如果你想在满足条件时，执行接下来剩下的分支，你需要在分支后添加&lt;code&gt;fallthrough&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch {
    case val1 == val2:
        fallthrough
        //你的代码case val3 == val4:
        fallthrough
        //你的代码
    default:
        //你的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fallthrough&lt;/code&gt;在执行剩下的分支时，不会判断剩下分支的表达式是否为真，就如Java中的case分支不加上break。&lt;/p&gt;
&lt;h5&gt;type-switch&lt;/h5&gt;
&lt;p&gt;除此之外，switch语句还可以判断某个接口的类型。如果你还没学到接口，这里可以跳过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

type MyInterface interface{
    call() int32
}

type MyStruct struct {
    name string
}

func (a *MyStruct) call() int32{
    return 1
}

func main() {
    var myInterface MyInterface
    myStruct := new(MyStruct)
    myInterface = myStruct

    switch i := myInterface.(type){
    case *MyStruct:
        fmt.Println(&quot;MyStruct&quot;, i)
    case nil:
        fmt.Println(&quot;nil&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;select&lt;/h4&gt;
&lt;p&gt;select语句与switch类似。但与switch不同的是，case中的表达式必须是一个通信。如果case中没有可运行的，程序将堵塞，直到有case可以运行为止。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select {
    case 通信1:
        //你的代码
    case 通信2:
        //你的代码
    //可选
    //default: 
        //你的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个case必须是一个通信&lt;/li&gt;
&lt;li&gt;如果有多个case可运行，go将公平地随机选取一个case运行&lt;/li&gt;
&lt;li&gt;如果没有case可运行
&lt;ul&gt;
&lt;li&gt;如果没有defalut分支，程序将堵塞&lt;/li&gt;
&lt;li&gt;如果有，将运行default分支&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所有被发送的表达式都会被求值&lt;/li&gt;
&lt;li&gt;如果任意某个通信可以进行，它就执行，其他被忽略&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;循环语句&lt;/h3&gt;
&lt;h4&gt;for&lt;/h4&gt;
&lt;p&gt;for循环有3种形式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for 初始值; 条件; 赋值表达式 {} //Java中的for(初始值; 条件; 赋值表达式){}
for 条件 {} //Java中的while(条件){}
for {} //Java中的while(true){}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以用range格式，相当于Java的foreach。for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for key, value := oldMap {
    newMap[key] = value
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;numbers := []int32{100, 200, 300}
for i, value := numbers {
    fmt.Printf(&quot;%d: %d\n&quot;, i, value)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外，与Java类似，循环分支包括以下几种循环控制语句。&lt;/p&gt;
&lt;h5&gt;break&lt;/h5&gt;
&lt;p&gt;break可以跳出for循环。相同的，你可以使用标签来跳出指定的分支。&lt;/p&gt;
&lt;h5&gt;continue&lt;/h5&gt;
&lt;p&gt;continue可以跳过当前循环，执行下一个循环。&lt;/p&gt;
&lt;h5&gt;goto&lt;/h5&gt;
&lt;p&gt;emmm...这个还是别用了吧&lt;/p&gt;
&lt;h3&gt;函数&lt;/h3&gt;
&lt;p&gt;函数定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func 函数名 (参数) [返回类型] {
   函数体
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func add (a int, b int) int {
    return a + b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main(){
    var b int
    b = add(1, 2)//调用函数
    fmt.Print(b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，函数可以有多个返回值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func swap (a, b int) int{
    return b, a
}

func main(){
    a, b := swap(3, 4)
    fmt.Printf(&quot;%d %d&quot;, a, b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外，你还可以以指针作为参数传递&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func print(str *string){
    *str += &quot;123&quot;
}

func main(){
    str := &quot;456&quot;
    print(&amp;amp;str)
    fmt.Println(str)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;闭包&lt;/h5&gt;
&lt;p&gt;Go 语言支持匿名函数，可作为闭包。匿名函数是一个“内联”语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量，不必申明。 例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func get(i int) func() int{
    i++
    return func() int {
        i += 2
        return i
    }
}

func main(){
    var a int
    a := get(2)
    fmt.Print(a())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;方法&lt;/h5&gt;
&lt;p&gt;go语言中，函数也可变为方法。方法的定义如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (对象名 对象方法) 方法名(参数) [返回类型]{
    方法体
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即，这个对象拥有了这个方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Dog struct{

}

func (dog *Dog) bark(){
    fmt.Print(&quot;I&apos;m barking!&quot;)
}

func main(){
    dog := new(Dog)
    dog.bark();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行后，bark方法被执行。&lt;/p&gt;
</content:encoded></item><item><title>Go初探 (2) - 文件类型、数据类型、常量与变量</title><link>https://blog.nowcent.cn/posts/go-intro-2-types-constants-variables/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/go-intro-2-types-constants-variables/</guid><pubDate>Fri, 23 Oct 2020 15:54:43 GMT</pubDate><content:encoded>&lt;h2&gt;文件结构&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;首先看上一章的实例代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
import &quot;fmt&quot;

func main() {
    fmt.Println(&quot;Hello world!&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第1行，定义这个包的名字为main。注意，main包是程序执行的入口，每个Go项目都应该含有名为main的包。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第2行，引用了fmt包。fmt包实现了系统IO函数（类似于Java中的System）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第4行，定义了main函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第5行，在控制台输出&lt;code&gt;Hello world!&lt;/code&gt;，并换行。与&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fmt.Print(&quot;Hello world!\n&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义相同。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意了，你不能把这里的main函数写成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() 
{
    fmt.Println(&quot;Hello world!&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;花括号在一行开头在Go语言中是不允许的。&lt;/p&gt;
&lt;h2&gt;标记、分隔符与关键字&lt;/h2&gt;
&lt;p&gt;Go 程序可以由多个标记组成，可以是关键字，标识符，常量，字符串，符号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fmt
.
Println
(
&quot;Hello world!&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面每行代表一个标识符。 Go语言中，一行代码为一行语句，不需要分号结尾。如果你打算在一行输入多行语句，你需要在之间加上分号，如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fmt.Println(&quot;Hello world!&quot;);fmt.Println(&quot;Hello world!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下是Go的保留关键字&lt;/p&gt;
&lt;p&gt;break&lt;/p&gt;
&lt;p&gt;default&lt;/p&gt;
&lt;p&gt;func&lt;/p&gt;
&lt;p&gt;interface&lt;/p&gt;
&lt;p&gt;select&lt;/p&gt;
&lt;p&gt;case&lt;/p&gt;
&lt;p&gt;defer&lt;/p&gt;
&lt;p&gt;go&lt;/p&gt;
&lt;p&gt;map&lt;/p&gt;
&lt;p&gt;struct&lt;/p&gt;
&lt;p&gt;chan&lt;/p&gt;
&lt;p&gt;else&lt;/p&gt;
&lt;p&gt;goto&lt;/p&gt;
&lt;p&gt;package&lt;/p&gt;
&lt;p&gt;switch&lt;/p&gt;
&lt;p&gt;const&lt;/p&gt;
&lt;p&gt;fallthrough&lt;/p&gt;
&lt;p&gt;if&lt;/p&gt;
&lt;p&gt;range&lt;/p&gt;
&lt;p&gt;type&lt;/p&gt;
&lt;p&gt;continue&lt;/p&gt;
&lt;p&gt;for&lt;/p&gt;
&lt;p&gt;import&lt;/p&gt;
&lt;p&gt;return&lt;/p&gt;
&lt;p&gt;var&lt;/p&gt;
&lt;p&gt;36个预定义标识符&lt;/p&gt;
&lt;p&gt;append&lt;/p&gt;
&lt;p&gt;bool&lt;/p&gt;
&lt;p&gt;byte&lt;/p&gt;
&lt;p&gt;cap&lt;/p&gt;
&lt;p&gt;close&lt;/p&gt;
&lt;p&gt;complex&lt;/p&gt;
&lt;p&gt;complex64&lt;/p&gt;
&lt;p&gt;complex128&lt;/p&gt;
&lt;p&gt;uint16&lt;/p&gt;
&lt;p&gt;copy&lt;/p&gt;
&lt;p&gt;false&lt;/p&gt;
&lt;p&gt;float32&lt;/p&gt;
&lt;p&gt;float64&lt;/p&gt;
&lt;p&gt;imag&lt;/p&gt;
&lt;p&gt;int&lt;/p&gt;
&lt;p&gt;int8&lt;/p&gt;
&lt;p&gt;int16&lt;/p&gt;
&lt;p&gt;uint32&lt;/p&gt;
&lt;p&gt;int32&lt;/p&gt;
&lt;p&gt;int64&lt;/p&gt;
&lt;p&gt;iota&lt;/p&gt;
&lt;p&gt;len&lt;/p&gt;
&lt;p&gt;make&lt;/p&gt;
&lt;p&gt;new&lt;/p&gt;
&lt;p&gt;nil&lt;/p&gt;
&lt;p&gt;panic&lt;/p&gt;
&lt;p&gt;uint64&lt;/p&gt;
&lt;p&gt;print&lt;/p&gt;
&lt;p&gt;println&lt;/p&gt;
&lt;p&gt;real&lt;/p&gt;
&lt;p&gt;recover&lt;/p&gt;
&lt;p&gt;string&lt;/p&gt;
&lt;p&gt;true&lt;/p&gt;
&lt;p&gt;uint&lt;/p&gt;
&lt;p&gt;uint8&lt;/p&gt;
&lt;p&gt;uintptr&lt;/p&gt;
&lt;h2&gt;赋值、运算符&lt;/h2&gt;
&lt;h3&gt;赋值&lt;/h3&gt;
&lt;p&gt;Go语言中赋值非常简单，语法为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var name type
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，定义后，你一定需要使用它们，或给它们赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想在一行内赋值，你可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var name = value //由编译器决定变量类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，以下用法用得比较多。:=用于初始化并赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name := value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想声明多变量，可以这么做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var name1, name2, name3 type
name1, name2, name3 = value1, value2, value3

var name1, name2, name3 = value1, value2, value3

name1, name2, name3 := value1, value2, value3

var (
    vname1 v_type1
    vname2 v_type2
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;_被用于抛弃值，如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_, name := 3, 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则3的值被抛弃。&lt;/p&gt;
&lt;h3&gt;运算&lt;/h3&gt;
&lt;p&gt;通用的运算符大致与Java一致。 特殊运算符：&lt;/p&gt;
&lt;p&gt;运算符&lt;/p&gt;
&lt;p&gt;描述&lt;/p&gt;
&lt;p&gt;实例&lt;/p&gt;
&lt;p&gt;&amp;amp;&lt;/p&gt;
&lt;p&gt;返回变量存储地址&lt;/p&gt;
&lt;p&gt;&amp;amp;a; 将给出变量的实际地址。&lt;/p&gt;
&lt;p&gt;*&lt;/p&gt;
&lt;p&gt;指针变量。&lt;/p&gt;
&lt;p&gt;*a; 是一个指针变量&lt;/p&gt;
&lt;h2&gt;变量类型&lt;/h2&gt;
&lt;p&gt;Go语言的变量类型与Java中有一些不相同，主要分为4类：布尔类型，数字类型，字符串类型，派生类型。&lt;/p&gt;
&lt;h3&gt;数字类型&lt;/h3&gt;
&lt;p&gt;描述数字的特征，与Java中int与long大致相同。如果你学过C/C++，你应该对无符号数很了解。&lt;/p&gt;
&lt;p&gt;序号&lt;/p&gt;
&lt;p&gt;类型和描述&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;uint8&lt;/strong&gt; 无符号 8 位整型 (0 到 255)&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;uint16&lt;/strong&gt; 无符号 16 位整型 (0 到 65535)&lt;/p&gt;
&lt;p&gt;3&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;uint32&lt;/strong&gt; 无符号 32 位整型 (0 到 4294967295)&lt;/p&gt;
&lt;p&gt;4&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;uint64&lt;/strong&gt; 无符号 64 位整型 (0 到 18446744073709551615)&lt;/p&gt;
&lt;p&gt;5&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;int8&lt;/strong&gt; 有符号 8 位整型 (-128 到 127)&lt;/p&gt;
&lt;p&gt;6&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;int16&lt;/strong&gt; 有符号 16 位整型 (-32768 到 32767)&lt;/p&gt;
&lt;p&gt;7&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;int32&lt;/strong&gt; 有符号 32 位整型 (-2147483648 到 2147483647)&lt;/p&gt;
&lt;p&gt;8&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;int64&lt;/strong&gt; 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)&lt;/p&gt;
&lt;p&gt;浮点型&lt;/p&gt;
&lt;p&gt;序号&lt;/p&gt;
&lt;p&gt;类型和描述&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;float32&lt;/strong&gt; IEEE-754 32位浮点型数&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;float64&lt;/strong&gt; IEEE-754 64位浮点型数&lt;/p&gt;
&lt;p&gt;3&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;complex64&lt;/strong&gt; 32 位实数和虚数&lt;/p&gt;
&lt;p&gt;4&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;complex128&lt;/strong&gt; 64 位实数和虚数&lt;/p&gt;
&lt;p&gt;其它&lt;/p&gt;
&lt;p&gt;序号&lt;/p&gt;
&lt;p&gt;类型和描述&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;byte&lt;/strong&gt; 类似 uint8&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;rune&lt;/strong&gt; 类似 int32&lt;/p&gt;
&lt;p&gt;3&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;uint&lt;/strong&gt; 32 或 64 位&lt;/p&gt;
&lt;p&gt;4&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;int&lt;/strong&gt; 与 uint 一样大小&lt;/p&gt;
&lt;p&gt;5&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;uintptr&lt;/strong&gt; 无符号整型，用于存放一个指针&lt;/p&gt;
&lt;h3&gt;布尔类型&lt;/h3&gt;
&lt;p&gt;类似于Java中的boolean，只能存true或false。&lt;/p&gt;
&lt;h3&gt;字符串类型&lt;/h3&gt;
&lt;p&gt;默认为UTF8编码。&lt;/p&gt;
&lt;h3&gt;派生类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;(a) 指针类型（Pointer）&lt;/li&gt;
&lt;li&gt;(b) 数组类型&lt;/li&gt;
&lt;li&gt;(c) 结构化类型(struct)&lt;/li&gt;
&lt;li&gt;(d) Channel 类型&lt;/li&gt;
&lt;li&gt;(e) 函数类型&lt;/li&gt;
&lt;li&gt;(f) 切片类型&lt;/li&gt;
&lt;li&gt;(g) 接口类型（interface）&lt;/li&gt;
&lt;li&gt;(h) Map 类型&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;定义常量&lt;/h2&gt;
&lt;p&gt;类似于Java中的const，Go的常量可以通过如下方法声明。其中type可省略，由编译器判断变量类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const name [type] = value
const name1, name2 [type] = value1, value2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;iota&lt;/h3&gt;
&lt;p&gt;iota可以理解为程序常量的计数器。在const关键词出现时被重置与0。在一个常量声明时，iota的值加1。如下面代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const (
    a = iota //0
    b = iota //1
    c = iota //2
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码可简写为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const (
    a = iota //0
    b //1
    c //2
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这么写，下一个常量的值来自上一个中的iota值加一，再带入到上一个常量运算表达式的值。举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const (
    a = iota //0
    b = 2 * iota //2
    c //4
    d = iota * iota //9
    e //25
)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Go初探 (1) - 环境搭建</title><link>https://blog.nowcent.cn/posts/go-intro-1-setup/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/go-intro-1-setup/</guid><pubDate>Fri, 23 Oct 2020 14:55:40 GMT</pubDate><content:encoded>&lt;h2&gt;环境搭建&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;打开&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://golang.google.cn/dl/&quot;}&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2020-10-23-142350.png&quot; alt=&quot;&quot; /&gt; 选择相应的操作系统版本，下载程序，并进行安装。 在命令行输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果提示 &lt;code&gt;&apos;go&apos; 不是内部或外部命令，也不是可运行的程序或批处理文件。&lt;/code&gt;，则需要在系统配置环境变量。 &lt;img src=&quot;img/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2020-10-23-143031.png&quot; alt=&quot;&quot; /&gt; 在PATH中添加你的go/bin路径。 然后前往&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://www.jetbrains.com/go/&quot;}&lt;/p&gt;
&lt;p&gt;下载Goland。如果你有学生账户，可免费授权，或免费使用30天。&lt;/p&gt;
&lt;h2&gt;创建第一个Go项目&lt;/h2&gt;
&lt;p&gt;打开Goland，选择New Project &lt;img src=&quot;img/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2020-10-23-143458.png&quot; alt=&quot;&quot; /&gt; 如果不用更改路径名称，直接点击Create &lt;img src=&quot;img/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2020-10-23-143702.png&quot; alt=&quot;&quot; /&gt; 在目录下创建文件Test.go，在Test.go输入以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
import &quot;fmt&quot;

func main() {
    fmt.Println(&quot;Hello world!&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;img/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2020-10-23-144002.png&quot; alt=&quot;&quot; /&gt; 直接点击main方法运行 &lt;img src=&quot;img/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2020-10-23-144121.png&quot; alt=&quot;&quot; /&gt; 好的，你的第一个Go程序跑起来了！&lt;/p&gt;
</content:encoded></item><item><title>腾讯云短信的使用</title><link>https://blog.nowcent.cn/posts/tencent-cloud-sms-guide/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/tencent-cloud-sms-guide/</guid><pubDate>Thu, 22 Oct 2020 16:12:13 GMT</pubDate><content:encoded>&lt;p&gt;首先你需要开通&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://console.cloud.tencent.com/smsv2&quot;}&lt;/p&gt;
&lt;p&gt;开通后，默认会每个月送100条短信。&lt;/p&gt;
&lt;p&gt;开通后，点击&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://console.cloud.tencent.com/smsv2/guide&quot;}&lt;/p&gt;
&lt;p&gt;按照教程里进行操作。好的，教程到此结束，谢谢大家的观看（逃&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;1.创建签名&lt;/h2&gt;
&lt;p&gt;点击侧边栏&lt;strong&gt;xx短信-&amp;gt;签名管理&lt;/strong&gt; &lt;img src=&quot;img/4AEIVSUE9AOWX05T0QP.png&quot; alt=&quot;&quot; /&gt; 然后点击&lt;strong&gt;创建签名&lt;/strong&gt; &lt;img src=&quot;img/NSF2PHNDLLRDABIX8D.png&quot; alt=&quot;&quot; /&gt; 然后填写认证信息。可选择类型有网站、APP、公众号、小程序，然后对类型进行认证。审核在2小时内处理完毕。 &lt;img src=&quot;img/9I5NLJ@QNVFMW1F5GM07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2. 创建模板&lt;/h2&gt;
&lt;p&gt;点击侧边栏&lt;strong&gt;xx短信-&amp;gt;正文模板管理&lt;/strong&gt; &lt;img src=&quot;img/T55PPMGS8VVVU0XR.png&quot; alt=&quot;&quot; /&gt; 点击&lt;strong&gt;创建正文模板&lt;/strong&gt; &lt;img src=&quot;img/A829EFLU3JY1WFLGY7.png&quot; alt=&quot;&quot; /&gt; 然后按照要求进行填写即可。这里审核较签名宽松，只要不违法，基本都是通过。审核在2小时内处理完毕。 &lt;img src=&quot;img/Q@2SCXNELJ_ZU13P8S6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3.测试群发&lt;/h2&gt;
&lt;p&gt;点击侧边栏&lt;strong&gt;xx短信-&amp;gt;群发短信&lt;/strong&gt;，然后点击&lt;strong&gt;创建群发任务&lt;/strong&gt; &lt;img src=&quot;img/PEM1@JYBZXM9Z5FQ.png&quot; alt=&quot;&quot; /&gt; 选择签名、模板、发送时间，按照不同类型填写发送对象。 如果你的短信模板是带变量的，发送对象只能填上传接收号码，然后上传一个excel文件。 &lt;img src=&quot;img/VEUS091MVDLMOVFEE3F.png&quot; alt=&quot;&quot; /&gt; 点击&lt;strong&gt;文件&lt;/strong&gt;下&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://upload-dianshi-1255598498.file.myqcloud.com/smsv2-tpl-zh-20200924-75d09111b4834ee1ff8a60b9414f8a5046b5c0bc.xlsx&quot;}&lt;/p&gt;
&lt;p&gt;下载官方excel模板，然后按照自己的模板变量进行更改。 最后点击&lt;strong&gt;确定&lt;/strong&gt;，创建群发任务。注意，群发任务审核后才能进行。&lt;/p&gt;
&lt;h2&gt;4.通过Java SDK群发短信&lt;/h2&gt;
&lt;p&gt;::url-card{url=&quot;https://cloud.tencent.com/document/product/382&quot;}&lt;/p&gt;
&lt;p&gt;首先在&lt;strong&gt;应用管理-&amp;gt;应用列表&lt;/strong&gt;找到你创建短信签名、模板的应用，记下SDKAppID。 在&lt;/p&gt;
&lt;p&gt;::url-card{url=&quot;https://console.cloud.tencent.com/cam/capi&quot;}&lt;/p&gt;
&lt;p&gt;创建、获取你的SecretId与SecretKey。 创建项目，并添加Maven依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
     &amp;lt;groupId&amp;gt;com.tencentcloudapi&amp;lt;/groupId&amp;gt;
     &amp;lt;artifactId&amp;gt;tencentcloud-sdk-java&amp;lt;/artifactId&amp;gt;
     &amp;lt;version&amp;gt;3.1.62&amp;lt;/version&amp;gt;&amp;lt;!-- 注：这里只是示例版本号，请获取并替换为 最新的版本号 --&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参照腾讯云文档，添加如下代码发送短信&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20190711.SmsClient;
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;


public class Demo
{
    public static void main( String[] args )
    {
        try {
            Credential cred = new Credential(&quot;刚刚获取的secretId&quot;, &quot;刚刚获取的secretKey&quot;);

            HttpProfile httpProfile = new HttpProfile();
            httpProfile.setReqMethod(&quot;POST&quot;);
            httpProfile.setConnTimeout(60);
            httpProfile.setEndpoint(&quot;sms.tencentcloudapi.com&quot;);

            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setSignMethod(&quot;HmacSHA256&quot;);
            clientProfile.setHttpProfile(httpProfile);
            SmsClient client = new SmsClient(cred, &quot;&quot;,clientProfile);
            SendSmsRequest req = new SendSmsRequest();

            String appid = &quot;刚刚获取的SDKAppID&quot;;
            req.setSmsSdkAppid(appid);

            String sign = &quot;你刚刚申请的签名&quot;;
            req.setSign(sign);

            /* 国际/港澳台短信 senderid: 国内短信填空，默认未开通，如需开通请联系 [sms helper] */
            String senderid = &quot;&quot;;
            req.setSenderId(senderid);

            /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息，server 会原样返回 */
            String session = &quot;&quot;;
            req.setSessionContext(session);

            /* 短信码号扩展号: 默认未开通，如需开通请联系 [sms helper] */
            String extendcode = &quot;&quot;;
            req.setExtendCode(extendcode);

            String templateID = &quot;你刚刚申请的模板ID&quot;;
            req.setTemplateID(templateID);

            /* 下发手机号码，采用 e.164 标准，+[国家或地区码][手机号]
             * 例如+8613711112222， 其中前面有一个+号 ，86为国家码，13711112222为手机号，最多不要超过200个手机号*/
            String[] phoneNumbers = {&quot;+8621212313123&quot;, &quot;+8612345678902&quot;, &quot;+8612345678903&quot;};
            req.setPhoneNumberSet(phoneNumbers);

            /* 模板参数: 若无模板参数，则设置为空*/
            String[] templateParams = {&quot;5678&quot;};
            req.setTemplateParamSet(templateParams);

            /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
             * 返回的 res 是一个 SendSmsResponse 类的实例，与请求对象对应 */
            SendSmsResponse res = client.SendSms(req);

            // 输出 JSON 格式的字符串回包
            System.out.println(SendSmsResponse.toJsonString(res));

        } catch (TencentCloudSDKException e) {
            e.printStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，模板参数对应的是一条短信。如果你需要对不同的手机号码发送不同的短信，你需要多次调用这个发送短信的方法。 然后，运行程序。如果参数正确，接受端立刻可以收到短信。&lt;/p&gt;
</content:encoded></item><item><title>Jenkins + Maven + Github/Gitlab + Springboot/Vue.js 实现自动化部署</title><link>https://blog.nowcent.cn/posts/jenkins-maven-github-gitlab-springboot-vue-automation-deploy/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/jenkins-maven-github-gitlab-springboot-vue-automation-deploy/</guid><pubDate>Tue, 01 Sep 2020 17:32:53 GMT</pubDate><content:encoded>&lt;h2&gt;Jenkins的安装&lt;/h2&gt;
&lt;p&gt;::url-card{url=&quot;https://www.jenkins.io/zh/doc/&quot;}&lt;/p&gt;
&lt;p&gt;Jenkins在docker环境下安装非常简单。只需要执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#创建网络
docker network create jenkins

#创建容器卷
docker volume create jenkins-docker-certs
docker volume create jenkins-data

#让Jenkins跑起来
docker run 
  -u root 
  --rm 
  -d 
  -p 8080:8080 
  -p 50000:50000 
  -v jenkins-data:/var/jenkins_home 
  -v /var/run/docker.sock:/var/run/docker.sock 
  jenkinsci/blueocean
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;-p 8080:8080 docker映射端口号，这里是访问Jenkins的端口号&lt;/li&gt;
&lt;li&gt;-v jenkins-data:/var/jenkins_home 卷，Jenkins持久数据存储地址&lt;/li&gt;
&lt;li&gt;-v /var/run/docker.sock:/var/run/docker.sock 卷，映射宿主机的docker到容器内部&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Jenkins的环境配置&lt;/h2&gt;
&lt;h3&gt;1. 打开服务器Jenkins的网页&lt;/h3&gt;
&lt;p&gt;如果你没有更改端口号，那么这个地址是&lt;strong&gt;你的服务器ip:8080&lt;/strong&gt;。注意服务器需要打开相应的安全组配置和防火墙设置。然后你会看到这个&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/20190626082959503.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解锁Jenkins&lt;/p&gt;
&lt;p&gt;（这个图是网上找的，除了地址以外无任何区别）&lt;/p&gt;
&lt;p&gt;进入Jenkins的docker容器，找到密码，复制到上面即可。&lt;/p&gt;
&lt;h3&gt;2. 选择插件，一般选安装推荐的插件。&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;img/20190626083014442.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/20190626083025772.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/20190626083038377.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（这些图也是在网上找的）&lt;/p&gt;
&lt;p&gt;你可以选择创建管理员账户，也可以不创建，直接点击“使用admin账户继续”。这时候登录名是admin，密码是你刚刚复制的一长串字符。&lt;/p&gt;
&lt;h3&gt;3. 配置镜像地址&lt;/h3&gt;
&lt;p&gt;点击&lt;strong&gt;系统管理-&amp;gt;插件管理-&amp;gt;高级&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/DQBP2H7YQ1TM0Y2IDA-1024x507.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/1JX5YJ@KGPCIPMK6BZ5E_U-1024x501.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/QZRMHQOO@X3CE0U2-1024x498.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;升级站点改成清华大学镜像地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后点击“提交”&lt;/p&gt;
&lt;h3&gt;4. 安装相应的插件&lt;/h3&gt;
&lt;p&gt;选择&lt;strong&gt;可选插件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/GOQXWQ5_7N51J7U@T1-1024x577.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你需要部署SpringBoot项目，需要安装Maven插件；&lt;/p&gt;
&lt;p&gt;如果你需要部署NodeJS项目（如Vue.js），需要安装nodejs插件；&lt;/p&gt;
&lt;p&gt;如果你需要从gitlab拉取代码，需要安装gitlab及gitlab hook插件。&lt;/p&gt;
&lt;p&gt;然后点击“直接安装”。&lt;/p&gt;
&lt;p&gt;安装完成后记得重启Jenkins。&lt;/p&gt;
&lt;h3&gt;5. 配置全局工具&lt;/h3&gt;
&lt;p&gt;进入&lt;strong&gt;系统管理-&amp;gt;全局工具配置&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;5.1.1 安装JDK&lt;/h4&gt;
&lt;p&gt;进入docker容器中，输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo $JAVA_HOME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制JAVA路径，备用。&lt;/p&gt;
&lt;p&gt;在刚刚的页面点击“新增JDK”，并取消“自动安装”。在JAVA_HOME输入刚刚复制的路径。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/H1_TI2RUCWEJ9S8-1024x329.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5.1.2 安装Maven及NodeJS&lt;/h4&gt;
&lt;p&gt;直接点击“新增Maven”“新增Node JS”即可，记得输入Name。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/47SI_THVE0RTW3Y8PJ-1024x435.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/A0@5VFO5UWFWV47-1024x574.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后记得点击保存。&lt;/p&gt;
&lt;h2&gt;Jenkins的用户配置&lt;/h2&gt;
&lt;p&gt;如果你的代码存储在Gitlab中，记得配置全局用户信息。&lt;/p&gt;
&lt;p&gt;先进入Gitlab中，生成一个private access token&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/VGH1TN2YNVS4S5LRFQIY-1024x495.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/XY5CNGYY8BIDAX55MXW-1024x503.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择生成后，复制token，备用。&lt;/p&gt;
&lt;p&gt;接下来到Jenkins部分&lt;/p&gt;
&lt;p&gt;进入&lt;strong&gt;系统管理-&amp;gt;系统配置&lt;/strong&gt;，找到Gitlab&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/TGJRXX_I5Z2I4DSW74-1024x425.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一般是没有Credentials的，可以新增一个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/DKP_O606DQ3DM4FO3UK-1024x525.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的API token填写刚刚在Gitlab复制的token&lt;/p&gt;
&lt;p&gt;最后记得保存。&lt;/p&gt;
&lt;h2&gt;创建一个储存在Github的Maven（SpringBoot）项目流水线&lt;/h2&gt;
&lt;p&gt;首先在项目根目录下创建Dockerfile，注意这个Dockerfile可以按照自己的需求更改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM java:8
ADD target/*.jar appName.jar
VOLUME /tmp
EXPOSE 9010
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;appName.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Jenkins创建新的任务&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-1024x518.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后填写自己的Github项目地址&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-2-1024x639.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后源码管理选择git，输入自己的git地址。指定分支选择自己需要构建的分支。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-3-1024x565.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你没有认证，添加一个即可。可以使用账号密码或ssh认证。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-4-1024x522.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;触发器选择GitHub钩子。当然，选择轮询也是可行的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-5-1024x642.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后build步骤，记得选择需要构建的pom文件，及构建需要执行的maven指令&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-6-1024x223.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在构建后的步骤选择执行shell&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为这里用容器启动springboot项目，shell的作用是根据项目的Dockerfile创建一个运行项目的容器。注意，复制下面的shell时，需要更改生成的项目路径、容器名及映射端口号，请按照实际填写。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-8-1024x653.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /var/jenkins_home/workspace/test/

img_output=test

# 先删除之前的容器
echo &quot;remove old container&quot;
# docker ps -a  grep $img_output  awk &apos;{print $1}&apos;

if docker ps -agrep -i volleyball;then 
docker rm -f volleyball
fi

# 删除之前的镜像
echo &quot;remove old image&quot;
docker rmi -f volleyball

# 构建镜像
docker build -t $img_output .

# 打印当前镜像
echo &quot;current docker images&quot;
docker images  grep $img_output
# 启动容器
echo &quot;start container&quot;
docker run --name $img_output -p 9010:9010 -d $img_output
# 打印当前容器
echo &quot;current container&quot;
docker ps -a  grep $img_output
echo &quot;start service success!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后回到Github，我们把钩子设置一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-9-1024x506.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-10-1024x717.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;URL填写按照上面的填写，为 服务器ip:端口/github-webhook/，注意Content-type选择json。然后选择添加。&lt;/p&gt;
&lt;p&gt;最后试试push。然后回到Jenkins，看看构建结果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/image-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果显示为蓝色，然后在浏览器启动项目也成功，那就构建成功啦！&lt;/p&gt;
&lt;p&gt;（Vue部分待更新有时间再写qwq）&lt;/p&gt;
</content:encoded></item><item><title>测光：感受光之美</title><link>https://blog.nowcent.cn/posts/metering-the-beauty-of-light/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/metering-the-beauty-of-light/</guid><pubDate>Sun, 28 Jun 2020 14:37:04 GMT</pubDate><content:encoded>&lt;p&gt;测光是啥？比如你要拍摄的地方有明有暗，而你想突出某个地方，这个时候就要你自己设置测光值了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;虽然EV可以人为干预测光，但在某些对比比较强的地方EV也无能为力，这个时候就需要更改测光模式了。&lt;/p&gt;
&lt;h2&gt;点测光&lt;/h2&gt;
&lt;p&gt;顾名思义就是你选定一个点进行测光。一般而言，这个点和你的对焦点是大致相同的。&lt;/p&gt;
&lt;p&gt;优势在于精准，能够凸显自己想拍部分。&lt;/p&gt;
&lt;h2&gt;多重测光&lt;/h2&gt;
&lt;p&gt;多重测光也是评价测光，是用得最多的测光模式之一。该模式下，相机将从多个区域进行判断然后平均测光。优势在于平均各个明、暗部分，让光比大的部分都能换元细节。&lt;/p&gt;
&lt;h2&gt;中心测光&lt;/h2&gt;
&lt;p&gt;点测光的一种，对中心点的范围进行判断测光。&lt;/p&gt;
&lt;h2&gt;区域测光&lt;/h2&gt;
&lt;p&gt;对某个区域进行判断测光。&lt;/p&gt;
&lt;h2&gt;高光&lt;/h2&gt;
&lt;p&gt;该模式下将会降低曝光，适用于拍曝光下的剪影。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;现阶段我了解测光模式也不是太多，主要在拍背光剪影会改变测光，其它一般一律使用”多重“。希望在接下来的学习中可以感受到不同测光模式的魅力。&lt;/p&gt;
</content:encoded></item><item><title>P、S、M、A四个档位是干啥的？</title><link>https://blog.nowcent.cn/posts/psma-modes-explained/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/psma-modes-explained/</guid><pubDate>Wed, 24 Jun 2020 23:11:25 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;今天闲得无聊去了一趟笔架山。到山顶发现自己带了充电宝没带数据线，然后手机电量剩余百分之20。于是赶紧下山，跑进地铁站，进站时手机刚好没电关机。后面惊喜地发现手机关机也可以用NFC，于是愉快地进入了地铁，过了几个站发现自己坐反方向。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;自动档&lt;/h2&gt;
&lt;p&gt;跟手机拍照一样，使用自动档时相机将自动设置曝光三要素、测光、色彩，这个时候你不能改变其它参数，也就是你把相机当成手机拍摄照片了。（虽然技术不咋地，但画质肯定还是比手机好的。不服？你手机的底有多大？）&lt;/p&gt;
&lt;p&gt;好处是这样适合新手以及进行快速拍摄，但缺点也很明显：相机自行设置的参数可能你不喜欢。&lt;/p&gt;
&lt;h2&gt;P档&lt;/h2&gt;
&lt;p&gt;又称程序曝光模式，是自动档的降级模式。在该档，你可以自行设置ISO和曝光补偿，甚至可以微调光圈大小以及快门。&lt;/p&gt;
&lt;p&gt;好处和自动档一样，适合刚入门不久的新手以及抓拍。但缺点也很明显，一是你要准确调节ISO及EV，否则照片容易过曝或欠曝；二是自动设置的光圈或快门参数你又可能不喜欢。&lt;/p&gt;
&lt;h2&gt;S档&lt;/h2&gt;
&lt;p&gt;S档又称为快门优先模式。这个时候你可以自行设置快门大小，将光圈、ISO交给相机进行控制（当然ISO可调）。EV也处于可调状态，简单来说这个时候你只要关注快门就可以了。&lt;/p&gt;
&lt;p&gt;这个档位适合需要准确调节快门速度时，比如对高速运动的物体进行拍摄、或拍摄车流等慢门。但缺点也是特别明显，光圈你是不可自行调整的；并且如果你设置的快门速度超过了相机的承受能力，这时ISO会升高，然后享受被噪点支配的乐趣吧。&lt;/p&gt;
&lt;h2&gt;A档&lt;/h2&gt;
&lt;p&gt;A档又称为光圈优先模式。这个是摄影师最常用的模式，适合对一般的物体、人像进行拍摄。这个时候相机会根据你设置的光圈值自动设置其它三要素，所以你只需要关心光圈大小。而光圈大小一般情况下控制着景深，所以你可以自行控制景深深浅。对于人像，一般使用大光圈拍摄；而对于日光下的景色，一般采用小光圈进行拍摄。与S档一样，你也可以自行调节ISO及EV，达到个性化的需求。&lt;/p&gt;
&lt;p&gt;优点刚刚说了一大堆，因为这也是我最常用的模式（因为菜）。但缺点也很明显，在超弱光条件下相机设置的快门时间可能非常长。如果你手持拍摄，不用拍了，我不信你不会手抖。&lt;/p&gt;
&lt;p&gt;例如下面这幅图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/DSC00440-1024x683.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;笔架山小亭子 f4 1/4 ISO12800&lt;/p&gt;
&lt;p&gt;这是我今晚下山时拍的。知道为啥拍虚了吧。这个真不是我肾虚，我是真的控制不了，控制不了...&lt;/p&gt;
&lt;h2&gt;M档&lt;/h2&gt;
&lt;p&gt;M档是全手动模式，手机上一般称为专业模式，一般而言大师必会。这个模式下曝光三要素完全需要自行调整。这个模式适合拍摄悠闲的、静态的物品及风景。&lt;/p&gt;
&lt;p&gt;好处很明显，毕竟越自由的东西大家都说好；缺点也很明显，比如你要拍鸟或田径运动员，你在慢悠悠地调参数？或者说你要拍模特，模特需要换个地方取景，你又要慢悠悠重新设置一遍参数。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;不管怎么样，适合的档位才能拍出最好的照片。有些人讽刺用自动档的人，说不定他们也不知道三要素的设置，只是自己想装装而已。真正的大师，不会听信他人的嘲讽，只用适合自己的东西，毕竟摄影是一个人享受美的过程。&lt;/p&gt;
</content:encoded></item><item><title>摄影中的曝光三要素</title><link>https://blog.nowcent.cn/posts/exposure-triangle-in-photography/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/exposure-triangle-in-photography/</guid><pubDate>Wed, 24 Jun 2020 12:31:04 GMT</pubDate><content:encoded>&lt;p&gt;一切摄影都离不开曝光。不同的作品，为了凸显不同的主题，曝光方式往往不同。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;曝光三要素：光圈、快门、ISO&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;光圈&lt;/h2&gt;
&lt;p&gt;光圈衡量镜头进入的光线，用F表示。不同大小的光圈可以控制在单位时间内，进入镜头光线的多少。&lt;/p&gt;
&lt;p&gt;F值=镜头焦距/镜头光圈直径（通光口直径）&lt;/p&gt;
&lt;p&gt;其中数值越大，光圈越小；相应地，数值越小，光圈越大。一般称F4以上的光圈为大光圈，F8以下的光圈为小光圈。&lt;/p&gt;
&lt;p&gt;光圈可以影响景深大小。比如要拍人像，我们可以设置大焦距和小光圈，这样人像的背景就会更”虚“。如果拍风景，在明亮环境下可以设置小光圈，达到全局清晰的效果。&lt;/p&gt;
&lt;p&gt;当然对于夜晚，大光圈是有必要的。通过增加进入镜头的光线，从而减小相应的ISO，提高画质。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/574e9258d109b3de1bd3e2c45cd13084810a4ca5.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;光圈&lt;/p&gt;
&lt;h2&gt;快门&lt;/h2&gt;
&lt;p&gt;快门，指的是曝光时间，用秒表示。快门速度越快，曝光时间越短；快门速度越慢，曝光时间越长。不同的快门速度可以控制在相等光圈下，进入镜头光线的多少。&lt;/p&gt;
&lt;p&gt;一般而言，对于拍摄快速物体，我们往往使用更快的快门速度；相应地，对于有意识拍摄慢速效果，比如星轨、车流轨迹、水面拉丝效果，一般使用更慢的“慢门”。&lt;/p&gt;
&lt;h2&gt;ISO&lt;/h2&gt;
&lt;p&gt;这里的ISO可不是国标。ISO又被称为感光度，可以控制底片对光线的感应程度。一般而言，ISO越大，感应程度越强，画面越亮；ISO越弱，感应程度越弱，画面越暗。&lt;/p&gt;
&lt;p&gt;但在高ISO的情况下，画面往往会出现较为多的、难以消去的噪点，越高的ISO往往意味着画质越差。一般而言，在光线较好的情况下，会使用低ISO进行拍摄（ISO&amp;lt;=200)；对于夜晚场景，如果要拍摄静态的物体，可以通过提高快门时间、光圈大小来降低ISO，以获得更好的拍摄画质。&lt;/p&gt;
&lt;p&gt;当然对于索尼来说，噪点不算什么。&lt;/p&gt;
&lt;h2&gt;曝光互易律&lt;/h2&gt;
&lt;p&gt;对于M模式来说，摄影师需要在拍摄前控制三者。一般而言，控制的方式遵循曝光互易律，即：&lt;strong&gt;曝光三要素光圈，快门，感光度，可以按正比互易而曝光量不变。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通俗地说，就是指在固定某一参数下，一个参数增加了多少挡，另一个参数就要减少多少档。&lt;/p&gt;
&lt;p&gt;例如 F2.8 1/1000 ISO100 = F2 1/2000 ISO100&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/v2-ac15689417e0f9e003fa6bafb68608a8_720w.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;曝光互易率&lt;/p&gt;
&lt;h2&gt;曝光补偿&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;曝光补偿&lt;/strong&gt;(EV)是一种曝光控制方式，曝光补偿就是有意识地变更相机自动演算出的“合适”曝光参数，让照片更明亮或者更昏暗的拍摄手法。&lt;/p&gt;
&lt;p&gt;一般来说，曝光补偿可以在确定曝光三要素的情况下，让画面人为地更“亮”、更“暗”，使其符合人的审美需求。&lt;/p&gt;
&lt;p&gt;例如需要拍摄背光场景，如果我们想凸显楼背后的火烧云，我们可以通过降低EV，让楼变成“剪影”；但反过来，如果我们在背光环境拍摄人像，如果不调整EV值，人脸会显黑（如果拍女生会被打死23333）。这个时候可以增加EV，让人脸的曝光处于正常的位置。&lt;/p&gt;
&lt;p&gt;曝光补偿口诀：白加黑减。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;当然如果你拥有一款宽容度爆棚的相机，那么这些来说对你没那么重要。毕竟“三分拍，七分修”，如果曝光不好，把RAW里的曝光拉一下不就行了嘛。但作为摄影，我们应该尽力保证前期照片的完美，减少后期修图不必要的操作。&lt;/p&gt;
</content:encoded></item><item><title>从零开始入门的摄影小白</title><link>https://blog.nowcent.cn/posts/photography-beginners-guide/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/photography-beginners-guide/</guid><pubDate>Wed, 24 Jun 2020 02:24:21 GMT</pubDate><content:encoded>&lt;p&gt;终于有足够的时间研究摄影了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;摄影、拍摄毕竟是艺术，而不是一蹴而就的。毕竟有可能研究个好几年，终究审美水平和辍学的同学一样菜。&lt;/p&gt;
&lt;p&gt;从今以后这里也会讨论小白学到的摄影相关技巧以及鉴赏。虽然我知道我的审美水平还是非常的菜，不过没关系，慢慢学习嘛，谁不是这么出来的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;img/2-13.jpg&quot; alt=&quot;陈鹏鹏卤肉饭&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Hello blog.</title><link>https://blog.nowcent.cn/posts/hello-blog/</link><guid isPermaLink="true">https://blog.nowcent.cn/posts/hello-blog/</guid><pubDate>Wed, 24 Jun 2020 02:20:56 GMT</pubDate><content:encoded>&lt;p&gt;从去年就开始有写博客的想法了，但因为时间原因一拖再拖到现在才实现。&lt;/p&gt;
&lt;p&gt;曾经考虑过CSDN和博客园，但想了想还是用自己的服务器香。虽然可能会有不可控的因素导致数据丢失，但反正可以备份就是了。&lt;/p&gt;
</content:encoded></item></channel></rss>