主頁 > 移動端開發 > Android 世界中,誰喊醒了 Zygote ?

Android 世界中,誰喊醒了 Zygote ?

2020-09-16 08:50:43 移動端開發

本文基于 Android 9.0 , 代碼倉庫地址 : android_9.0.0_r45

文中原始碼鏈接:

SystemServer.java

ActivityManagerService.java

Process.java

ZygoteProcess.java

ZygoteSystemServer 啟動流程還不熟悉的建議閱讀下面兩篇文章:

Java 世界的盤古和女媧 —— Zygote

Zygote 家的大兒子 —— SystemServer

Zygote 作為 Android 世界的受精卵,在成功繁殖出 system_server 行程之后并沒有完全功成身退,仍然承擔著受精卵的責任,Zygote 通過呼叫其持有的 ZygoteServer 物件的 runSelectLoop() 方法開始等待客戶端的呼喚,有求必應,客戶端的請求無非是創建應用行程,以 startActivity() 為例,假如開啟的是一個尚未創建行程的應用,那么就會向 Zygote 請求創建行程,下面將從 客戶端發送請求服務端處理請求 兩方面來進行決議,

客戶端發送請求

startActivity() 的具體流程這里就不分析了,系列后續文章會寫到,我們直接看到創建行程的 startProcess() 方法,該方法在 ActivityManagerService 中,后面簡稱 AMS

Process.startProcess()

> ActivityManagerService.java

private ProcessStartResult startProcess(String hostingType, String entryPoint,
        ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
        String seInfo, String requiredAbi, String instructionSet, String invokeWith,
        long startTime) {
    try {
        checkTime(startTime, "startProcess: asking zygote to start proc");
        final ProcessStartResult startResult;
        if (hostingType.equals("webview_service")) {
            startResult = startWebView(entryPoint,
                    app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                    app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                    app.info.dataDir, null,
                    new String[] {PROC_START_SEQ_IDENT + app.startSeq});
        } else {
            // 新建行程
            startResult = Process.start(entryPoint,
                    app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                    app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                    app.info.dataDir, invokeWith,
                    new String[] {PROC_START_SEQ_IDENT + app.startSeq});
        }
        checkTime(startTime, "startProcess: returned from zygote!");
        return startResult;
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    }
}

呼叫 Process.start() 方法新建行程,繼續追進去:

> Process.java

public static final ProcessStartResult start(
                // android.app.ActivityThread,創建行程后會呼叫其 main() 方法
                final String processClass,
                final String niceName, // 行程名
                int uid, int gid, int[] gids,
                int runtimeFlags, int mountExternal,
                int targetSdkVersion,
                String seInfo,
                String abi,
                String instructionSet,
                String appDataDir,
                String invokeWith, // 一般新建應用行程時,此引數不為 null
                String[] zygoteArgs) {
        return zygoteProcess.start(processClass, niceName, uid, gid, gids,
                    runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                    abi, instructionSet, appDataDir, invokeWith, zygoteArgs);
    }

繼續呼叫 zygoteProcess.start()

> ZygoteProess.java

public final Process.ProcessStartResult start(final String processClass,
                                              final String niceName,
                                              int uid, int gid, int[] gids,
                                              int runtimeFlags, int mountExternal,
                                              int targetSdkVersion,
                                              String seInfo,
                                              String abi,
                                              String instructionSet,
                                              String appDataDir,
                                              String invokeWith,
                                              String[] zygoteArgs) {
    try {
        return startViaZygote(processClass, niceName, uid, gid, gids,
                runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                abi, instructionSet, appDataDir, invokeWith, false /* startChildZygote */,
                zygoteArgs);
    } catch (ZygoteStartFailedEx ex) {
        Log.e(LOG_TAG,
                "Starting VM process through Zygote failed");
        throw new RuntimeException(
                "Starting VM process through Zygote failed", ex);
    }
}

呼叫 startViaZygote() 方法,終于看到 Zygote 的身影了,

startViaZygote()

> ZygoteProcess.java

