主頁 > 移動端開發 > 原始碼詳解Android 9.0 系統啟動流程之init行程(第一階段)

原始碼詳解Android 9.0 系統啟動流程之init行程(第一階段)

2021-11-06 08:56:37 移動端開發

Android系統啟動流程 init行程

  • 1. 背景
  • 2. 介紹
    • 2.1 概述
    • 2.2 init行程入口
  • 3. ueventd/watchdogd跳轉
    • 3.1 ueventd_main
    • 3.2 watchdogd_main
    • 3.3 InstallRebootSignalHandlers
  • 4. 環境變數設定及創建檔案系統目錄并掛載相關的檔案系統
    • 4.1 clearenv
    • 4.2 setenv
    • 4.3 mount
    • 4.4 mknod
  • 5. 初始化日志輸出、掛載磁區設備
    • 5.1 InitKernelLogging
      • 5.1.1 InitLogging
      • 5.1.2 KernelLogger
  • 5.2 DoFirstStageMount
      • 5.2.1 FirstStageMount::Create()
      • 5.2.2 handle->DoFirstStageMount
  • 6. 啟用SELinux安全策略
    • 6.1 SelinuxSetupKernelLogging
    • 6.2 SelinuxInitialize
      • 6.2.1 LoadPolicy
        • IsSplitPolicyDevice
        • LoadSplitPolicy
        • LoadMonolithicPolicy
      • 6.2.2 checkreqprot
  • 7. 開始第二階段前的準備
  • 8. 小結

1. 背景

最近因作業需要,正在學習Android系統的啟動流程,我們目前維護的代碼主要是Android 9.0系統,因此結合代碼,對整個流程進行學習熟悉,

2. 介紹

2.1 概述

Linux系統執行完初始化操作最后會執行根目錄下的init檔案,init是一個可執行程式,它的原始碼在platform/system/core/init/init.cpp,init行程是用戶空間的第一個行程,我們熟悉的app應用程式都是以它為父行程的,init行程入口函式是main函式,這個函式做的事情還是比較多的,主要分為三個部分

  1. init行程第一階段
  2. init行程第二階段
  3. init.rc檔案決議

依據這三個部分將init行程分三篇文章描述,本文主要講解第一階段,第一階段主要有以下內容

  • ueventd/watchdogd跳轉
  • 環境變數設定及創建檔案系統目錄并掛載相關的檔案系統
  • 初始化日志輸出、掛載磁區設備
  • 啟用SELinux安全策略
  • 開始第二階段前的準備

2.2 init行程入口

Android init行程的入口檔案在system/core/init/init.cpp中,
其中的main函式為主要介紹部分,

int main(int argc, char** argv) {
	 /*
     * 1.strcmp是String的一個函式,比較字串,相等回傳0
     * 2.C++中0也可以表示false
     * 3.basename是C庫中的一個函式,得到特定的路徑中的最后一個'/'后面的內容,
     * 比如/sdcard/miui_recovery/backup,得到的結果是backup
     * 4.InitKernelLogging做了一些重定向來記錄一些系統log主要就是我們常用的dev/kmsg的輸出
     */
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return watchdogd_main(argc, argv);
    }

    if (argc > 1 && !strcmp(argv[1], "subcontext")) {
        InitKernelLogging(argv);
        const BuiltinFunctionMap function_map;
        return SubcontextMain(argc, argv, &function_map);
    }

    if (REBOOT_BOOTLOADER_ON_PANIC) {
     	//初始化重啟系統的處理信號,內部通過sigaction 注冊信號,當監聽到該信號時重啟系統
        InstallRebootSignalHandlers();
    }

    bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);

    if (is_first_stage) {
    	//用于記錄啟動時間
        boot_clock::time_point start_time = boot_clock::now();

        // Clear the umask.
        umask(0);

    	//注冊環境變數PATH
		// _PATH_DEFPATH 是定義在bionic/libc/include/paths.h中
        clearenv();
        setenv("PATH", _PATH_DEFPATH, 1);
        // Get the basic filesystem setup we need put together in the initramdisk
        // on / and then we'll let the rc file figure out the rest.
        mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
        mkdir("/dev/pts", 0755);
        mkdir("/dev/socket", 0755);
        mount("devpts", "/dev/pts", "devpts", 0, NULL);
        #define MAKE_STR(x) __STRING(x)
        mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
        // Don't expose the raw commandline to unprivileged processes.
        chmod("/proc/cmdline", 0440);
        gid_t groups[] = { AID_READPROC };
        setgroups(arraysize(groups), groups);
        mount("sysfs", "/sys", "sysfs", 0, NULL);
        mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);

        mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));

        if constexpr (WORLD_WRITABLE_KMSG) {
            mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11));
        }

        mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
        mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));

        // Mount staging areas for devices managed by vold
        // See storage config details at http://source.android.com/devices/storage/
        mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
              "mode=0755,uid=0,gid=1000");
        // /mnt/vendor is used to mount vendor-specific partitions that can not be
        // part of the vendor partition, e.g. because they are mounted read-write.
        mkdir("/mnt/vendor", 0755);

        // Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
        // talk to the outside world...
        InitKernelLogging(argv);

        LOG(INFO) << "init first stage started!";

        if (!DoFirstStageMount()) {
            LOG(FATAL) << "Failed to mount required partitions early ...";
        }

        SetInitAvbVersionInRecovery();

        // Enable seccomp if global boot option was passed (otherwise it is enabled in zygote).
        global_seccomp();

        // Set up SELinux, loading the SELinux policy.
        SelinuxSetupKernelLogging();
        SelinuxInitialize();

        // We're in the kernel domain, so re-exec init to transition to the init domain now
        // that the SELinux policy has been loaded.
        if (selinux_android_restorecon("/init", 0) == -1) {
            PLOG(FATAL) << "restorecon failed of /init failed";
        }

        setenv("INIT_SECOND_STAGE", "true", 1);

        static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
        uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
        setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1);

        char* path = argv[0];
        char* args[] = { path, nullptr };
        execv(path, args);

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

