• 1.android消息机制-java


    分析过,应用层消息机制 再仔细扒一扒看看,后面继续扒一扒native

    Android有大量的消息驱动方式来进行交互,四大组件启动过程交互都离不开消息机制。主要涉及MessageQueue、Message,Looper,Handler四个类

    • Handler 向MessageQueue发送Message
    • Message 作为消息载体,携带具体数据
    • MessageQueue Message容器
    • Looper 内部对MessageQueue进行无限循环,有消息就去除交给Handler处理,没消息就阻塞

    handler要想起作用有三个步骤:

    1. 创建Looper
    2. 创建当前线程的Handler
    3. 调用Looper的looper方法

    结构图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gO57aH4o-1656519751011)(C:\Users\bafc\AppData\Roaming\Typora\typora-user-images\image-20220628230832580.png)]

    1 message 消息池

    public final class Message implements Parcelable {
    	//当Handler发送多个消息时,用作不同消息处理的区分
    	public int what;
    	//当你传输的数据仅仅是int型时优先使用arg1,arg2
    	public int arg1;
        public int arg2;
        //Message携带的数据
    	public Object obj;
    	//Message执行的延迟时间
    	public long when;
    	//处理此Message的目标Handler
    	Handler target;
    	//Message的回调,如果设置了此回调,handler的handleMessage方法
    	//将不会被执行
    	Runnable callback;
    	//Message采用链表的形式进行复用,next指向下一个消息
    	Message next;
    	//Message消息池的头指针
    	private static Message sPool;
    	//Message消息池的大小
    	private static int sPoolSize = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    sPool总是指向消息链表的头部,首次创建Message时sPool为空,就需要通过new Message的方式创建Message,我们看下sPool首次是在哪里赋值的,是在recycleUnchecked中,看名字以及注释我们知道此方法是Message使用完毕之后会调用,也就是说我们创建的第一个消息使用完毕之后,会赋值给sPool

    public static Message obtain() {
        synchronized (sPoolSync) {
            //如果sPool不为空,则从消息池取一个Message
            if (sPool != null) {
                //从消息池中取出头部这个消息
                Message m = sPool;
                //sPool继续指向下一个消息
                sPool = m.next;
                //将取出的消息的链断掉
                m.next = null;
                //修改此消息的flag为0
                m.flags = 0; // clear in-use flag
                //消息池大小减1
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }
    
    void recycleUnchecked() {
        //当Message使用完成之后会将Message的一系列数据初始化
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;
    
        synchronized (sPoolSync) {
            //MAX_POOL_SIZE为50,作为消息池的大小,当sPoolSize没有到50时,
            //则此消息应该加入消息池
            if (sPoolSize < MAX_POOL_SIZE) {
                //如果是第一次消息回收next = sPool = null
                next = sPool;
                //将当前这个消息赋值给sPool,我们可以看到sPool总是指向
                //最后调用recycleUnchecked方法的Message
                sPool = this;
                //消息池大小加1
                sPoolSize++;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    从obtain和recycleUnchecked方法中我们能够看出来Message内部消息池采用链表的形式,sPool指向链表的头,每次取出消息是从链表头部开始,每次消息回收也是加在链表头部

    2 .Message消息投递

    public Handler(@Nullable Callback callback, boolean async) {
        //获取当前线程的Looper对象,后面分析
        mLooper = Looper.myLooper();
        //如果Looper为空会抛如下这个异常,这个异常相信Android初学者
        //遇见过很多次
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                + " that has not called Looper.prepare()");
        }
        //获取当前线程Looper的MessageQueue
        mQueue = mLooper.mQueue;
        //Handler内部回调
        mCallback = callback;
        //是否是异步消息,后面分析
        mAsynchronous = async;
    }
    
    public final boolean sendMessage(@NonNull Message msg) {
            return sendMessageDelayed(msg, 0);
        }
    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
            if (delayMillis < 0) {
                delayMillis = 0;
            }
            return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
        }
    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
            MessageQueue queue = mQueue;//构造函数中由looper那里获取
            if (queue == null) {
                RuntimeException e = new RuntimeException(
                        this + " sendMessageAtTime() called with no mQueue");
                Log.w("Looper", e.getMessage(), e);
                return false;
            }
            return enqueueMessage(queue, msg, uptimeMillis);
        }
     private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
                long uptimeMillis) {
            //msg.target代表处理此消息的Handler
            msg.target = this;
            msg.workSourceUid = ThreadLocalWorkSource.getUid();
    		//是否是异步消息,后面分析
            if (mAsynchronous) {
                msg.setAsynchronous(true);
            }
            //最终调用MessageQueue的enqueueMessage将此Message投递进MessageQueue
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    3 looper

    // ActivityThread.java
    public static void main(String[] args) {
    
        Looper.prepareMainLooper();
        .......
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);
        ......
        Looper.loop();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    //Looper.java
    public static void prepareMainLooper() {
        //注意UI线程调用prepare时传递false,代表不允许Looper退出
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    //通过ThreadLocal保存当前线程的Looper对象,ThreadLocal用来存储线程私有变量,起到线程隔离的目的
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    //创建了MessageQueue,并保存了当前线程
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    
    //MessageQueue
    MessageQueue(boolean quitAllowed) {
            mQuitAllowed = quitAllowed;
            mPtr = nativeInit();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    4 enqueueMessage

    // 从handler那里接上,MessageQueue.java
    boolean enqueueMessage(Message msg, long when) {
        //如果有message而没有Handler则抛出异常
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        //如果此Message正在使用中则抛出异常
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
    
        synchronized (this) {
            //如果MessageQueue正在退出则抛出异常
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
            //将此Message添加flag,代表正在使用
            msg.markInUse();
            //when代表此Message执行的延迟时间
            msg.when = when;
            //mMessages指向Message消息链表的头部
            Message p = mMessages;
            //是否需要唤醒
            boolean needWake;
            //p == null代表此时没有消息,when == 0代表此消息需要立即执行
            //when < p.when代表新增加的消息比此时MessageQueue中的消息头
            //的消息还要先执行(消息队列的顺序是when最小排在头部,when最大排在尾部)
            if (p == null || when == 0 || when < p.when) {
                //所以如果进到此条件则代表增加的这条消息是最先执行的,应该加到
                //消息队列头部
                msg.next = p;
                mMessages = msg;
                //将当前MessageQueue的状态赋值给needWake,以便能在阻塞时唤醒
                //MessageQueue
                needWake = mBlocked;
            } else {
                //p.target == null && msg.isAsynchronous()代表是否开启
                //同步屏障,后面分析
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                //循环整个Message链表,将此新增的消息插入到合适位置
                for (;;) {
                    prev = p;
                    p = p.next;
                    //当找到p == null代表已经到链表尾部,或者下一个msg的when
                    //大于当前新增消息的when
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //链表增加元素的常规操作,断开链next,将msg插入进去
                msg.next = p; 
                prev.next = msg;
            }
    
            //唤醒MessageQueue依靠native层
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    enqueueMessage方法其实就是一个链表的插入操作,规则是按照msg的执行时间排序,时间越短越靠近头部

    5 .Looper.loop

    public static void loop() {
        //获取当前线程关联的Looper对象
        final Looper me = myLooper();
        //looper为空则抛出异常
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        //获取Looper关联的MessageQueue
        final MessageQueue queue = me.mQueue;
        //死循环的获取MessageQueue的消息
        for (;;) {
            //获取消息的核心方法,
            Message msg = queue.next(); // might block
            //如果获取到的msg为空则说明MessageQueue已经退出,则Looper也需要
            //退出循环
            if (msg == null) {
    
                return;
            }
            //待queue.next()方法分析完之后再接着分析后面代码
            ......
                ......
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    6 MessageQueue.next

    Message next() {
        //mPtr保存native层MessageQueue的指针
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
    
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        //nextPollTimeoutMillis代表当前消息的执行时间,0代表立即执行,-1代表
        //此时MessageQueue没有消息
        int nextPollTimeoutMillis = 0;
        //无限循环的取出消息
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            //此方法是消息机制的核心,会在下一篇native层消息机制进行分析
            nativePollOnce(ptr, nextPollTimeoutMillis);
    
            synchronized (this) {
                //获取当前时间
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                //前面分析过,mMessages指向消息链表头部
                Message msg = mMessages;
                //如果msg不为空handler却为空说明此时Looper需要获取异步消息
                if (msg != null && msg.target == null) {
                    do {
                        //典型的链表循环
                        prevMsg = msg;
                        msg = msg.next;
                        //退出条件是找到设置了Asynchronous的消息或者
                        //直到链表尾部也没找到,说明此链表不含异步消息
                    } while (msg != null && !msg.isAsynchronous());
                }
                //msg不为空代表此消息链表中是有消息的
                if (msg != null) {
                    //还没到消息执行时间
                    if (now < msg.when) {
                        //则重置nextPollTimeoutMillis为等待执行的时间
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {//需要取出消息开始执行
                        //将mBlocked置为false代表MessageQueue不需要阻塞
                        mBlocked = false;
                        //prevMsg指向异步消息前一个消息,不为空代表有异步消息
                        if (prevMsg != null) {
                            //链表取出消息的常规操作
                            prevMsg.next = msg.next;
                        } else {
                            //如果没有异步消息,则取出消息队列头部的消息,并让
                            //mMessages继续指向下一个消息
                            mMessages = msg.next;
                        }
                        //断开取出消息的链
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);					
                        //标记此消息为正在使用
                        msg.markInUse();
                        //返回取出的消息
                        return msg;
                    }
                } else {
                    //消息队列中没有消息
                    nextPollTimeoutMillis = -1;
                }
    
                //如果消息队列退出
                if (mQuitting) {
                    dispose();
                    return null;
                }
    
                //省略的代码是关于pendingIdleHandler的处理,这个handler会在
                //MessageQueue为空时执行..不去细看了
                .....
                    pendingIdleHandlerCount = 0;
                nextPollTimeoutMillis = 0;
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    Handler消息机制中最重要的消息循环与获取的流程,做一个总结:

    1. Message在MessageQueue中以链表的形式存在,并且是按when执行时间从小到大排序,mMessages总是指向消息链表头部
    2. Loop会无限循环MessageQueue,有消息就取出,没有消息就阻塞(阻塞的核心实现在native层,我们下一篇进行分析)
    3. MessageQueue中有一种异步消息,它的优先级高于同步消息(后面分析)
      当消息队列为空时会执行pendingIdleHandler
    public static void loop() {
        ......
            for (;;) {
                Message msg = queue.next(); // might block
                if (msg == null) {
    
                    return;
                }
                //省略了很多log相关代码,主要帮助开发者实现监听UI线程卡顿
                ......
                    try {
                        //msg取到之后调用了msg的target也就是handler的dispatchMessage
                        //方法进行消息处理
                        msg.target.dispatchMessage(msg);
                    }
                .......
                    //msg的回收复用,前面已经讲过,此方法会将使用过的msg加入消息池
                    //进行复用
                    msg.recycleUnchecked();
            }
    }
    //handler.java
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    7 handler异步消息

    最后我们再来看看异步消息,在Android源码绘制流程中使用了异步消息,目的是尽可能快的完成View的绘制

    //ViewRootImpl.java
    void scheduleTraversals() {
        ......
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        .....
            mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ......
    }
    //Choreographer.java
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
    public void postCallbackDelayed(int callbackType,
                                    Runnable action, Object token, long delayMillis) {
        ......
            postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
    
    private void postCallbackDelayedInternal(int callbackType,
                                             Object action, Object token, long delayMillis) {
        ......
            synchronized (mLock) {
            .....
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    先调用postSyncBarrier开启同步屏障,屏蔽同步消息,接着设置setAsynchronous为true,将此消息设置为异步,则优先处理异步消息

    8 同步屏障原理

    我们从handler投递msg的分析知道,无论那种投递方式,只要是通过handler的sendXXX方法,最终一定会调用到enqueueMessage方法,

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
                                   long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
    
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此方法第一行代码要做的事就是msg.target = this,而从MessageQueue.next方法中,我们知道要优先处理异步消息有两个条件,一是**(msg != null && msg.target == null),然后才循环消息链表,找到msg.isAsynchronous()为true的msg**,

    Message next() {
    	......
    		for (;;) {
    				if (msg != null && msg.target == null) {
                        do {
                            prevMsg = msg;
                            msg = msg.next;
                        } while (msg != null && !msg.isAsynchronous());
                    }
    			}
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们可以得出结论,同步屏障就是向MessageQueue投递一个handler为空的msg,有了这个msg,从此msg开始,之后的所有同步消息都会被屏蔽,只会处理isAsynchronous为true的异步消息

    //设置FLAG_ASYNCHRONOUS标示此msg为异步消息
    public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }
    
    //开启同步屏障:MessageQueue.java
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }
    
    private int postSyncBarrier(long when) {
    
        synchronized (this) {
            final int token = mNextBarrierToken++;
            //创建没有handler的msg
            final Message msg = Message.obtain();
            msg.markInUse();
            //时间是当前系统时间
            msg.when = when;
            msg.arg1 = token;
    
            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                //找到晚于当前时间的msg
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            //将新创建的handler为空的msg插入到prev后面
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                //说明msg应该插到消息头部
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    我们总结下使用异步消息,两个条件:

    1. 开启同步屏障即投递一个handler为空的msg
    2. 将需要发送的msg设置为异步即调用setAsynchronous(true)

    en != 0) {
    //找到晚于当前时间的msg
    while (p != null && p.when <= when) {
    prev = p;
    p = p.next;
    }
    }
    //将新创建的handler为空的msg插入到prev后面
    if (prev != null) { // invariant: p == prev.next
    msg.next = p;
    prev.next = msg;
    } else {
    //说明msg应该插到消息头部
    msg.next = p;
    mMessages = msg;
    }
    return token;
    }
    }

    
    我们总结下使用异步消息,两个条件:
    
    1. 开启同步屏障即投递一个handler为空的msg
    2. 将需要发送的msg设置为异步即调用setAsynchronous(true)
    
    同步屏障只能屏蔽消息链表中添加空handler的msg之后的同步消息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    node 第十九天 使用node插件node-jsonwebtoken实现身份令牌jwt认证
    web前端面试题附答案006-主流浏览器都有哪些?内核是什么?知道内核又能干什么呢?
    js关于深度克隆问题
    在python中我对包的理解,希望对你有帮助
    python之Scrapy爬虫案例:豆瓣
    后端——egg.js是什么、egg.js安装、约定规则、路由Router、控制器Controller、跨域
    C/C++语言100题练习计划 80——好多好多符(二分查找实现)
    基于Java+SpringBoot+Vue宠物咖啡馆平台设计和实现
    QT系列教程(11) TextEdit实现Qt 文本高亮
    uniapp AES加密解密
  • 原文地址:https://blog.csdn.net/gangjindianzi/article/details/125532101