private Process.ProcessStartResult startViaZygote(final String processClass,
                                                  final String niceName,
                                                  final int uid, final int gid,
                                                  final int[] gids,
                                                  int runtimeFlags, int mountExternal,
                                                  int targetSdkVersion,
                                                  String seInfo,
                                                  String abi,
                                                  String instructionSet,
                                                  String appDataDir,
                                                  String invokeWith,
                                                  boolean startChildZygote, // 是否克隆 zygote 行程的所有狀態
                                                  String[] extraArgs)
                                                  throws ZygoteStartFailedEx {
    ArrayList<String> argsForZygote = new ArrayList<String>();

    // --runtime-args, --setuid=, --setgid=,
    // and --setgroups= must go first
    // 處理引數
    argsForZygote.add("--runtime-args");
    argsForZygote.add("--setuid=" + uid);
    argsForZygote.add("--setgid=" + gid);
    argsForZygote.add("--runtime-flags=" + runtimeFlags);
    if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) {
        argsForZygote.add("--mount-external-default");
    } else if (mountExternal == Zygote.MOUNT_EXTERNAL_READ) {
        argsForZygote.add("--mount-external-read");
    } else if (mountExternal == Zygote.MOUNT_EXTERNAL_WRITE) {
        argsForZygote.add("--mount-external-write");
    }
    argsForZygote.add("--target-sdk-version=" + targetSdkVersion);

    // --setgroups is a comma-separated list
    if (gids != null && gids.length > 0) {
        StringBuilder sb = new StringBuilder();
        sb.append("--setgroups=");

        int sz = gids.length;
        for (int i = 0; i < sz; i++) {
            if (i != 0) {
                sb.append(',');
            }
            sb.append(gids[i]);
        }

        argsForZygote.add(sb.toString());
    }

    if (niceName != null) {
        argsForZygote.add("--nice-name=" + niceName);
    }

    if (seInfo != null) {
        argsForZygote.add("--seinfo=" + seInfo);
    }

    if (instructionSet != null) {
        argsForZygote.add("--instruction-set=" + instructionSet);
    }

    if (appDataDir != null) {
        argsForZygote.add("--app-data-dir=" + appDataDir);
    }

    if (invokeWith != null) {
        argsForZygote.add("--invoke-with");
        argsForZygote.add(invokeWith);
    }

    if (startChildZygote) {
        argsForZygote.add("--start-child-zygote");
    }

    argsForZygote.add(processClass);

    if (extraArgs != null) {
        for (String arg : extraArgs) {
            argsForZygote.add(arg);
        }
    }

    synchronized(mLock) {
        // 和 Zygote 行程進行 socket 通信
        return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
    }
}

前面一大串代碼都是在處理引數,大致瀏覽即可,核心在于最后的 openZygoteSocketIfNeeded()zygoteSendArgsAndGetResult() 這兩個方法,從方法命名就可以看出來,這里要和 Zygote 進行 socket 通信了,還記得 ZygoteInit.main() 方法中呼叫的 registerServerSocketFromEnv() 方法嗎?它在 Zygote 行程中創建了服務端 socket,

openZygoteSocketIfNeeded()

先來看看 openZygoteSocketIfNeeded() 方法,

> ZygoteProcess.java

private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
    Preconditions.checkState(Thread.holdsLock(mLock), "ZygoteProcess lock not held");
    
    // 未連接或者連接已關閉
    if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
        try {
            // 開啟 socket 連接
            primaryZygoteState = ZygoteState.connect(mSocket);
        } catch (IOException ioe) {
            throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
        }
        maybeSetApiBlacklistExemptions(primaryZygoteState, false);
        maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
    }
    if (primaryZygoteState.matches(abi)) {
        return primaryZygoteState;
    }

    // 當主 zygote 沒有匹配成功,嘗試 connect 第二個 zygote
    if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
        try {
            secondaryZygoteState = ZygoteState.connect(mSecondarySocket);
        } catch (IOException ioe) {
            throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
        }
        maybeSetApiBlacklistExemptions(secondaryZygoteState, false);
        maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState);
    }

    if (secondaryZygoteState.matches(abi)) {
        return secondaryZygoteState;
    }

    throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
}

如果與 Zygote 行程的 socket 連接未開啟,則嘗試開啟,可能會產生阻塞和重試,連接呼叫的是 ZygoteState.connect() 方法,ZygoteStateZygoteProcess 的內部類,

> ZygoteProcess.java

public static class ZygoteState {
       final LocalSocket socket;
       final DataInputStream inputStream;
       final BufferedWriter writer;
       final List<String> abiList;

       boolean mClosed;

       private ZygoteState(LocalSocket socket, DataInputStream inputStream,
               BufferedWriter writer, List<String> abiList) {
           this.socket = socket;
           this.inputStream = inputStream;
           this.writer = writer;
           this.abiList = abiList;
       }

