年后面了六家大厂,每家都会问的一个问题就是Android的消息机制!可见Android的消息机制是多么重要!

消息机制之所以这么重要是因为Android应用程序是通过消息来驱动的,Android某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关,并且消息处理机制在整个Android知识体系中也是尤其重要,在太多的源码分析的文章讲得比较繁琐,很多人对整个消息处理机制依然是懵懵懂懂。

这篇文章通过一些问答的模式结合Android主线程(UI线程)的工作原理来讲解,源码注释很全,还有结合流程图,如果你对Android 消息处理机制还不是很理解,我相信只要你静下心来耐心的看,肯定会有不少的收获的。

目录

  1. Android消息机制流程
  2. Handler
  3. Message
  4. MessageQueue
  5. Looper
  6. HandleThread
  7. 文末福利

篇外话

最近想把自己比较薄弱的Java&Android基础抽时间进行学习加强些,这也更符合自己的内心追求和自我期待。并行的开始另外一段学习旅程,从Handler消息机制开启,结合消息机制的流程以及源码进行学习分析。

一、Android消息机制流程

我们先通过下面两张图来对Android消息机制流程以及关键类之间的关系有个了解,后面我们再结合源码一一进行分析。

消息机制的流程

Handler、Message、MessageQueue、Looper之间的关系

二、Handler

Handler有两个主要的用途:

  1. 调度消息在某个时间点执行;
  2. 不同线程之间通信

2.1 全局变量

 final Looper mLooper;     //有Looper的引用
final MessageQueue mQueue;//有MessageQueue的引用
final Callback mCallback;
final boolean mAsynchronous;
IMessenger mMessenger;

2.2 构造方法

    public Handler() {
this(null, false);
}
public Handler(@NonNull Looper looper) {
this(looper, null, false);
} public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

2.3 获取Message

//从Message复用池中获取一个Message
public final Message obtainMessage()
{
return Message.obtain(this);
} //和上面的方法基本一致,差异在于从复用池中获取到Message后给what赋值
public final Message obtainMessage(int what)
{
return Message.obtain(this, what);
}
//...其他obtainMessage类似

2.4 发送消息

下面我们挑几个发送方法来看下

sendMessage: 发送一个Message,when为当前的时间

MessageQueue根据when进行匹配插入位置

    public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
} public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
} public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
......
return enqueueMessage(queue, msg, uptimeMillis);
}

post:从消息复用池中获取Message,设置Message的Callback

public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
} private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

postAtFrontOfQueue(): 将消息插入到队列头部

通过调用sendMessageAtFrontOfQueue 加入一个when为0的message到队列,即插入到队列的头部,需要注意的是 MessageQueue#enqueueMessage的插入到链表中时是根据when比较的(when < p.when),如果之前已经有多个when等于0的消息在队列中,这个新的会加入到前面when也为0的后面。

    public final boolean postAtFrontOfQueue(Runnable r)
{
return sendMessageAtFrontOfQueue(getPostMessage(r));
} public final boolean sendMessageAtFrontOfQueue(Message msg) {
MessageQueue queue = mQueue;
......
//第三个参数为0,即Message的when为0,插入到队列的头部,注意到MessageQueue#enqueueMessage的插入到链表中时是根据when比较的(when < p.when),如果之前已经有多个when等于0的消息在队列中,这个新的会加入到前面when也为0的后面。
return enqueueMessage(queue, msg, 0);
}

2.5 派发消息 dispatchMessage

优先级如下:

Message的回调方法callback.run() >

Handler的回调方法mCallback.handleMessage(msg) > Handler的默认方法handleMessage(msg)

public void dispatchMessage(@NonNull Message msg) {
//Message的回调方法,优先级最高
if (msg.callback != null) {
handleCallback(msg);
} else {
//Handler的mCallBack优先级次之
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
//Handler的handleMessage方法优先级最低(大部分都是在该方法中实现Message的处理)
handleMessage(msg);
}
}