3. ueventd/watchdogd跳轉

在main函式剛開始,會根據引數argv去判斷是否需要啟動ueventd和
watchdogd,其中ueventd是為了維護/dev/下的一些設備節點,比如洗掉或者增加,watchdogd就是常說的看門狗,是為了監測系統,保證系統的穩定性,比如出現鎖死,卡死之類的情況能及時處理,關于watchdogd可自行百度了解,InitKernelLogging做了一些重定向來記錄一些系統log主要就是我們常用的dev/kmsg的輸出,關于REBOOT_BOOTLOADER_ON_PANIC這個是否定義是在init的mk檔案中定義的,一般只有調式版本才會用到這個,定義了一些信號,當init行程崩潰時去重啟bootloader,我們也更容易發現一些問題

3.1 ueventd_main

定義在system/core/init/ueventd.cpp

Android根檔案系統的映像中不存在“/dev”目錄,該目錄是init行程啟動后動態創建的,
因此,建立Android中設備節點檔案的重任,也落在了init行程身上,為此,init行程創建子行程ueventd,并將創建設備節點檔案的作業托付給ueventd,ueventd通過兩種方式創建設備節點檔案,

第一種方式對應“冷插拔”(Cold Plug),即以預先定義的設備資訊為基礎,當ueventd啟動后,統一創建設備節點檔案,這一類設備節點檔案也被稱為靜態節點檔案,

第二種方式對應“熱插拔”(Hot Plug),即在系統運行中,當有設備插入USB埠時,ueventd就會接收到這一事件,為插入的設備動態創建設備節點檔案,這一類設備節點檔案也被稱為動態節點檔案,

DeviceHandler CreateDeviceHandler() {
    Parser parser;

    std::vector<Subsystem> subsystems;
    parser.AddSectionParser("subsystem", std::make_unique<SubsystemParser>(&subsystems));

    using namespace std::placeholders;
    std::vector<SysfsPermissions> sysfs_permissions;
    std::vector<Permissions> dev_permissions;
    parser.AddSingleLineParser("/sys/",
                               std::bind(ParsePermissionsLine, _1, &sysfs_permissions, nullptr));
    parser.AddSingleLineParser("/dev/",
                               std::bind(ParsePermissionsLine, _1, nullptr, &dev_permissions));

    parser.ParseConfig("/ueventd.rc");//決議.rc檔案,這個后續再講
    parser.ParseConfig("/vendor/ueventd.rc");
    parser.ParseConfig("/odm/ueventd.rc");

    /*
     * keep the current product name base configuration so
     * we remain backwards compatible and allow it to override
     * everything
     * TODO: cleanup platform ueventd.rc to remove vendor specific
     * device node entries (b/34968103)
     */
    std::string hardware = android::base::GetProperty("ro.hardware", "");
    parser.ParseConfig("/ueventd." + hardware + ".rc");

    auto boot_devices = fs_mgr_get_boot_devices();
    return DeviceHandler(std::move(dev_permissions), std::move(sysfs_permissions),
                         std::move(subsystems), std::move(boot_devices), true);
}

int ueventd_main(int argc, char** argv) {
    /*
     * init sets the umask to 077 for forked processes. We need to
     * create files with exact permissions, without modification by
     * the umask.
     */
    umask(000);//設定新建檔案的默認值,這個與chmod相反,這里相當于新建檔案后的權限為666

    InitKernelLogging(argv);//初始化日志輸出

    LOG(INFO) << "ueventd started!";

    SelinuxSetupKernelLogging();//注冊selinux相關的用于列印log
    SelabelInitialize();

    DeviceHandler device_handler = CreateDeviceHandler();
    UeventListener uevent_listener;

    if (access(COLDBOOT_DONE, F_OK) != 0) {
        ColdBoot cold_boot(uevent_listener, device_handler);
        cold_boot.Run();//冷啟動
    }

    // We use waitpid() in ColdBoot, so we can't ignore SIGCHLD until now.
    signal(SIGCHLD, SIG_IGN);//忽略子行程終止信號
    // Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN
    // for SIGCHLD above.
    while (waitpid(-1, nullptr, WNOHANG) > 0) {
    }
	//監聽來自驅動的uevent,進行“熱插拔”處理
    uevent_listener.Poll([&device_handler](const Uevent& uevent) {
        HandleFirmwareEvent(uevent);
        device_handler.HandleDeviceEvent(uevent);
        return ListenerAction::kContinue;
    });

    return 0;
}

3.2 watchdogd_main

定義在system/core/init/watchdogd.cpp

“看門狗”本身是一個定時器電路,內部會不斷的進行計時(或計數)操作,計算機系統和”看門狗”有兩個引腳相連接,正常運行時每隔一段時間就會通過其中一個引腳向”看門狗”發送信號,”看門狗”接收到信號后會將計時器清零并重新開始計時,而一旦系統出現問題,進入死回圈或任何阻塞狀態,不能及時發送信號讓”看門狗”的計時器清零,當計時結束時,”看門狗”就會通過另一個引腳向系統發送“復位信號”,讓系統重啟,
watchdogd_main主要是定時器作用,而DEV_NAME就是那個引腳