       public static ZygoteState connect(LocalSocketAddress address) throws IOException {
           DataInputStream zygoteInputStream = null;
           BufferedWriter zygoteWriter = null;
           final LocalSocket zygoteSocket = new LocalSocket();

           try {
               zygoteSocket.connect(address);

               zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream());

               zygoteWriter = new BufferedWriter(new OutputStreamWriter(
                       zygoteSocket.getOutputStream()), 256);
           } catch (IOException ex) {
               try {
                   zygoteSocket.close();
               } catch (IOException ignore) {
               }

               throw ex;
           }

           String abiListString = getAbiList(zygoteWriter, zygoteInputStream);
           Log.i("Zygote", "Process: zygote socket " + address.getNamespace() + "/"
                   + address.getName() + " opened, supported ABIS: " + abiListString);

           return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter,
                   Arrays.asList(abiListString.split(",")));
       }
   ...
}

通過 socket 連接 Zygote 遠程服務端,

再回頭看之前的 zygoteSendArgsAndGetResult() 方法,

zygoteSendArgsAndGetResult()

 > ZygoteProcess.java
 
private static Process.ProcessStartResult zygoteSendArgsAndGetResult(
       ZygoteState zygoteState, ArrayList<String> args)
       throws ZygoteStartFailedEx {
   try {
       ...
       final BufferedWriter writer = zygoteState.writer;
       final DataInputStream inputStream = zygoteState.inputStream;

       writer.write(Integer.toString(args.size()));
       writer.newLine();

       // 向 zygote 行程發送引數
       for (int i = 0; i < sz; i++) {
           String arg = args.get(i);
           writer.write(arg);
           writer.newLine();
       }

       writer.flush();

       // 是不是應該有一個超時時間?
       Process.ProcessStartResult result = new Process.ProcessStartResult();

       // Always read the entire result from the input stream to avoid leaving
       // bytes in the stream for future process starts to accidentally stumble
       // upon.
       // 讀取 zygote 行程回傳的子行程 pid
       result.pid = inputStream.readInt();
       result.usingWrapper = inputStream.readBoolean();

       if (result.pid < 0) { // pid 小于 0 ,fork 失敗
           throw new ZygoteStartFailedEx("fork() failed");
       }
       return result;
   } catch (IOException ex) {
       zygoteState.close();
       throw new ZygoteStartFailedEx(ex);
   }
}

通過 socket 發送請求引數,然后等待 Zygote 行程回傳子行程 pid ,客戶端的作業到這里就暫時完成了,我們再追蹤到服務端,看看服務端是如何處理客戶端請求的,

Zygote 處理客戶端請求

Zygote 處理客戶端請求的代碼在 ZygoteServer.runSelectLoop() 方法中,

> ZygoteServer.java

Runnable runSelectLoop(String abiList) {
   ...

   while (true) {
      ...
       try {
           // 有事件來時往下執行,沒有時就阻塞
           Os.poll(pollFds, -1);
       } catch (ErrnoException ex) {
           throw new RuntimeException("poll failed", ex);
       }
       for (int i = pollFds.length - 1; i >= 0; --i) {
           if ((pollFds[i].revents & POLLIN) == 0) {
               continue;
           }

           if (i == 0) { // 有新客戶端連接
               ZygoteConnection newPeer = acceptCommandPeer(abiList);
               peers.add(newPeer);
               fds.add(newPeer.getFileDesciptor());
           } else { // 處理客戶端請求
               try {
                   ZygoteConnection connection = peers.get(i);
                   // fork 子行程,并回傳包含子行程 main() 函式的 Runnable 物件
                   final Runnable command = connection.processOneCommand(this);

                   if (mIsForkChild) {
                       // 位于子行程
                       if (command == null) {
                           throw new IllegalStateException("command == null");
                       }

                       return command;
                   } else {
                       // 位于父行程
                       if (command != null) {
                           throw new IllegalStateException("command != null");
                       }

                       if (connection.isClosedByPeer()) {
                           connection.closeSocket();
                           peers.remove(i);
                           fds.remove(i);
                       }
                   }
               } catch (Exception e) {
                   ...
               } finally {
                   mIsForkChild = false;
               }
           }
       }
   }
}

acceptCommandPeer() 方法用來回應新客戶端的 socket 連接請求,processOneCommand() 方法用來處理客戶端的一般請求,

processOneCommand()

> ZygoteConnection.java

