年后面了六家大厂,每家都会问的一个问题就是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. zbxtable的使用

    实验环境: zabbix server 172.16.1.121 mysql 172.16.1.121 访问端 172.16.1.122 54.1 zbxtable 1 说明 ZbxTable使用Go ...

  2. 2、oracle用户和权限

    权限主要可以分成三类:系统权限.角色.对象权限,角色是一类系统权限的分组, Oracle 的角色存放在表 dba_roles 中,某角色包含的系统权限存放在 dba_sys_privs 中, 包含的对 ...

  3. Linux + .net core 开发升讯威在线客服系统:同时支持 SQL Server 和 MySQL 的实现方法

    前段时间我发表了一系列文章,开始介绍基于 .net core 的在线客服系统开发过程. 有很多朋友一直提出希望能够支持 MySQL 数据库,考虑到已经有朋友在用 SQL Server,我在升级的过程中 ...

  4. yoyogo v1.7.4 发布,支持 grpc v1.3.8 & etcd 3.5.0

    YoyoGo (Go语言框架)一个简单.轻量.快速.基于依赖注入的微服务框架( web .grpc ),支持Nacos/Consoul/Etcd/Eureka/k8s /Apollo等 . https ...

  5. gRPC四种模式、认证和授权实战演示,必赞~~~

    前言 上一篇对gRPC进行简单介绍,并通过示例体验了一下开发过程.接下来说说实际开发常用功能,如:gRPC的四种模式.gRPC集成JWT做认证和授权等. 正文 1. gRPC四种模式服务 以下案例演示 ...

  6. Message /index.jsp (line: [17], column: [45]) The JSP specification requires that an attribute name is preceded by whitespace

    Error: Message /index.jsp (line: [17], column: [45]) The JSP specification requires that an attribut ...

  7. python使用笔记002

    一.字符串常用方法 1 s = ' ha.hahaha.' 2 print(s.count('a'))#找某一个元素在字符串里出现的次数 3 print(s.index('a'))#找某一个元素的下标 ...

  8. 华为:harmonyos 鸿蒙

    鸿蒙 1.设置--更新 2.华为搜索--抢鲜体验-下载描述文件--同意 3.更新-安装

  9. 微信小程序云开发-云函数-数据库和云函数获取数据的区别

    一.数据库获取数据 1.1 数据库获取数据的写法 在本地创建的页面js文件中写代码 1.2 数据库获取数据返回数据限制20条 数据库获取数据,每次返回20条数据(数据库有108条数据) 1.3 数据库 ...

  10. Docker编排利器DockerCompose

    Docker 编排利器 DockerCompose,编排之后可以一次性通过一条命令启动一组服务 例如一条命令启动 SpringBoot 项目,同时启动 SpringBoot 项目依赖的其他中间件(My ...