int watchdogd_main(int argc, char **argv) {
    InitKernelLogging(argv);

    int interval = 10;
    if (argc >= 2) interval = atoi(argv[1]);//atoi作用是將字串轉變為數值

    int margin = 10;
    if (argc >= 3) margin = atoi(argv[2]);

    LOG(INFO) << "watchdogd started (interval " << interval << ", margin " << margin << ")!";

    int fd = open(DEV_NAME, O_RDWR|O_CLOEXEC);//打開檔案 /dev/watchdog
    if (fd == -1) {
        PLOG(ERROR) << "Failed to open " << DEV_NAME;
        return 1;
    }

    int timeout = interval + margin;
    //ioctl是設備驅動程式中對設備的I/O通道進行管理的函式,WDIOC_SETTIMEOUT是設定超時時間
    int ret = ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
    if (ret) {
        PLOG(ERROR) << "Failed to set timeout to " << timeout;
        ret = ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
        if (ret) {
            PLOG(ERROR) << "Failed to get timeout";
        } else {
            if (timeout > margin) {
                interval = timeout - margin;
            } else {
                interval = 1;
            }
            LOG(WARNING) << "Adjusted interval to timeout returned by driver: "
                         << "timeout " << timeout
                         << ", interval " << interval
                         << ", margin " << margin;
        }
    }

    while (true) {//每間隔一定時間往檔案中寫入一個空字符,這就是看門狗的關鍵了
        write(fd, "", 1);
        sleep(interval);
    }
}

3.3 InstallRebootSignalHandlers

定義在system/core/init/init.cpp
這個函式主要作用將各種信號量,如SIGABRT,SIGBUS等的行為設定為SA_RESTART,一旦監聽到這些信號即執行重啟系統

static void InstallRebootSignalHandlers() {
    // Instead of panic'ing the kernel as is the default behavior when init crashes,
    // we prefer to reboot to bootloader on development builds, as this will prevent
    // boot looping bad configurations and allow both developers and test farms to easily
    // recover.
    struct sigaction action;
    memset(&action, 0, sizeof(action));
    sigfillset(&action.sa_mask);//將所有信號加入至信號集
    action.sa_handler = [](int signal) {
        // These signal handlers are also caught for processes forked from init, however we do not
        // want them to trigger reboot, so we directly call _exit() for children processes here.
        if (getpid() != 1) {
            _exit(signal);
        }

        // Calling DoReboot() or LOG(FATAL) is not a good option as this is a signal handler.
        // RebootSystem uses syscall() which isn't actually async-signal-safe, but our only option
        // and probably good enough given this is already an error case and only enabled for
        // development builds.
        RebootSystem(ANDROID_RB_RESTART2, "bootloader");//重啟系統
    };
    action.sa_flags = SA_RESTART;
    sigaction(SIGABRT, &action, nullptr);
    sigaction(SIGBUS, &action, nullptr);
    sigaction(SIGFPE, &action, nullptr);
    sigaction(SIGILL, &action, nullptr);
    sigaction(SIGSEGV, &action, nullptr);
#if defined(SIGSTKFLT)
    sigaction(SIGSTKFLT, &action, nullptr);
#endif
    sigaction(SIGSYS, &action, nullptr);
    sigaction(SIGTRAP, &action, nullptr);
}

4. 環境變數設定及創建檔案系統目錄并掛載相關的檔案系統

is_first_stage 值是通過獲取INIT_SECOND_STAGE是否指向空來判斷的,此時系統剛啟動,環境變數等尚未初始化,所以此時is_first_stage 的值是ture,會進入if陳述句,剛開始會通過umask來清空和初始化創建檔案的權限掩碼,通過clearenv來清慷訓境變數,之后就會去創建一些開機用到的系統檔案并將其掛載,

定義在system/core/init/init.cpp

int main(int argc, char** argv) {
	.....
	//查看是否有環境變數INIT_SECOND_STAGE
    bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);

    if (is_first_stage) {
        boot_clock::time_point start_time = boot_clock::now();

        // Clear the umask.
        umask(0);//清空檔案權限

    	//注冊環境變數PATH
		// _PATH_DEFPATH 是定義在bionic/libc/include/paths.h中
        clearenv();
        setenv("PATH", _PATH_DEFPATH, 1);
        // Get the basic filesystem setup we need put together in the initramdisk
        // on / and then we'll let the rc file figure out the rest.
        mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
        mkdir("/dev/pts", 0755);
        mkdir("/dev/socket", 0755);
        mount("devpts", "/dev/pts", "devpts", 0, NULL);
        #define MAKE_STR(x) __STRING(x)
        mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
        // Don't expose the raw commandline to unprivileged processes.
        chmod("/proc/cmdline", 0440);
        gid_t groups[] = { AID_READPROC };
        setgroups(arraysize(groups), groups);
        mount("sysfs", "/sys", "sysfs", 0, NULL);
        mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);

        mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));

        if constexpr (WORLD_WRITABLE_KMSG) {
            mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11));
        }

        mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
        mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));

        // Mount staging areas for devices managed by vold
        // See storage config details at http://source.android.com/devices/storage/
        mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
              "mode=0755,uid=0,gid=1000");
        // /mnt/vendor is used to mount vendor-specific partitions that can not be
        // part of the vendor partition, e.g. because they are mounted read-write.
        mkdir("/mnt/vendor", 0755);
        ......
    }
    ......
}