Runnable processOneCommand(ZygoteServer zygoteServer) {
    String args[];
    Arguments parsedArgs = null;
    FileDescriptor[] descriptors;

    try {
        // 1. 讀取 socket 客戶端發送過來的引數串列
        args = readArgumentList();
        descriptors = mSocket.getAncillaryFileDescriptors();
    } catch (IOException ex) {
        throw new IllegalStateException("IOException on command socket", ex);
    }

    ...

    // 2. fork 子行程
    pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
            parsedArgs.runtimeFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
            parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.startChildZygote,
            parsedArgs.instructionSet, parsedArgs.appDataDir);

    try {
        if (pid == 0) {
            // 處于進子行程
            zygoteServer.setForkChild();
            // 關閉服務端 socket
            zygoteServer.closeServerSocket();
            IoUtils.closeQuietly(serverPipeFd);
            serverPipeFd = null;
            // 3. 處理子行程事務
            return handleChildProc(parsedArgs, descriptors, childPipeFd,
                    parsedArgs.startChildZygote);
        } else {
            // 處于 Zygote 行程
            IoUtils.closeQuietly(childPipeFd);
            childPipeFd = null;
            // 4. 處理父行程事務
            handleParentProc(pid, descriptors, serverPipeFd);
            return null;
        }
    } finally {
        IoUtils.closeQuietly(childPipeFd);
        IoUtils.closeQuietly(serverPipeFd);
    }
}

processOneCommand() 方法大致可以分為五步,下面逐步分析,

readArgumentList()

> ZygoteConnection.java

private String[] readArgumentList()
        throws IOException {

    int argc;

    try {
        // 逐行讀取引數
        String s = mSocketReader.readLine();

        if (s == null) {
            // EOF reached.
            return null;
        }
        argc = Integer.parseInt(s);
    } catch (NumberFormatException ex) {
        throw new IOException("invalid wire format");
    }

    // See bug 1092107: large argc can be used for a DOS attack
    if (argc > MAX_ZYGOTE_ARGC) {
        throw new IOException("max arg count exceeded");
    }

    String[] result = new String[argc];
    for (int i = 0; i < argc; i++) {
        result[i] = mSocketReader.readLine();
        if (result[i] == null) {
            // We got an unexpected EOF.
            throw new IOException("truncated request");
        }
    }

    return result;
}

讀取客戶端發送過來的請求引數,

forkAndSpecialize()

> Zygote.java

public static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags,
      int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
      int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir) {
    VM_HOOKS.preFork();
    // Resets nice priority for zygote process.
    resetNicePriority();
    int pid = nativeForkAndSpecialize(
              uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose,
              fdsToIgnore, startChildZygote, instructionSet, appDataDir);
    // Enable tracing as soon as possible for the child process.
    if (pid == 0) {
        Trace.setTracingEnabled(true, runtimeFlags);

        // Note that this event ends at the end of handleChildProc,
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork");
    }
    VM_HOOKS.postForkCommon();
    return pid;
}

nativeForkAndSpecialize() 是一個 native 方法,在底層 fork 了一個新行程,并回傳其 pid,不要忘記了這里的 一次fork,兩次回傳pid > 0 說明還是父行程,pid = 0 說明進入了子行程,子行程中會呼叫 handleChildProc,而父行程中會呼叫 handleParentProc()

handleChildProc()

> ZygoteConnection.java

private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,
        FileDescriptor pipeFd, boolean isZygote) {
    closeSocket(); // 關閉 socket 連接
    ...

    if (parsedArgs.niceName != null) {
        // 設定行程名
        Process.setArgV0(parsedArgs.niceName);
    }

    if (parsedArgs.invokeWith != null) {
        WrapperInit.execApplication(parsedArgs.invokeWith,
                parsedArgs.niceName, parsedArgs.targetSdkVersion,
                VMRuntime.getCurrentInstructionSet(),
                pipeFd, parsedArgs.remainingArgs);

        // Should not get here.
        throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
    } else {
        if (!isZygote) { // 新建應用行程時 isZygote 引數為 false
            return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,
                    null /* classLoader */);
        } else {
            return ZygoteInit.childZygoteInit(parsedArgs.targetSdkVersion,
                    parsedArgs.remainingArgs, null /* classLoader */);
        }
    }
}

當看到 ZygoteInit.zygoteInit() 時你應該感覺很熟悉了,接下來的流程就是:

ZygoteInit.zygoteInit()RuntimeInit.applicationInit()findStaticMain()

