• [转]startx启动过程分析


    From: http://blog.csdn.net/hustwarhd/article/details/3069066

    startx启动过程分析

     

     

    JiananHe

    09/19/2008

     

    目录

    1     xinit

    1.1      功能

    1.2      用法

    1.3      例子

    1.4      分析

    2     startx脚本

    2.1      功能

    2.2      用法

    2.3      例子

    2.4      分析

    2.5      总结

    3     startx默认启动过程

    3.1      startx的几种启动方式

    3.2      Xsession

    4     startx启动过程小结

    1       xinit

    在说明startx之前,我想我们应该先了解一下xinit。因为startx就是通过调用xinit启动X的。

    1.1    功能

    当我们安装了Ubuntu后,默认就已经安装了xinit,它位于/usr/bin下。xinit是一个二进制文件,并非是一个脚本。它的主要功能是启动一个X服务器,同时启动一个基于X的应用程序。

    1.2    用法

    xinit的用法为:xinit [[client] options ] [-- [server] [display] options]。其中client用于指定一个基于X的应用程序,client后面的options是传给这个应用程序的参数,server是用于指定启动哪个X服务器,一般为/usr/bin/X/usr/bin/Xorgdisplay用于指定display number,一般为0,表示第一个displayoption为传给server的参数。

     

    如果不指定clientxinit会查找HOME(环境变量)目录下的.xinitrc文件,如果存在这个文件,xinit直接调用execvp函数执行该文件。如果这个文件不存在,那么client及其options为: xterm -geometry +1+1 -n login -display :0 

     

    如果不指定serverxinit会查找HOME(环境变量)目录下的.xserverrc文件,如果存在这个文件,xinit直接调用execvp函数执行该文件。如果这个文件不存在,那么server及其display为: X :0。如果系统目录中不存在X命令,那么我们需要在系统目录下建立一个名为X的链接,使其指向真正的X server命令(Ubuntu下为Xorg)。

     

    1.3    例子

    下面是几个关于xinit应用的例子:

    1)      xinit /usr/bin/xclock -- /usr/bin/X :0

    该例子将启动X server, 同时将会启动xclock。请注意指定client或server时,需要用绝对路径,否则xinit将因无法区别是传给xterm或server的参数还是指定的client或server而直接当成是参数处理。

    2)      HOME下新建.xinitrc文件,并加入以下几行:

             xsetroot -solid gray &
             xclock -g 50x50-0+0 -bw 0 &
             xterm -g 80x24+0+0 &
             xterm -g 80x24+0-0 &
             twm

    xinit启动时,它会先启动X server,然后启动一个clock,两个xterm,最后启动窗口管理器twm。

    请注意:

    最后一个命令不能后台运行,否则所有命令都后台运行的话xinit就会返回退出,同样的,除最后一个命令外都必须后台运行,否则后面的命令将只有在该命令退出后才能运行。

     

    1.4    分析

    看到这里,眼尖的人或许早以看出xinit的功能完全可以由脚本来实现,例如要启动X Server 和一个xterm,就像xinit默认启动的那样,只需要在新建一个脚本或在rc.local中加入:

    X&

    export DISPLAY=:0.0

    xterm

    这个实现完全正确,然而却并没有完全实现xinit所具有的功能,xinit所有的一项功能就是当最后一个启动的client(如上面第二个例子中的twm窗口管理器)退出后,X服务器也会退出。而我们的脚本实现中当我们退出xterm后并不会退出X server

     

    2       startx脚本

    用过linux的人基本上都知道linux下有个命令叫做startx,那么它到底是怎么实现的呢?

    2.1    功能

    当我们在终端下想启动图形界面时,我们都会通过输入startx来实现,该命令可以启动一个Xserver,而且可以启动一个漂亮的图形界面(Ubuntu下,我装的是gnome)。

    2.2    用法

    Startx的用法和xinit的基本一样:startx [ [ client ] options ... ] [ -- [ server ] options ... ]。为什么呢?这是因为startx其实就是一个脚本,它启动X server就是通过调用xinit命令实现的,startx的参数将全部传给xinit。因此,这些参数的意义和xinit的参数是一样的。

    2.3    例子

    下面是两个关于startx命令的简单例子:

    1)  startx -- -depth 16

    该例子主要是以16位色启动X 服务器。

    2)  startx -- -dpi 100

    该例子主要是以100的dpi启动X 服务器。

    2.4    分析

    下面我们来分析一下startx脚本。startx脚本位于/usr/bin下,直接用vim打开我们可以看到它的具体实现如下:

     

    #!/bin/bash #注意:该脚本用的是bash shell解析的

     

    # $Xorg: startx.cpp,v 1.3 2000/08/17 19:54:29 cpqbld Exp $

    #

    # This is just a sample implementation of a slightly less primitive

    # interface than xinit. It looks for user .xinitrc and .xserverrc

    # files, then system xinitrc and xserverrc files, else lets xinit choose

    # its default. The system xinitrc should probably do things like check

    # for .Xresources files and merge them in, startup up a window manager,

    # and pop a clock and serveral xterms.

    #

    # Site administrators are STRONGLY urged to write nicer versions.

    #

    # $XFree86: xc/programs/xinit/startx.cpp,v 3.16tsi Exp $

     

    #下面主要是对一些变量进行赋值。

    userclientrc=$HOME/.xinitrc

    sysclientrc=/etc/X11/xinit/xinitrc

     

     

    userserverrc=$HOME/.xserverrc

    sysserverrc=/etc/X11/xinit/xserverrc

    defaultclient=xterm

    defaultserver=/usr/bin/X

    defaultclientargs=""

    defaultserverargs=""

    clientargs=""

    serverargs=""

     

    #下面的语句主要是说:如果$HOME/.xinitrc文件存在,并且不是一个目录,那么就将defaultclientargs赋值为$HOME/.xinitrc,否则,如果/etc/X11/xinit/xinitrc存在并且不是一个目录,就将defaultclientargs赋值为/etc/X11/xinit/xinitrc。

     

    if [ -f $userclientrc ]; then

        defaultclientargs=$userclientrc

    elif [ -f $sysclientrc ]; then

        defaultclientargs=$sysclientrc

     

     

     

     

     

     

    fi

     

    #下面的语句主要是说:如果$HOME/.xserverrc文件存在,并且不是一个目录,那么就将defaultclientargs赋值为$HOME/.xserverrc,否则,如果/etc/X11/xinit/xserverrc存在并且不是一个目录,就将defaultclientargs赋值为/etc/X11/xinit/xserverrc。

     

    if [ -f $userserverrc ]; then

        defaultserverargs=$userserverrc

    elif [ -f $sysserverrc ]; then

        defaultserverargs=$sysserverrc

    fi

    #将whoseargs变量赋值为字符串“client”,表示当前解析的指定client的参数。

    whoseargs="client"

    #当startx的一个参数不为空时就进入while循环。

    while [ x"$1" != x ]; do

        case "$1" in

        # '' required to prevent cpp from treating "/*" as a C comment.

        /''*|/./''*)

     if [ "$whoseargs" = "client" ]; then

         if [ x"$clientargs" = x ]; then

      client="$1" #解析出了用户指定的Client程序

         else

      clientargs="$clientargs $1"  #解析出了Client的参数

         fi

     else

         if [ x"$serverargs" = x ]; then

      server="$1"  #解析出了用户指定的X Server程序

         else

      serverargs="$serverargs $1" #解析出了X Server的参数

         fi

     fi

     ;;

        --)    #遇到“- -”就解析server

     whoseargs="server"

     ;;

        *)

     if [ "$whoseargs" = "client" ]; then

         clientargs="$clientargs $1"

     else

         # display must be the FIRST server argument

         if [ x"$serverargs" = x ] && /

       expr "$1" : ':[0-9][0-9]*$' > /dev/null 2>&1; then

      display="$1" #解析出display

         else

      serverargs="$serverargs $1"

         fi

     fi

     ;;

        esac     #case语句结束

    shift  #将参数列表左移一位,即解析下个参数.

    done

     

    # process client arguments

    if [ x"$client" = x ]; then #如果client变量为空,即用户没有指定client。

        # if no client arguments either, use rc file instead

        if [ x"$clientargs" = x ]; then #如果用户没有指定client参数 就将client设为前面设定的默认的rc文件(为$HOME/.xinitrc,或/etc/X11/xinit/xinitrc)

     client="$defaultclientargs"

        else

     client=$defaultclient #如果用户指定了client参数,就将client设定为xterm

        fi

    fi

     

    # process server arguments

    if [ x"$server" = x ]; then #如果server变量为空,即用户没有指定server。

        # if no server arguments or display either, use rc file instead

        if [ x"$serverargs" = x -a x"$display" = x ]; then #如果serverargs为空,并且display为空,就将server设为前面设定的默认的rc文件(为$HOME/. xserverrc,或/etc/X11/xinit/ xserverrc)

     server="$defaultserverargs"

        else

     server=$defaultserver #如果用户指定了serverargs或display,就将server设定为/usr/bin/X

        fi

    fi

     

    if [ x"$XAUTHORITY" = x ]; then #如果环境变量XAUTHORITY为空,就设定为$HOME/.Xauthority

        XAUTHORITY=$HOME/.Xauthority

        export XAUTHORITY

    fi

     

    removelist=

     

    # set up default Xauth info for this machine

     

    # check for GNU hostname

    if hostname --version > /dev/null 2>&1; then #如果hostname命令存在

           if [ -z "`hostname --version 2>&1 | grep GNU`" ]; then #如果hostname –version中不包含GNU就将hostname变量设定为命令hostname –f返回的字符串。

      hostname=`hostname -f`

           fi

    fi

     

    if [ -z "$hostname" ]; then #如果hostname长度为0,就将hostname变量设定为命令hostname返回的字符串。

    hostname=`hostname`

    fi

     

    authdisplay=${display:-:0}

     

    mcookie=`/usr/bin/mcookie`

     

     

     

     

     

     

     

    dummy=0

     

    # create a file with auth information for the server. ':0' is a dummy.

    xserverauthfile=`mktemp -p /tmp serverauth.XXXXXXXXXX`

    trap "rm -f $xserverauthfile" HUP INT QUIT ILL TRAP KILL BUS TERM

    xauth -q -f $xserverauthfile << EOF

    add :$dummy . $mcookie

    EOF

    serverargs=${serverargs}" -auth "${xserverauthfile}

     

    # now add the same credentials to the client authority file

    # if '$displayname' already exists do not overwrite it as another

    # server man need it. Add them to the '$xserverauthfile' instead.

    for displayname in $authdisplay $hostname$authdisplay; do

         authcookie=`xauth list "$displayname" /

           | sed -n "s/.*$displayname[[:space:]*].*[[:space:]*]//p"` 2>/dev/null;

        if [ "z${authcookie}" = "z" ] ; then

            xauth -q << EOF

    add $displayname . $mcookie

    EOF

     removelist="$displayname $removelist"

        else

            dummy=$(($dummy+1));

            xauth -q -f $xserverauthfile << EOF

    add :$dummy . $authcookie

    EOF

        fi

    done

    echo "client=$client,clientargs=$clientargs,server= $server, display= $display, serverargs=$serverargs"

     

    #下面的语句通过xinit启动X server和Clients

    xinit $client $clientargs -- $server $display $serverargs

     

     

    if [ x"$removelist" != x ]; then

        xauth remove $removelist

    fi

    if [ x"$xserverauthfile" != x ]; then

        rm -f $xserverauthfile

    fi

     

     

     

     

     

    if command -v deallocvt > /dev/null 2>&1; then

        deallocvt  #释放所有未使用的虚拟终端的核心内存和数据结构

    fi

     

     

    2.5    总结

    由以上对startx脚本的分析,我们可以知道:startx将会先解析用户的参数,如果该用户指定了该参数(即解析结果不为空),那么startx就会以该参数来启动xinit,否则就会解析(与其说是解析,还不如说是执行)$HOME目录下的rc文件,如果该文件不存在,就会解析系统目录下(/etc/X11/xinit/)的rc文件,如果这个文件也不存在,那startx就将以默认的clientxterm)和server/usr/bin/X)为参数来启动xinit

     

    3       startx默认启动过程

    通过以上对startx脚本的分析,我们知道了startx的基本的启动流程,但是到目前为止,我们还不知道仅仅在终端输入startx是怎么样启动gnome那漂亮的桌面的,下面我们来看一下其启动过程。

    3.1    startx的几种启动方式

    由对startx脚本的分析,我们可以知道startx主要有三种启动方式:

    a)、一种是自己指定要启动的clientserver, 例如:startx /usr/bin/xclock -- /usr/bin/X :0

    b)、一种是通过在$HOME下新建.xinitrc文件来指定要启动的多个client.xserverrc来指定要启动的server(注意:这两个文件本来是不存在的);

    c)、还有一种是直接输入startx而不指定参数,这也就是我们启动gnome桌面的方法。这里

     

    我们主要介绍最后一种启动方法。

     

    在c这种启动方法中,我们可以知道,startx脚本会先去看系统目录(/etc/X11/xinit/)下的rc文件是否存在,如果不存在就会用默认的xterm/usr/bin/X来启动xinit。显然,startx启动的不是xterm,而是gnome桌面,因此gnome的启动是通过系统文件/etc/X11/xinit/xinitrc来指定的。

     

    /etc/X11/xinit/xinitrc文件的内容如下所示:

     

    #!/bin/bash  #注意该脚本用的是bash shell解析的

     

    # $Xorg: xinitrc.cpp,v 1.3 2000/08/17 19:54:30 cpqbld Exp $

     

    # /etc/X11/xinit/xinitrc

    #

    # global xinitrc file, used by all X sessions started by xinit (startx)

     

    # invoke global X session script

    . /etc/X11/Xsession  #在当前这个shell环境中执行Xsession脚本

     

     

    因此,gnome的启动应该在Xsession里。

     

    而X Server的启动则是通过系统文件/etc/X11/xinit/xserverrc来指定的,这个文件的内容为:

     

    #!/bin/sh #注意:该脚本用的是Bourne shell解析的

     

    # $Id: xserverrc 189 2005-06-11 00:04:27Z branden $

     

    exec /usr/bin/X11/X -nolisten tcp

     

    3.2      Xsession

    下面是Xsession脚本的内容:

    #!/bin/sh #注意:该脚本用的是Bourne shell解析的

    #

    # /etc/X11/Xsession

    #

    # global Xsession file -- used by display managers and xinit (startx)

     

    # $Id: Xsession 967 2005-12-27 07:20:55Z dnusinow $

     

    set –e #打开errexit选项,该选项表示:如果下面有命令返回的状态非0,则退出程序。

     

    PROGNAME=Xsession

     

    #下面四个是信息输出函数,可以不管

    message () {

      # pretty-print messages of arbitrary length; use xmessage if it

      # is available and $DISPLAY is set

      MESSAGE="$PROGNAME: $*"

      echo "$MESSAGE" | fold -s -w ${COLUMNS:-80} >&2

      if [ -n "$DISPLAY" ] && which xmessage > /dev/null 2>&1; then

        echo "$MESSAGE" | fold -s -w ${COLUMNS:-80} | xmessage -center -file -

      fi

    }

     

    message_nonl () {

      # pretty-print messages of arbitrary length (no trailing newline); use

      # xmessage if it is available and $DISPLAY is set

      MESSAGE="$PROGNAME: $*"

      echo -n "$MESSAGE" | fold -s -w ${COLUMNS:-80} >&2;

      if [ -n "$DISPLAY" ] && which xmessage > /dev/null 2>&1; then

        echo -n "$MESSAGE" | fold -s -w ${COLUMNS:-80} | xmessage -center -file -

      fi

    }

     

    errormsg () {

      # exit script with error

      message "$*"

      exit 1

    }

     

    internal_errormsg () {

      # exit script with error; essentially a "THIS SHOULD NEVER HAPPEN" message

    # One big call to message() for the sake of xmessage; if we had two then

      # the user would have dismissed the error we want reported before seeing the

      # request to report it.

      errormsg "$*" /

               "Please report the installed version of the /"x11-common/"" /

               "package and the complete text of this error message to" /

               "<debian-x@lists.debian.org>."

    }

     

    # initialize variables for use by all session scripts

     

    OPTIONFILE=/etc/X11/Xsession.options

     

    SYSRESOURCES=/etc/X11/Xresources

    USRRESOURCES=$HOME/.Xresources

     

    SYSSESSIONDIR=/etc/X11/Xsession.d

    USERXSESSION=$HOME/.xsession

    USERXSESSIONRC=$HOME/.xsessionrc

    ALTUSERXSESSION=$HOME/.Xsession

    ERRFILE=$HOME/.xsession-errors

     

    # attempt to create an error file; abort if we cannot

    if (umask 077 && touch "$ERRFILE") 2> /dev/null && [ -w "$ERRFILE" ] &&

      [ ! -L "$ERRFILE" ]; then

      chmod 600 "$ERRFILE"

    elif ERRFILE=$(tempfile 2> /dev/null); then

      if ! ln -sf "$ERRFILE" "${TMPDIR:=/tmp}/xsession-$USER"; then

        message "warning: unable to symlink /"$TMPDIR/xsession-$USER/" to" /

                 "/"$ERRFILE/"; look for session log/errors in" /

                 "/"$TMPDIR/xsession-$USER/"."

      fi

    else

      errormsg "unable to create X session log/error file; aborting."

    fi

    # truncate ERRFILE if it is too big to avoid disk usage DoS

    if [ "`stat -c%s /"$ERRFILE/"`" -gt 500000 ]; then

      T=`mktemp -p "$HOME"`

      tail -c 500000 "$ERRFILE" > "$T" && mv -f "$T" "$ERRFILE" || rm -f "$T"

    fi

     

    exec >>"$ERRFILE" 2>&1

    echo "$PROGNAME: X session started for $LOGNAME at $(date)"

     

    # sanity check; is our session script directory present?

     

    #如果/etc/X11/Xsession.d不存在或不是一个目录则打印错误信息并退出。

    if [ ! -d "$SYSSESSIONDIR" ]; then

      errormsg "no /"$SYSSESSIONDIR/" directory found; aborting."

    fi

    # Attempt to create a file of non-zero length in /tmp; a full filesystem can

    # cause mysterious X session failures.  We do not use touch, :, or test -w

    # because they won't actually create a file with contents.  We also let standard

    # error from tempfile and echo go to the error file to aid the user in

    # determining what went wrong.

    WRITE_TEST=$(tempfile)

    if ! echo "*" >>"$WRITE_TEST"; then

      message "warning: unable to write to ${WRITE_TEST%/*}; X session may exit" /

              "with an error"

    fi

    rm -f "$WRITE_TEST"

    # use run-parts to source every file in the session directory; we source

    # instead of executing so that the variables and functions defined above

    # are available to the scripts, and so that they can pass variables to each

    # other

     

    #将/etc/X11/Xsession.d目录中的所有文件都读出,并存入SESSIONFILES变量中。

    SESSIONFILES=$(run-parts --list $SYSSESSIONDIR)

    #如果SESSIONFILES变量中的字符串不为空,即/etc/X11/Xsession.d中有文件存在

    if [ -n "$SESSIONFILES" ]; then

      set +e #关闭errexit选项

      for SESSIONFILE in $SESSIONFILES; do

        . $SESSIONFILE #在当前shell环境下执行该文件

      done

      set –e #打卡errexit选项

    fi

    exit 0

     

    从以上的对Xsession脚本文件的分析,可以看出,Xsession脚本仅仅是执行了/etc/X11/Xsession.d目录下的所有文件,在该目录下,文件包括:

    20x11-common_process-args

    30x11-common_xresources

    40x11-common_xsessionrc

    50x11-common_determine-startup

    55gnome-session_gnomerc

    60seahorse

    60xdg-user-dirs-update

    80im-switch

    90-console-kit

    90x11-common_ssh-agent

    99x11-common_start

     

    每个文件名都以数字开头,这主要是为了确保这些脚本的执行顺序,run-parts会将数字小的排在前面,这样就能确保以上文件能按数字由小到大的顺序执行。

     

    1、20x11-common_process-args

        这个文件主要是处理传给/etc/X11/xinit/ xinitrc脚本文件的参数的。该参数个数只能为0或一个,否则将不进行任何处理。如果该参数是failsafe,则该脚本将执行x-terminal-emulator,否则就执行该参数。需要说明的是,x-terminal-emulator是一个符号链接,指向/etc/alternatives/x-terminal-emulator,同时,/etc/alternatives/x-terminal-emulator也是一个符号链接,它指向/usr/bin/gnome-terminal.wrapper,而gnome-terminal.wrapper则是一个perl脚本,它最终是调用了gnome-terminal。

     

    2、30x11-common_xresources

        该文件主要是调用xrdb来将/etc/X11/Xresources目录下及$HOME/.Xresources目录下的文件的内容来设置根窗口的屏幕 0 上的RESOURCE_MANAGER属性的内容。

     

    3、40x11-common_xsessionrc

        该文件主要是判断$HOME/.xsessionrc文件是否存在,如果存在则执行该脚本文件。

     

    4、50x11-common_determine-startup

        该文件主要先查看配置文件/etc/X11/Xsession.options中是否允许使用用户的xsession,如果/etc/X11/Xsession.options中存在allow-user-xsession字段,则查看$HOME/.xsession是否存在并有执行权限,如果是,则将STARTUP变量设置为该文件,如果没有执行权限就将STARTUP变量设置为“sh 该xsession文件”。如果此时STARTUP变量仍然为空,则将其设置为x-session-manager,x-window-manager或x-terminal-emulator。注意:这个STARTUP将会在后面的脚本中被启动。

     

    5、55gnome-session_gnomerc

       该文件会先得到STARTUP的basename,如:STARTUP=/usr/bin/x-session-manager,则其basename为x-session-manager。再判断该basename 是否为gnome-session,或者为x-session-manager并且x-session-manager是个符号链接,它指向/usr/bin/gnome-session,如果是则执行$HOME/.gnomerc(如果该文件存在并且可读)。

     

    6、60seahorse

       将STARTUP重新赋值为“/usr/bin/seahorse-agent $STARTUP”,这个可能只是为安全考虑才这么做的,具体的我也不是很清楚,只是看了一下seahorse-agent的帮助,知道seahorse是一个GNOME的应用程序,它用于为用户的输入进行暂时的安全存储,而seahorse-agent则是seahorse的一个代理而已。

     

    7、60xdg-user-dirs-update

       用xdg-user-dirs-update自动生成$HOME下的文件夹,该命令主要是根据/etc/xdg/user-dirs.defaults文件的内容来为用户创建文件夹的。

     

    8、80im-switch

       该文件主要用于设置输入法。具体的请自己参考文件内容。

     

    9、90-console-kit

       如果环境变量$XDG_SESSION_COOKIE为空,并且/usr/bin/ck-launch-session可执行,则将STARTUP重新赋值为” /usr/bin/ck-launch-session $STARTUP”。至于ck-launch-session的功能,我也不是很清楚,估计是和session有关。

     

    10、90x11-common_ssh-agent

       该文件主要先查看配置文件/etc/X11/Xsession.options中是否使用ssh agent,如果/etc/X11/Xsession.options中存在use-ssh-agent字段,则判断/usr/bin/ssh-agent是否可执行,并且环境变量$SSH_AUTH_SOCK和$SSH2_AUTH_SOCK是否都为空,如果是,这将STARTUP重新赋值为” /usr/bin/ssh-agent $STARTUP”。

     

    11、99x11-common_start

        它仅仅是用exec启动$STARTUP。关于exec,在Bourne shell中,它与fork的区别就在于它执行一个新的脚本不需创建sub-shell,而它与Source和Dot的区别就在与在这条语句后面的语句将不会再被执行。此时,我们可以发现变量$STARTUP的值为:“startup=/usr/bin/ssh-agent  /usr/bin/ck-launch-session /usr/bin/seahorse-agent --execute x-session-manager”, 因此,最终将会被执行的就是这么一条语句。而x-session-manager在Ubuntu8.04中仅仅是个符号链接,它最终指向的是gnome-session。

        gnome-session则是启动GNOME桌面环境的,这个程序一般被登入管理器gdm、xdm和脚本startx调用。(gnome-session如何启动桌面,待研究)

     

     

    4       startx启动过程小结

    综上所述,startx的默认启动过程为:startx调用并将系统文件/etc/X11/xinit/xinitrc/etc/X11/xinit/xserverrc 作为参数传给xinitxinit就会先执行系统文件/etc/X11/xinit/xserverrc以启动X Server,然后执行/etc/X11/xinit/xinitrc,而xinitrc则会执行脚本/etc/X11/Xsession,而Xsession则会按顺序调用执行/etc/X11/Xsession.d目录下的文件,从而最终调用了gnome-session这个用于启动GNOME桌面环境的程序。

  • 相关阅读:
    WP8日历(含农历)APP
    NHibernate3剖析:Mapping篇之集合映射基础(2):Bag映射
    初探springmvc
    树的子结构
    Java内存分析
    java8_api_misc
    iOS开发多线程篇 09 —NSOperation简单介绍
    CALayer1-简介
    NSCharacterSet
    iOS 音频开发
  • 原文地址:https://www.cnblogs.com/cnland/p/2866505.html
Copyright © 2020-2023  润新知