4.1 clearenv

主要負責清慷訓境變數,

int clearenv() {
  char** e = environ;
  if (e != NULL) {
    for (; *e; ++e) {
      *e = NULL;
    }
  }
  return 0;
}

4.2 setenv

定義在uboot\common\cmd_nvedit.c

這個函式主要作用是將一個鍵值對放到一個Char陣列中,如果陣列中有key就替換,沒有就插入


int setenv(const char *varname, const char *varvalue)
{
	const char * const argv[4] = { "setenv", varname, varvalue, NULL };

	/* before import into hashtable */
	if (!(gd->flags & GD_FLG_ENV_READY))
		return 1;

	if (varvalue == NULL || varvalue[0] == '\0')
		return _do_env_set(0, 2, (char * const *)argv);
	else
		return _do_env_set(0, 3, (char * const *)argv);
}

4.3 mount

mount是用來掛載檔案系統的,mount屬于Linux系統呼叫,函式原型如下:

int mount(const char *source, const char *target, const char *filesystemtype,
unsigned long mountflags, const void *data);
引數:
source:將要掛上的檔案系統,通常是一個設備名,

target:檔案系統所要掛載的目標目錄,

filesystemtype:檔案系統的型別,可以是"ext2""msdos""proc""ntfs""iso9660",,,

mountflags:指定檔案系統的讀寫訪問標志,可能值有以下

            引數  含義
            MS_BIND 執行bind掛載,使檔案或者子目錄樹在檔案系統內的另一個點上可視,
            MS_DIRSYNC  同步目錄的更新,
            MS_MANDLOCK 允許在檔案上執行強制鎖,
            MS_MOVE 移動子目錄樹,
            MS_NOATIME  不要更新檔案上的訪問時間,
            MS_NODEV    不允許訪問設備檔案,
            MS_NODIRATIME   不允許更新目錄上的訪問時間,
            MS_NOEXEC   不允許在掛上的檔案系統上執行程式,
            MS_NOSUID   執行程式時,不遵照set-user-ID和set-group-ID位,
            MS_RDONLY   指定檔案系統為只讀,
            MS_REMOUNT  重新加載檔案系統,這允許你改變現存檔案系統的mountflag和資料,而無需使用先卸載,再掛上檔案系統的方式,
            MS_SYNCHRONOUS  同步檔案的更新,
            MNT_FORCE   強制卸載,即使檔案系統處于忙狀態,
            MNT_EXPIRE  將掛載點標記為過時,
data:檔案系統特有的引數

在init初始化程序中,Android分別掛載了tmpfs,devpts,proc,sysfs,selinuxfs這5類檔案系統,

tmpfs是一種虛擬記憶體檔案系統,它會將所有的檔案存盤在虛擬記憶體中,如果你將tmpfs檔案系統卸載后,那么其下的所有的內容將不復存在,tmpfs既可以使用RAM,也可以使用交換磁區,會根據你的實際需要而改變大小,tmpfs的速度非常驚人,畢竟它是駐留在RAM中的,即使用了交換磁區,性能仍然非常卓越,
由于tmpfs是駐留在RAM的,因此它的內容是不持久的,斷電后,tmpfs的內容就消失了,這也是被稱作tmpfs的根本原因,

devpts檔案系統為偽終端提供了一個標準介面,它的標準掛接點是/dev/ pts,只要pty的主復合設備/dev/ptmx被打開,就會在/dev/pts下動態的創建一個新的pty設備檔案,

proc檔案系統是一個非常重要的虛擬檔案系統,它可以看作是內核內部資料結構的介面,通過它我們可以獲得系統的資訊,同時也能夠在運行時修改特定的內核引數,

與proc檔案系統類似,sysfs檔案系統也是一個不占有任何磁盤空間的虛擬檔案系統,它通常被掛接在/sys目錄下,sysfs檔案系統是Linux2.6內核引入的,它把連接在系統上的設備和總線組織成為一個分級的檔案,使得它們可以在用戶空間存取

selinuxfs也是虛擬檔案系統,通常掛載在/sys/fs/selinux目錄下,用來存放SELinux安全策略檔案

4.4 mknod

mknod用于創建Linux中的設備檔案,函式原型如下:

int mknod(const char* path, mode_t mode, dev_t dev) {

}
引數:
path:設備所在目錄
mode:指定設備的型別和讀寫訪問標志,可能的型別
    引數  含義
    S_IFMT  type of file ,檔案型別掩碼
    S_IFREG regular 普通檔案
    S_IFBLK block special 塊設備檔案
    S_IFDIR directory 目錄檔案
    S_IFCHR character special 字符設備檔案
    S_IFIFO fifo 管道檔案
    S_IFNAM special named file 特殊檔案
    S_IFLNK symbolic link 鏈接檔案

dev: 表示設備,由makedev(1, 9) 函式創建,9為主設備號、1為次設備號

5. 初始化日志輸出、掛載磁區設備

定義在system/core/init/init.cpp

int main(int argc, char** argv) {
    ......
        if (is_first_stage) {
            ......
        	InitKernelLogging(argv);

        	LOG(INFO) << "init first stage started!";

        	if (!DoFirstStageMount()) {
            	LOG(FATAL) << "Failed to mount required partitions early ...";
        }
        ......
        }
    ......
 }

5.1 InitKernelLogging

定義在system\core\init\log.cpp