SystemServer 行程的創建流程一致,這里要找的 main 方法就是 ActivityThrad.main()ActivityThread 雖然并不是一個執行緒,但你可以把它理解為應用的主執行緒,

handleParentProc()

> ZygoteConnection.java

private void handleParentProc(int pid, FileDescriptor[] descriptors, FileDescriptor pipeFd) {
        if (pid > 0) {
            setChildPgid(pid);
        }

        if (descriptors != null) {
            for (FileDescriptor fd: descriptors) {
                IoUtils.closeQuietly(fd);
            }
        }

        boolean usingWrapper = false;
        if (pipeFd != null && pid > 0) {
            int innerPid = -1;
            try {
                // Do a busy loop here. We can't guarantee that a failure (and thus an exception
                // bail) happens in a timely manner.
                final int BYTES_REQUIRED = 4;  // Bytes in an int.

                StructPollfd fds[] = new StructPollfd[] {
                        new StructPollfd()
                };

                byte data[] = new byte[BYTES_REQUIRED];

                int remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS;
                int dataIndex = 0;
                long startTime = System.nanoTime();

                while (dataIndex < data.length && remainingSleepTime > 0) {
                    fds[0].fd = pipeFd;
                    fds[0].events = (short) POLLIN;
                    fds[0].revents = 0;
                    fds[0].userData = https://www.cnblogs.com/bingxinshuo/p/null;

                    int res = android.system.Os.poll(fds, remainingSleepTime);
                    long endTime = System.nanoTime();
                    int elapsedTimeMs = (int)((endTime - startTime) / 1000000l);
                    remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS - elapsedTimeMs;

                    if (res > 0) {
                        if ((fds[0].revents & POLLIN) != 0) {
                            // Only read one byte, so as not to block.
                            int readBytes = android.system.Os.read(pipeFd, data, dataIndex, 1);
                            if (readBytes < 0) {
                                throw new RuntimeException("Some error");
                            }
                            dataIndex += readBytes;
                        } else {
                            // Error case. revents should contain one of the error bits.
                            break;
                        }
                    } else if (res == 0) {
                        Log.w(TAG, "Timed out waiting for child.");
                    }
                }

                if (dataIndex == data.length) {
                    DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
                    innerPid = is.readInt();
                }

                if (innerPid == -1) {
                    Log.w(TAG, "Error reading pid from wrapped process, child may have died");
                }
            } catch (Exception ex) {
                Log.w(TAG, "Error reading pid from wrapped process, child may have died", ex);
            }

            // Ensure that the pid reported by the wrapped process is either the
            // child process that we forked, or a descendant of it.
            if (innerPid > 0) {
                int parentPid = innerPid;
                while (parentPid > 0 && parentPid != pid) {
                    parentPid = Process.getParentPid(parentPid);
                }
                if (parentPid > 0) {
                    Log.i(TAG, "Wrapped process has pid " + innerPid);
                    pid = innerPid;
                    usingWrapper = true;
                } else {
                    Log.w(TAG, "Wrapped process reported a pid that is not a child of "
                            + "the process that we forked: childPid=" + pid
                            + " innerPid=" + innerPid);
                }
            }
        }

        try {
            mSocketOutStream.writeInt(pid);
            mSocketOutStream.writeBoolean(usingWrapper);
        } catch (IOException ex) {
            throw new IllegalStateException("Error writing to command socket", ex);
        }
    }

主要進行一些資源清理的作業,到這里,子行程就創建完成了,

總結

  1. 呼叫 Process.start() 創建應用行程
  2. ZygoteProcess 負責和 Zygote 行程建立 socket 連接,并將創建行程需要的引數發送給 Zygote 的 socket 服務端
  3. Zygote 服務端接收到引數之后呼叫 ZygoteConnection.processOneCommand() 處理引數,并 fork 行程
  4. 最后通過 findStaticMain() 找到 ActivityThread 類的 main() 方法并執行,子行程就啟動了

預告

到現在為止已經決議了 Zygote 行程 ,SystemServer 行程,以及應用行程的創建,下一篇的內容是和應用最密切相關的系統服務 ActivityManagerService , 來看看它在 SystemServer 中是如何被創建和啟動的,敬請期待!

文章首發微信公眾號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解,

更多最新原創文章,掃碼關注我吧!

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

標籤:Android

上一篇:Android開發——RecyclerView實作下載串列

下一篇:Android 開發涼了嗎!

標籤雲
其他(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