三、Message

全局变量

//一些重要的变量

    public int arg1;
public int arg2;
public Object obj;
public long when;
Bundle data;
Handler target; //Message中有个Handler的引用
Runnable callback; //Message有next指针,可以组成单向链表
Message next; public static final Object sPoolSync = new Object(); //复用池中的第一个Message
private static Message sPool; //复用池的大小,默认最大50个(如果短时间内有超过复用池最大数量的Message会怎样,重新new)
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;

构造方法

查看下是否有可以复用的message,如果有,复用池的中可复用的Message个数减一,返回该Message;如果没有重新new一个。注意复用池默认最大数量为50。

   public static Message obtain() {
synchronized (sPoolSync) {
//查看下是否有可以复用的message
if (sPool != null) {
//取出第一个Message
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
//复用池的中可复用的Message个数减一
sPoolSize--;
return m;
}
}
//如果复用池中没有Message了重新new
return new Message();
}

recycleUnchecked

//标记一个Message时异步消息,正常的情况都是同步的Message,当遇到同步屏障的时候,优先执行第一个异步消息。关于同步屏障,我们在MessageQueue中在结合next等方法再介绍。

public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
} void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
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) {
//可以复用的message为50个,如果超过了就不会再复用
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}

//toString和dumpDebug可以Dump出message信息,遇到一些问题时可以帮助分析

android.os.Message#toString(long)

android.os.Message#dumpDebug

四、MessageQueue

MessageQueue是一个单链表优先队列

Message不能直接添加到MessageQueue中,要通过Handler以及相对应的Looper进行添加。

变量

//MessageQueue链表中的第一个Message
Message mMessages;

next:从消息队列中取出下一条要执行的消息

如果是同步屏障消息,找到第一个队列中中第一个异步消息

如果第一个Message的执行时间比当前时间见还要晚,记录还要多久开始执行;否则就找到下一条要执行的Message。

后面的Looper的loop方法会从过queue.next调用该方法,获取需要执行的下一个Message,其中会调用到阻塞的native方法nativePollOnce,该方法用于“等待”, 直到下一条消息可用为止. 如果在此调用期间花费的时间很长, 表明对应线程没有实际工作要做,不会因此会出现ANR,ANR和这个没有半毛钱关系。

关键代码如下:

Message next() {

        //native层MessageQueue的指针
final long ptr = mPtr;
if (ptr == 0) {
return null;
} ......
for (;;) { //阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒
//nativePollOnce用于“等待”, 直到下一条消息可用为止. 如果在此调用期间花费的时间很长, 表明对应线程没有实际工作要做,不会因此会出现ANR,ANR和这个没有半毛钱关系。
nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { final long now = SystemClock.uptimeMillis();
Message prevMsg = null; //创建一个新的Message指向 当前消息队列的头
Message msg = mMessages; //如果是同步屏障消息,找到第一个队列中中第一个异步消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
} if (msg != null) {
//如果第一个Message的执行时间比当前时间见还要晚,记录还要多久开始执行
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//否则从链表中取出当前的Message ,并且把链表中next指向指向下一个Message mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
//取出当前的Message的值,next置为空
msg.next = null;
msg.markInUse();
return msg;
}
}
..... //android.os.MessageQueue#quit时mQuitting为true
//如果需要退出,立即执行并返回一个null的Message,android.os.Looper.loop收到一个null的message后退出Looper循环
if (mQuitting) {
dispose();
return null;
}
......
if (pendingIdleHandlerCount <= 0) {
// 注意这里,如果没有消息需要执行,mBlocked标记为true,在enqueueMessage会根据该标记判断是否调用nativeWake唤醒
mBlocked = true;
continue;
}
......
}
......
}

enqueueMessage:向消息队列中插入一条Message

如果消息链表为空,或者插入的Message比消息链表第一个消息要执行的更早,直接插入到头部