void InitKernelLogging(char* argv[]) {
    // Make stdin/stdout/stderr all point to /dev/null.
    int fd = open("/sys/fs/selinux/null", O_RDWR);//打開檔案
    //若開啟失敗,則記錄log
    if (fd == -1) {
        int saved_errno = errno;
        android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
        errno = saved_errno;
        PLOG(FATAL) << "Couldn't open /sys/fs/selinux/null";
    }
     /*
     * dup2(int old_fd, int new_fd) 的作用是復制檔案描述符,將old復制到new,下文中將
     *  0、1、2系結到null設備上,通過標準的輸入輸出無法輸出資訊
     */
    dup2(fd, 0);//重定向標準輸入stdin
    dup2(fd, 1);//重定向標準輸出stdout
    dup2(fd, 2);//重定向標準錯誤stderr
    if (fd > 2) close(fd);

    android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);//初始化log
}

這里需要說明的是,dup2函式的作用是用來復制一個檔案的描述符,
通常用來重定向行程的stdin、stdout和stderr,
它的函式原形是:

int dup2(int oldfd, int targetfd)

該函式執行后,targetfd將變成oldfd的復制品,

因此上述程序其實就是:創建出__null__設備后,將0、1、2系結到__null__設備上,
因此init行程呼叫InitKernelLogging函式后,通過標準的輸入輸出無法輸出資訊,

5.1.1 InitLogging

定義在system\core\base\logging.cpp

InitLogging主要作業是設定logger和aborter的處理函式,然后設定日志系統輸出等級

//此處設定的是KernelLogger
void InitLogging(char* argv[], LogFunction&& logger, AbortFunction&& aborter) {
 /*
 * C++中foo(std::forward<T>(arg))表示將arg按原本的左值或右值,傳遞給foo方法,
 * LogFunction& 這種表示是左值,LogFunction&&這種表示是右值
 */
  SetLogger(std::forward<LogFunction>(logger));//設定logger處理函式
  SetAborter(std::forward<AbortFunction>(aborter));//設定aborter處理函式

  if (gInitialized) {
    return;
  }

  gInitialized = true;

  // Stash the command line for later use. We can use /proc/self/cmdline on
  // Linux to recover this, but we don't have that luxury on the Mac/Windows,
  // and there are a couple of argv[0] variants that are commonly used.
  if (argv != nullptr) {
    SetDefaultTag(basename(argv[0]));
  }

  const char* tags = getenv("ANDROID_LOG_TAGS");//獲取系統當前日志輸出等級
  if (tags == nullptr) {
    return;
  }

  //根據TAG決定最小記錄等級
  std::vector<std::string> specs = Split(tags, " ");//將tags以空格拆分成陣列
  for (size_t i = 0; i < specs.size(); ++i) {
    // "tag-pattern:[vdiwefs]"
    std::string spec(specs[i]);
    if (spec.size() == 3 && StartsWith(spec, "*:")) {//如果字符數為3且以*:開頭
      //那么根據第三個字符來設定日志輸出等級(比如*:d,就是DEBUG級別)
      switch (spec[2]) {
        case 'v':
          gMinimumLogSeverity = VERBOSE;
          continue;
        case 'd':
          gMinimumLogSeverity = DEBUG;
          continue;
        case 'i':
          gMinimumLogSeverity = INFO;
          continue;
        case 'w':
          gMinimumLogSeverity = WARNING;
          continue;
        case 'e':
          gMinimumLogSeverity = ERROR;
          continue;
        case 'f':
          gMinimumLogSeverity = FATAL_WITHOUT_ABORT;
          continue;
        // liblog will even suppress FATAL if you say 's' for silent, but that's
        // crazy!
        case 's':
          gMinimumLogSeverity = FATAL_WITHOUT_ABORT;
          continue;
      }
    }
    LOG(FATAL) << "unsupported '" << spec << "' in ANDROID_LOG_TAGS (" << tags
               << ")";
  }
}

5.1.2 KernelLogger

定義在system\core\base\logging.cpp

在InitKernelLogging方法中有句呼叫android::base::InitLogging(argv, &android::base::KernelLogger);這句的作用就是將KernelLogger函式作為log日志的處理函式,KernelLogger主要作用就是將要輸出的日志格式化之后寫入到 /dev/kmsg 設備中

void KernelLogger(android::base::LogId, android::base::LogSeverity severity,
                  const char* tag, const char*, unsigned int, const char* msg) {
  // clang-format off
  static constexpr int kLogSeverityToKernelLogLevel[] = {
      [android::base::VERBOSE] = 7,              // KERN_DEBUG (there is no verbose kernel log
                                                 //             level)
      [android::base::DEBUG] = 7,                // KERN_DEBUG
      [android::base::INFO] = 6,                 // KERN_INFO
      [android::base::WARNING] = 4,              // KERN_WARNING
      [android::base::ERROR] = 3,                // KERN_ERROR
      [android::base::FATAL_WITHOUT_ABORT] = 2,  // KERN_CRIT
      [android::base::FATAL] = 2,                // KERN_CRIT
  };
  // clang-format on
  //static_assert是編譯斷言,如果第一個引數為true,那么編譯就不通過,這里是判斷kLogSeverityToKernelLogLevel陣列個數不能大于7
  static_assert(arraysize(kLogSeverityToKernelLogLevel) == android::base::FATAL + 1,
                "Mismatch in size of kLogSeverityToKernelLogLevel and values in LogSeverity");
  //打開 /dev/kmsg 檔案
  static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));
  if (klog_fd == -1) return;

  //根據傳入的日志等級得到Linux的日志等級,也就是kLogSeverityToKernelLogLevel對應下標的映射
  int level = kLogSeverityToKernelLogLevel[severity];

  // The kernel's printk buffer is only 1024 bytes.
  // TODO: should we automatically break up long lines into multiple lines?
  // Or we could log but with something like "..." at the end?
  char buf[1024];
  size_t size = snprintf(buf, sizeof(buf), "<%d>%s: %s\n", level, tag, msg);//格式化日志輸出
  if (size > sizeof(buf)) {
    size = snprintf(buf, sizeof(buf), "<%d>%s: %zu-byte message too long for printk\n",
                    level, tag, size);
  }

  iovec iov[1];
  iov[0].iov_base = buf;
  iov[0].iov_len = size;
  //通過iovec將log發送到dev/kmsg
  TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));//將日志寫入到 /dev/kmsg 中
}

5.2 DoFirstStageMount

定義在platform/system/core/init/init_first_stage.cpp

主要作用是初始化特定設備并掛載

bool DoFirstStageMount() {
    // Skips first stage mount if we're in recovery mode.
    if (IsRecoveryMode()) {//如果是刷機模式,直接跳過掛載
        LOG(INFO) << "First stage mount skipped (recovery mode)";
        return true;
    }

    // Firstly checks if device tree fstab entries are compatible.
    //如果fstab/compatible的值不是android,fstab,直接跳過掛載
    if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {
        LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
        return true;
    }
	
	//滿足上述條件時,就會呼叫FirstStageMount的DoFirstStageMount函式
    std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();
    if (!handle) {
        LOG(ERROR) << "Failed to create FirstStageMount";
        return false;
    }
    return handle->DoFirstStageMount();//主要是初始化特定設備并掛載
}

5.2.1 FirstStageMount::Create()

定義在system/core/init/init_first_stage.cpp

FirstStageMount::FirstStageMount()
    : need_dm_verity_(false), device_tree_fstab_(fs_mgr_read_fstab_dt(), fs_mgr_free_fstab) {
    if (!device_tree_fstab_) {
        LOG(INFO) << "Failed to read fstab from device tree";
        return;
    }
    // Stores device_tree_fstab_->recs[] into mount_fstab_recs_ (vector<fstab_rec*>)
    // for easier manipulation later, e.g., range-base for loop.
    for (int i = 0; i < device_tree_fstab_->num_entries; i++) {
        mount_fstab_recs_.push_back(&device_tree_fstab_->recs[i]);//將掛載資訊放入陣列中存起來
    }

    auto boot_devices = fs_mgr_get_boot_devices();
    device_handler_ =
        std::make_unique<DeviceHandler>(std::vector<Permissions>{}, std::vector<SysfsPermissions>{},
                                        std::vector<Subsystem>{}, std::move(boot_devices), false);
}

std::unique_ptr<FirstStageMount> FirstStageMount::Create() {
    if (IsDtVbmetaCompatible()) {
        return std::make_unique<FirstStageMountVBootV2>();
    } else {
        return std::make_unique<FirstStageMountVBootV1>();
    }
}

5.2.2 handle->DoFirstStageMount

定義在system/core/init/init_first_stage.cpp

bool FirstStageMount::DoFirstStageMount() {
    // Nothing to mount.
    if (mount_fstab_recs_.empty()) return true;

    if (!InitDevices()) return false;

    if (!MountPartitions()) return false;

    return true;
}

6. 啟用SELinux安全策略

SELinux是 Linux的一個擴張強制訪問控制安全模塊,在這種訪問控制體系的限制下,行程只能訪問那些在他的任務中所需要檔案

int main(int argc, char** argv) {
    ......
        if (is_first_stage) {
            ......
            //此處應該是初始化安全框架:Android Verified Boot
    		//AVB主要用于防止系統檔案本身被篡改,還包含了防止系統回滾的功能,
    		//以免有人試圖回滾系統并利用以前的漏洞
        	SetInitAvbVersionInRecovery();//在刷機模式下初始化avb的版本,不是刷機模式直接跳過

        	// Enable seccomp if global boot option was passed (otherwise it is enabled in zygote).
        	// 此處決定是否使能全域seccomp; 如果沒有使能, zygote也會使能
			// 讀取/proc/cmdline內容, 如果包含androidboot.seccomp=global則呼叫set_global_seccomp_filter
			// set_global_seccomp_filter位于bionic/libc/seccomp/seccomp_policy.cpp
        	global_seccomp();

        	// Set up SELinux, loading the SELinux policy.
        	SelinuxSetupKernelLogging();// 將selinux日志重定向到內核日志輸出, 即/dev/kmsg
        	SelinuxInitialize();//加載SELinux policy,也就是安全策略,

        	// We're in the kernel domain, so re-exec init to transition to the init domain now
        	// that the SELinux policy has been loaded.
        	//按selinux policy要求,重新設定init檔案屬性
        	if (selinux_android_restorecon("/init", 0) == -1) {//restorecon命令用來恢復SELinux檔案屬性即恢復檔案的安全背景關系
            	PLOG(FATAL) << "restorecon failed of /init failed";
        	}
        	......
    	}
    	......
 }

6.1 SelinuxSetupKernelLogging

設定log callback,可以呼叫selinux_log把log寫入到kmsg里面

定位在system\core\init\selinux.cpp

void SelinuxSetupKernelLogging() {
    selinux_callback cb;
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);
}

6.2 SelinuxInitialize

AVB相關的資訊目前還不太了解,不做深入分析,
此處,我們看看selinux_initialize相關的作業:
定位在system\core\init\selinux.cpp