否则在链表中找到合适位置插入,通常情况下不需要唤醒事件队列,以下两个情况除外:

  1. 消息链表中只有刚插入的这一个Message,并且mBlocked为true即,正在阻塞状态,收到一个消息后也进入唤醒
  2. 链表的头是一个同步屏障,并且该条消息是第一条异步消息

唤醒谁?MessageQueue.next中被阻塞的nativePollOnce

具体实现如下,

关于如何找到合适的位置?这涉及到链表的插入算法:引入一个prev变量,该变量指向p也message(如果是for循环的内部第一次执行),然后把p进行向next移动,和需要插入的Message进行比较when

关键代码如下:

 boolean enqueueMessage(Message msg, long when) {
...... synchronized (this) { msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake; //如果消息链表为空,或者插入的Message比消息链表第一个消息要执行的更早,直接插入到头部
if (p == null || when == 0 || when < p.when) { msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else { //否则在链表中找到合适位置插入
//通常情况下不需要唤醒事件队列,除非链表的头是一个同步屏障,并且该条消息是第一条异步消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
//具体实现如下,这个画张图来说明
//链表引入一个prev变量,该变量指向p也message(如果是for循环的内部第一次执行),然后把p进行向next移动,和需要插入的Message进行比较when
Message prev; for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
} //如果插入的是异步消息,并且消息链表第一条消息是同步屏障消息。
//或者消息链表中只有刚插入的这一个Message,并且mBlocked为true即,正在阻塞状态,收到一个消息后也进入唤醒
唤醒谁?MessageQueue.next中被阻塞的nativePollOnce
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}

简单着看下native的epoll (这块还没有深入分析,后面篇章补上吧)

nativePollOnce 和 nativeWake 利用 epoll 系统调用, 该系统调用可以监视文件描述符中的 IO 事件. nativePollOnce 在某个文件描述符上调用 epoll_wait, 而 nativeWake 写入一个 IO 操作到描述符

epoll属于IO复用模式调用,调用epoll_wait等待. 然后 内核从等待状态中取出 epoll 等待线程, 并且该线程继续处理新消息

removeMessages: 移除消息链表中对应的消息

需要注意的是,在该函数的实现中分为了头部meg的移除,和非头部的msg的移除。

移除消息链表中头部的和需要移除相同的msg

eg:msg1.what=0;msg2.what=0;msg3.what=0; msg4.what=1; 需要移除what为0的msg,即移除前三个

移除消息链表中非头部的对应的消息,eg:msg1.what=1;msg2.what=0;msg3.what=0; 需要移除what为0的消息,即移除后续的消息,处处体现链表的查询和移除算法

关键代码如下:

void removeMessages(Handler h, int what, Object object) {
......
synchronized (this) {
Message p = mMessages; //移除消息链表中头部的和需要移除相同的msg eg:msg1.what=0;msg2.what=0;msg3.what=0; msg4.what=1; 需要移除what为0的msg,即移除前三个
while (p != null && p.target == h && p.what == what
&& (object == null || p.obj == object)) {
Message n = p.next;
mMessages = n;
p.recycleUnchecked();
p = n;
} //移除消息链表中非头部的对应的消息,eg:msg1.what=1;msg2.what=0;msg3.what=0; 需要移除what为0的消息,即移除后续的消息,处处体现链表的查询和移除算法
while (p != null) {
Message n = p.next;
if (n != null) {
if (n.target == h && n.what == what
&& (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}
}
p = n;
}
}
}

postSyncBarrier:发送同步屏障消息

同步屏障也是一个message,只不过这个Message的target为null,. 通过ViewRootImpl#scheduleTraversals()发送同步屏障消息

同步屏障消息的插入位置并不是都是消息链表的头部,而是根据when等信息而定:如果when不为0,消息链表也不空,在消息链表中找到同步屏障要插入入的位置;如果prev为空,该条同步消息插入到队列的头部。

关键代码如下:

 /**
* android.view.ViewRootImpl#scheduleTraversals()发送同步屏障消息
* @param when
* @return
*/
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++; //同步屏障也是一个message,只不过这个Message的target为null final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token; Message prev = null;
Message p = mMessages;
if (when != 0) {
//如果when不为0,消息链表也不空,在消息链表中找到同步屏障要插入入的位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
//如果prev为空,该条同步消息插入到队列的头部
msg.next = p;
mMessages = msg;
}
return token;
}
}

dump: MessageQueue信息

有时候我们需要dump出当前looper的Message信息来分析一些问题,比不,是否Queue中有很多消息,如果太多就影响队列中后面的Message的执行,可能造成逻辑处理比较慢,甚至可能导致ANR等情况,MessageQueue的默认复用池是50个,如果太多排队的Message也会影响性能。通过dump Message信息可以帮助分析。mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);

 void dump(Printer pw, String prefix, Handler h) {
synchronized (this) {
long now = SystemClock.uptimeMillis();
int n = 0;
for (Message msg = mMessages; msg != null; msg = msg.next) {
if (h == null || h == msg.target) {
pw.println(prefix + "Message " + n + ": " + msg.toString(now));
}
n++;
}
pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
+ ", quitting=" + mQuitting + ")");
}
}