void SelinuxInitialize() {
    Timer t;

    LOG(INFO) << "Loading SELinux policy";
    if (!LoadPolicy()) {// 加載SELinux策略
        LOG(FATAL) << "Unable to load SELinux policy";
    }

    bool kernel_enforcing = (security_getenforce() == 1);// 從kernel獲取SELinux的狀態,和getenforce的實作一樣
    bool is_enforcing = IsEnforcing();// 從bootargs里面獲取
    if (kernel_enforcing != is_enforcing) {
        if (security_setenforce(is_enforcing)) {// 如果kernel里面的SELinux狀態和bootargs的不一致,要設定成bootargs里面傳過來的值
            PLOG(FATAL) << "security_setenforce(%s) failed" << (is_enforcing ? "true" : "false");
        }
    }

    if (auto result = WriteFile("/sys/fs/selinux/checkreqprot", "0"); !result) {// 由內核強制執行檢查保護
        LOG(FATAL) << "Unable to write to /sys/fs/selinux/checkreqprot: " << result.error();
    }

    // init's first stage can't set properties, so pass the time to the second stage.
    // 獲取SELinux策略加載時間并設定到設定環境變數INIT_SELINUX_TOOK
    setenv("INIT_SELINUX_TOOK", std::to_string(t.duration().count()).c_str(), 1);
}

6.2.1 LoadPolicy

從Android8.0之后,因為Project Treble,system和vendor策略分離,所以Android P上走的是LoadSplitPolicy

bool LoadPolicy() {
	/*
     * 加載SELinux策略
     * 如果IsSplitPolicyDevice回傳true, 執行LoadSplitPolicy, 否則執行LoadMonolithicPolicy
     */
    return IsSplitPolicyDevice() ? LoadSplitPolicy() : LoadMonolithicPolicy();
}

IsSplitPolicyDevice

IsSplitPolicyDevice 檢測/system/etc/selinux/plat_sepolicy.cil檔案是否存在

constexpr const char plat_policy_cil_file[] = "/system/etc/selinux/plat_sepolicy.cil";

bool IsSplitPolicyDevice() {
    return access(plat_policy_cil_file, R_OK) != -1;
}

LoadSplitPolicy

bool LoadSplitPolicy() {
    // IMPLEMENTATION NOTE: Split policy consists of three CIL files:
    // * platform -- policy needed due to logic contained in the system image,
    // * non-platform -- policy needed due to logic contained in the vendor image,
    // * mapping -- mapping policy which helps preserve forward-compatibility of non-platform policy
    //   with newer versions of platform policy.
    //
    // secilc is invoked to compile the above three policy files into a single monolithic policy
    // file. This file is then loaded into the kernel.

    // Load precompiled policy from vendor image, if a matching policy is found there. The policy
    // must match the platform policy on the system image.
    /*
     * 依次查找以下預編譯SELinux檔案
     * "/vendor/etc/selinux/precompiled_sepolicy"
     * "/odm/etc/selinux/precompiled_sepolicy"
     */
    std::string precompiled_sepolicy_file;
    if (FindPrecompiledSplitPolicy(&precompiled_sepolicy_file)) {// 先找odm磁區,再找vendor磁區
        unique_fd fd(open(precompiled_sepolicy_file.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));// open file
        if (fd != -1) {
            if (selinux_android_load_policy_from_fd(fd, precompiled_sepolicy_file.c_str()) < 0) {// 加載到kernel
                LOG(ERROR) << "Failed to load SELinux policy from " << precompiled_sepolicy_file;
                return false;
            }
            return true;
        }
    }
    // No suitable precompiled policy could be loaded

    LOG(INFO) << "Compiling SELinux policy";

    // Determine the highest policy language version supported by the kernel
    set_selinuxmnt("/sys/fs/selinux");
    // 將策略檔案寫入到 /sys/fs/selinux/load
    int max_policy_version = security_policyvers();
    if (max_policy_version == -1) {
        PLOG(ERROR) << "Failed to determine highest policy version supported by kernel";
        return false;
    }

    // We store the output of the compilation on /dev because this is the most convenient tmpfs
    // storage mount available this early in the boot sequence.
    char compiled_sepolicy[] = "/dev/sepolicy.XXXXXX";
    unique_fd compiled_sepolicy_fd(mkostemp(compiled_sepolicy, O_CLOEXEC));
    if (compiled_sepolicy_fd < 0) {
        PLOG(ERROR) << "Failed to create temporary file " << compiled_sepolicy;
        return false;
    }

    // Determine which mapping file to include
    std::string vend_plat_vers;
    if (!GetVendorMappingVersion(&vend_plat_vers)) {
        return false;
    }
    std::string mapping_file("/system/etc/selinux/mapping/" + vend_plat_vers + ".cil");

    // vendor_sepolicy.cil and plat_pub_versioned.cil are the new design to replace
    // nonplat_sepolicy.cil.
    std::string plat_pub_versioned_cil_file("/vendor/etc/selinux/plat_pub_versioned.cil");
    std::string vendor_policy_cil_file("/vendor/etc/selinux/vendor_sepolicy.cil");

    if (access(vendor_policy_cil_file.c_str(), F_OK) == -1) {
        // For backward compatibility.
        // TODO: remove this after no device is using nonplat_sepolicy.cil.
        vendor_policy_cil_file = "/vendor/etc/selinux/nonplat_sepolicy.cil";
        plat_pub_versioned_cil_file.clear();
    } else if (access(plat_pub_versioned_cil_file.c_str(), F_OK) == -1) {
        LOG(ERROR) << "Missing " << plat_pub_versioned_cil_file;
        return false;
    }

    // odm_sepolicy.cil is default but optional.
    std::string odm_policy_cil_file("/odm/etc/selinux/odm_sepolicy.cil");
    if (access(odm_policy_cil_file.c_str(), F_OK) == -1) {
        odm_policy_cil_file.clear();
    }
    const std::string version_as_string = std::to_string(max_policy_version);

    // clang-format off
    std::vector<const char*> compile_args {
        "/system/bin/secilc",
        plat_policy_cil_file,
        "-m", "-M", "true", "-G", "-N",
        // Target the highest policy language version supported by the kernel
        "-c", version_as_string.c_str(),
        mapping_file.c_str(),
        "-o", compiled_sepolicy,
        // We don't care about file_contexts output by the compiler
        "-f", "/sys/fs/selinux/null",  // /dev/null is not yet available
    };
    // clang-format on

    if (!plat_pub_versioned_cil_file.empty()) {
        compile_args.push_back(plat_pub_versioned_cil_file.c_str());
    }
    if (!vendor_policy_cil_file.empty()) {
        compile_args.push_back(vendor_policy_cil_file.c_str());
    }
    if (!odm_policy_cil_file.empty()) {
        compile_args.push_back(odm_policy_cil_file.c_str());
    }
    compile_args.push_back(nullptr);

    if (!ForkExecveAndWaitForCompletion(compile_args[0], (char**)compile_args.data())) {
        unlink(compiled_sepolicy);
        return false;
    }
    unlink(compiled_sepolicy);

    LOG(INFO) << "Loading compiled SELinux policy";
    if (selinux_android_load_policy_from_fd(compiled_sepolicy_fd, compiled_sepolicy) < 0) {
        LOG(ERROR) << "Failed to load SELinux policy from " << compiled_sepolicy;
        return false;
    }

    return true;
}

LoadSplitPolicy 加載selinux 策略時,首先從預先編譯好的二進制檔案precompiled_sepolicy讀取

如果沒有找到的話,再用secilc命令編譯所有的cil,重新得到一個二進制策略檔案

FindPrecompiledSplitPolicy
這個函式會查找兩個地方,/odm/etc/selinux/vendor/etc/selinux

/vendor/etc/selinux/precompiled_sepolicy
/odm/etc/selinux/precompiled_sepolicy
如果有odm磁區,precompiled_sepolicy會放在odm磁區,否則在vendor磁區,因此讀取的時候也是先找odm再找vendor

LoadMonolithicPolicy

從檔案/sepolicy加載策略

bool LoadMonolithicPolicy() {
    LOG(VERBOSE) << "Loading SELinux policy from monolithic file";
    if (selinux_android_load_policy() < 0) {
        PLOG(ERROR) << "Failed to load monolithic SELinux policy";
        return false;
    }
    return true;
}

6.2.2 checkreqprot

設定"checkreqprot"標記的初始值,

"0"表示由內核強制執行檢查保護(包括其中隱含的所有執行保護)
"1"表示由應用程式自己主動請求執行檢查保護
默認值由內核在編譯時確定,也可以在運行時通過/sys/fs/selinux/checkreqprot修改

7. 開始第二階段前的準備

這里主要就是設定一些變數如INIT_SECOND_STAGE,INIT_STARTED_AT,為第二階段做準備,然后再次呼叫init的main函式,啟動用戶態的init行程

int main(int argc, char** argv) {
    ......
        if (is_first_stage) {
            ......
            setenv("INIT_SECOND_STAGE", "true", 1);

        	static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
        	//記錄第二階段開始時間戳
        	uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
        	setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1);

        	char* path = argv[0];
        	char* args[] = { path, nullptr };
        	execv(path, args);//再次呼叫init的main函式,啟動用戶態的init行程

        	// execv() only returns if an error happened, in which case we
        	// panic and never fall through this conditional.
        	PLOG(FATAL) << "execv(\"" << path << "\") failed";
        	// 內核態的行程不應該退出
        	}
    ......
 }

可能大家不明白呼叫execv方法就重新執行main方法,下面給出該方法原型解釋,

函式原型
int execv(const char *progname, char *const argv[]);   //#include <unistd.h>
功能介紹
    execv會停止執行當前的行程,并且以progname應用行程替換被停止執行的行程,行程ID沒有改變,
引數:
    progname: 被執行的應用程式,
    argv: 傳遞給應用程式的引數串列, 注意,這個陣列的第一個引數應該是應用程式名字本身,并且最后一個引數應該為NULL,不參將多個引數合并為一個引數放入陣列,
回傳值:
    如果應用程式正常執行完畢,那么execv是永遠不會回傳的;當execv在呼叫出錯了,此時它的回傳值應該是-1,具體的錯誤代碼可以通過全域變數errno查看,還可以通過stderr得到具體的錯誤描述字符,

第一階段結束時,會復位一些資訊,并設定一些環境變數,
最后啟動用戶態的用戶態的init行程,進入init階段二,

8. 小結

梳理源代碼真是讓人痛苦的一件事,龐大的代碼量很難完全梳理一遍,很多細致的函式了解功能及可,細致分析過于浪費時間,下一篇文章講解init行程第二階段,

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

標籤:其他

上一篇:3年Android開發在上海的面經之旅

下一篇:Android Studio打包APK安裝失敗:應用是非正式版本,當前設備不支持安裝

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

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

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

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

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

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

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

    ......

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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