五、Looper

Looper主要涉及到构造、prepare和loop几个重要的方法,在保证一个线程有且只有一个Looper的设计上,采用了ThreadLocal以及代码逻辑的控制。

变量

//一些重要的变量
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); final MessageQueue mQueue; final Thread mThread;

构造方法

在构造Looper的时候 创建和Looper一一对应的MessageQueue

    private Looper(boolean quitAllowed) {
//在构造Looper的时候 new一一对应的MessageQueue
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

prepare

我们这里可以看到消息机制是 如何保证一个线程只有一个Looper。

//quitAllowed参数是否允许quit,UI线程的Looper不允许退出,其他的允许退出
private static void prepare(boolean quitAllowed) {
//保证一个线程只能有一个Looper,这里的sThreadLocal
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

loop

我们在MessageQueue的next方法已经分析过nativePollOnce这个方法可能会阻塞,直到拿到message。

如果next返回一个null的Message退出Looper循环,否则进行msg的派发。

取出的msg执行完之后,会加入到回收池中等待复用。recycleUnchecked我们在Message中也已经分析过了。不清楚的可以再回看。

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
……
for (;;) {
//next方法是一个会阻塞的方法,MessageQueue的next方法前面我们已经分析过nativePollOnce这个方法会可能阻塞,直到拿到message。
Message msg = queue.next();
//收到为空的msg,Loop循环退出。那么何时会收到为空的msg呐? quit
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
//msg的派发,msg.target就是Handler,即调用Handler的dispatchMessage派发消息
msg.target.dispatchMessage(msg); ……
//msg回收
msg.recycleUnchecked();
}

六、HandleThread

HandlerThread是一个带有Looper的Thread。

全局变量

public class HandlerThread extends Thread {
int mPriority;//线程优先级
int mTid = -1;//线程id
Looper mLooper;
private Handler mHandler;
......
}

构造方法

    public HandlerThread(String name) {
super(name);
//用于run时设置线程的优先级Process.setThreadPriority(mPriority); mPriority = Process.THREAD_PRIORITY_DEFAULT;
} public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}

run方法

进行Looper的prepare和loop的调用,配置好Looper环境

    @Override
public void run() {
//线程id
mTid = Process.myTid();
//调用Looper的prepare方法,把当前该线程关联的唯一的Looper加入到sThreadLocal中
Looper.prepare(); synchronized (this) {
//从sThreadLocal中获取Looper
mLooper = Looper.myLooper(); notifyAll();
}
//设置线程的优先级,默认THREAD_PRIORITY_DEFAULT,如果是后台业务可以配置为THREAD_PRIORITY_BACKGROUND,根据具体场景进行设置
Process.setThreadPriority(mPriority);
//可以做一些预设置的操作
onLooperPrepared();
//开始looper循环
Looper.loop();
mTid = -1;
}

使用HandlerThread的一般流程如下

// Step 1: 创建并启动HandlerThread线程,内部包含Looper
HandlerThread handlerThread = new HandlerThread("xxx");
handlerThread.start(); // Step 2: 创建Handler
Handler handler = new Handler(handlerThread.getLooper()); handler.sendMessage(msg);

这样有一个弊端,就是每次使用Handler都要new HandlerThread,而Thread又是比较占用内存,

能不能减少Thread的创建,或者说是Thread的复用.

并且实现Message能够得到及时执行,不被队列中前面的Message阻塞;

这的确是一个有很有意思很有挑战的事情。

七、资料

最后的最后,如果你打算开始读源码了,可以先看看我整理的这份资料。

《Android Framework精编内核解析》

本笔记讲解了Framework的主要模块,从环境的部署到技术的应用,再到项目实战,让我们不仅是学习框架技术的使用,而且可以学习到使用架构如何解决实际的问题,由浅入深,详细解析Framework,让你简单高效学完这块知识!

第一章:深入解析Binder

Binder机制作为进程间通信的一种手段,基本上贯穿了andorid框架层的全部。所以首先必须要搞懂的Android Binder的基本通信机制。

本章知识点

  • Binder 系列—开篇
  • Binder Driver 初探
  • Binder Driver 再探
  • Binder 启动 ServiceManager
  • 获取 ServiceManager
  • 注册服务(addService)
  • 获取服务(getService)
  • Framework 层分析
  • 如何使用 Binder
  • 如何使用 AIDL
  • Binder 总结
  • Binder 面试题全解析

第二章:深入解析Handler

本章先宏观理论分析与 Message 源码分析,再到MessageQueue 的源码分析,Looper 的源码分析,handler 的源码分析,Handler 机制实现原理总结。最后还整理Handler 所有面试题大全解析。

Handler这章内容很长,但思路是循序渐进的,如果你能坚持读完我相信肯定不会让你失望。

第三章:Dalvik VM 进程系统

Andorid系统启动、init 进程、Zygote、SystemServer启动流程、 应用程序的创建使用,Activity的创建、销毁 Handler和Looper。

第四章 深入解析 WMS

窗口管理框架 系统动画框架 View的工作原理。

第五章 PackagerManagerService

包管理服务,资源管理相关类。

由于篇幅限制,展示了部分内容截图,需要这些文档资料的,可以点赞支持一下我,然后【点击这里】免费阅读下载哦。

Android面试6家一线大厂,这个问题是必问!的更多相关文章

  1. 2020年最新阿里、字节、腾讯、京东等一线大厂高频面试(Android岗)真题合集,面试轻松无压力

    本文涵盖了阿里巴巴.腾讯.字节跳动.京东.华为等大厂的Android面试真题,不管你是要面试大厂还是普通的互联网公司,这些面试题对你肯定是有帮助的,毕竟大厂一定是行发展的标杆,很多公司的面试官同样会研 ...

  2. 2020年Android开发年终总结之如何挤进一线大厂?

    前言 年底总是一个充满回顾与展望的日子,在2020这场哀鸿遍野的"寒冬"里尤为明显. 其实不管是公司.集体还是个人,都需要在这个时候找个机会停下来,思考一下这一年来的收获与成长.失 ...

  3. 2018最新大厂Android面试真题

    前言 又到了金三银四的面试季,自己也不得不参与到这场战役中来,其实是从去年底就开始看,android的好机会确实不太多,但也还好,3年+的android开发经历还是有一些面试机会的,不过确实不像几年前 ...

  4. 一周内被程序员疯转3.2W次,最终被大厂封杀的《字节跳动Android面试手册》!

    一眨眼又到金三银四了,不知道各位有没有做好跳槽涨薪的准备了呢? 今天的话大家分享一份最新的<字节跳动Android面试手册>,内容包含Android基础+进阶,Java基础+进阶,数据结构 ...

  5. 【Android面试揭秘】面试官说“回去等通知”,我到底会不会等来通知?

    前言 大部分情况下,面试结束后,面试官都会跟你说:我们会在1-2个工作日内通知你面试结果. 许多人认为:所谓「等通知」其实是面试官委婉地给你「发拒信」.但是,这不是「等通知」的全部真相. 这篇文章,我 ...

  6. Android面试总结 (转)

    1. 下列哪些语句关于内存回收的说明是正确的? (b) A. 程序员必须创建一个线程来释放内存 B. 内存回收程序负责释放无用内存 C. 内存回收程序允许程序员直接释放内存 D. 内存回收程序可以在指 ...

  7. Android面试题目及其答案

    转自:http://blog.csdn.net/wwj_748/article/details/8868640 Android面试题目及其答案 1.Android dvm的进程和Linux的进程, 应 ...

  8. Android面试题目2

    1. 请描述下Activity的声明周期. onCreate->onStart->onRemuse->onPause->onStop->onRestart->onD ...

  9. Android面试经历2018

    本人14年7月份出来参加工作,至今工作将近4年的时间了,坐标是深圳.由于在目前的公司,感觉没什么成长,就想换一个公司.楼主已经在从实习到现在,已经换了三家公司了,所以这次出来的目标的100人以上,B轮 ...

随机推荐

  1. k8s1.20环境搭建部署(二进制版本)

    1.前提知识 1.1 生产环境部署K8s集群的两种方式 kubeadm Kubeadm是一个K8s部署工具,提供kubeadm init和kubeadm join,用于快速部署Kubernetes集群 ...

  2. Unity3D学习笔记2——绘制一个带纹理的面

    目录 1. 概述 2. 详论 2.1. 网格(Mesh) 2.1.1. 顶点 2.1.2. 顶点索引 2.2. 材质(Material) 2.2.1. 创建材质 2.2.2. 使用材质 2.3. 光照 ...

  3. Jenkins+SonarQube实现C#代码质量检查

    环境准备 SonarQube 项目创建 jenkins Windows构建节点配置 安装与SonarQube服务端相同版本jdk 安装sonar-scanner 并配置环境变量 安装Visual St ...

  4. linux 查看目录大小

    查看当前目录下各个目录大小容量 du -sh * du -sh /app/* du -h --max-depth=1 .

  5. 36、网卡绑定bond

    注意:虚拟机需要网卡模式为同一模式,否则无法进行通信: 36.1.mode0(平衡负载模式): 平时两块网卡均工作,且自动备援,但需要在与服务器本地网卡相连的交换机设备上进行端口聚合来支持绑定技术. ...

  6. ps 快速替换背景颜色

    1.打开图片: 点击工具栏上的"选择"--色彩范围--按[delete]

  7. AcWing 1290. 越狱

    监狱有连续编号为1~n的n个房间,每个房间关押一个犯人.有 M种宗教,每个犯人可能信仰其中一种.如果相邻房间的犯人信仰的宗教相同,就可能发生越狱.求有多少种状态可能发生越狱. #include< ...

  8. CentOS-查找删除历史文件

    背景:因服务器磁盘空间有限,根据实际情况控制保留指定的几天内的历史文件 find参数说明: /home/tmp        设置查找的目录 -mtime +30       设置修改时间为30天前 ...

  9. Hadoop:什么是Hadoop??

    官方讲解: Apache Hadoop 为可靠的,可扩展的分布式计算开发开源软件.Apache Hadoop软件库是一个框架,它允许使用简单的编程模型跨计算机群集分布式处理大型数据集(海量的数据). ...

  10. 资源:Maven仓库地址路径

    Maven下载路径 https://archive.apache.org/dist/maven/maven-3/ 查找需要引入的包路径时,可以在maven仓库进行查找 maven仓库地址:https: ...