閱讀更多

1頂
0踩

編程語言

轉載新聞 深入理解Android消息機制

2018-04-20 11:18 by 副主編 jihong10102006 評論(0) 有52546人瀏覽
在日常的開發中,Android 的消息機制作為系統運行的根本機制之一,顯得十分的重要。
從 Handler 發送消息開始

查看源碼,Handler的post、send方法最終都會走到
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
	if (delayMillis < 0) {
	    delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

sendMessageDelayed 會走到
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
	msg.target = this;
    if (mAsynchronous) {
	    msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

這里可以設置 Message 為異步消息

查看 queue 的 enqueueMessage 方法, 我們剝離出核心代碼:
if (p == null || when == 0 || when < p.when) {
	// New head, wake up the event queue if blocked.
	msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
 }

如果是新的隊列頭,直接插入隊列

如果隊列里面已經有消息了,執行如下邏輯
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
	prev = p;
    p = p.next;
    if (p == null || when < p.when) {
	    break;
    }
    if (needWake && p.isAsynchronous()) {
	    needWake = false;
    }
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;

插入消息的時候,一般不會喚醒消息隊列。如果消息是異步的,并且隊列頭不是一個異步消息的時候,會喚醒消息隊列
if (needWake) {
	nativeWake(mPtr);
}

消息隊列的具體喚醒過程我們暫時不細看。把關注點移到 Looper 上。looper在執行的時候具體執行了什么邏輯呢?查看 Looper.java 的 looper() 方法

looper 方法中有一個死循環, 在死循環中,會獲取下一個 Message
for (;;) {
	Message msg = queue.next(); // might block
}

if (msg != null && msg.target == null) {
// Stalled by a barrier.  Find the next asynchronous message in the queue.
do {
	prevMsg = msg;
	msg = msg.next;
} while (msg != null && !msg.isAsynchronous());

當存在一個 barrier 消息的時候,會尋找隊列中下一個異步任務。而不是按照順序。 例如3個消息,1,2,3, 2 是異步消息。如果不存在barrier的時候,next的順序就是 1,2,3 但是如果存在barrier的時候,則是 2,1,3
if (msg != null) {
	if (now < msg.when) {
    // Next message is not ready.  Set a timeout to wake up when it is ready.
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
    } else {
	    // Got a message.
        mBlocked = false;
        if (prevMsg != null) {
	        prevMsg.next = msg.next;
        } else {
		    mMessages = msg.next;
        }
        msg.next = null;
        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
        msg.markInUse();
	    return msg;
   }
} else {
	// No more messages.
	nextPollTimeoutMillis = -1;
}

這里如果 next 的 Message 不為空,就返回,并且將它移出隊列 在 MessageQueue 為空的時候,會順便去處理一下 add 過的 IdleHandler, 處理一些不重要的消息
for (int i = 0; i < pendingIdleHandlerCount; i++) {
	final IdleHandler idler = mPendingIdleHandlers[i];
    mPendingIdleHandlers[i] = null; // release the reference to the handler

    boolean keep = false;
    try {
		keep = idler.queueIdle();
    } catch (Throwable t) {
	    Log.wtf(TAG, "IdleHandler threw exception", t);
    }

    if (!keep) {
		synchronized (this) {
        mIdleHandlers.remove(idler);
     }
}

查看 IdleHandler 的源碼。
   
 * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     */
    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }

當 queueIdle() 為 false 的時候,會將它從 mIdleHandlers 中 remove,仔細思考下,我們其實可以利用IdleHandler實現不少功能, 例如
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
	@Override
	public boolean queueIdle() {
		return false
	}
});

我們可以在 queueIdle 中,趁著沒有消息要處理,統計一下頁面的渲染時間(消息發送完了說明UI已經渲染完了),或者算一下屏幕是否長時間沒操作等等。

拿到 Message 對象后,會將 Message 分發到對應的 target 去
msg.target.dispatchMessage(msg);

查看源碼
public void dispatchMessage(Message msg) {
	if (msg.callback != null) {
		handleCallback(msg);
    } else {
	    if (mCallback != null) {
	        if (mCallback.handleMessage(msg)) {
	            return;
            }
         }
     handleMessage(msg);
	}
}

當 msg 的 callback 不為 null 的時候,即通過 post(Runnable) 發送信息的會執行 handlerCallback(msg) 方法。如果 mCallback 不為 null并且 handleMessage 的結果為 false,則執行 handleMessage 方法。否則會停止分發。
private static void handleCallback(Message message) {
	message.callback.run();
}

查看 handlerCallback 方法源碼, callback 會得到執行。到這里基本的Android消息機制就分析完了,簡而言之就是,Handler 不斷的將Message發送到一 根據時間進行排序的優先隊列里面,而線程中的 Looper 則不停的從MQ里面取出消息,分發到相應的目標Handler執行。

為什么主線程不卡?

分析完基本的消息機制,既然 Looper 的 looper 方法是一個for(;;;)循環,那么新的問題提出來了。為什么Android會在主線程使用死循環?執行死循環的時候為什么主線程的阻塞沒有導致CPU占用的暴增??

繼續分析在源碼中我們沒有分析的部分:
  • 消息隊列構造的時候是否調用了jni部分
  • nativeWake、nativePollOnce這些方法的作用是什么
先查看MQ的構造方法:
MessageQueue(boolean quitAllowed) {
	mQuitAllowed = quitAllowed;
	mPtr = nativeInit();
}

會發現消息隊列還是和native層有關系,繼續查看android/platform/frameworks/base/core/jni/android_os_MessageQueue_nativeInit.cpp中nativeInit的實現:
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }

    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

這里會發現我們初始化了一個 NativeMessageQueue ,查看這個消息隊列的構造函數
NativeMessageQueue::NativeMessageQueue() :
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        Looper::setForThread(mLooper);
    }
}

這里會發現在mq中初始化了 native 的 Looper 對象,查看android/platform/framework/native/libs/utils/Looper.cpp中 Looper 對象的構造函數
// 簡化后的代碼
Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {

	int wakeFds[2];
	int result = pipe(wakeFds);

	mWakeReadPipeFd = wakeFds[0];
	mWakeWritePipeFd = wakeFds[1];
	
	result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
	result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);

	mEpollFd = epoll_create(EPOLL_SIZE_HINT);

	struct epoll_event eventItem;
    memset(& eventItem, 0, sizeof(epoll_event)); 
    eventItem.events = EPOLLIN;
    eventItem.data.fd = mWakeReadPipeFd;
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
}

這里我們會發現,在 native 層創建了一個epoll,并且對 epoll 的 event 事件進行了監聽。

什么是epoll

在繼續分析源碼之前,我們先分析一下,什么是epoll

epoll是Linux中的一種IO多路復用方式,也叫做event-driver-IO。

Linux的select 多路復用IO通過一個select()調用來監視文件描述符的數組,然后輪詢這個數組。如果有IO事件,就進行處理。

select的一個缺點在于單個進程能夠監視的文件描述符的數量存在最大限制,select()所維護的存儲大量文件描述符的數據結構,隨著文件描述符數量的增大,其復制的開銷也線性增長。

epoll在select的基礎上(實際是在poll的基礎上)做了改進,epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可。

另一個本質的改進在于epoll采用基于事件的就緒通知方式(設置回調)。在select中,進程只有在調用一定的方法后,內核才對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來注冊一個文件描述符,一旦基于某個文件描述符就緒時,內核會采用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知

關于epoll和select,可以舉一個例子來表達意思。select的情況和班長告訴全班同學交作業類似,會挨個去詢問作業是否完成,如果沒有完成,班長會繼續詢問。

而epoll的情況則是班長詢問的時候只是統計了待交作業的人數,然后告訴同學作業完成的時候告訴把作業放在某處,然后喊一下他。然后班長每次都去這個地方收作業。

大致了解了epoll之后,我們繼續查看nativePollOnce方法,同理,會調用native Looper的pollOnce方法
while (mResponseIndex < mResponses.size()) {
            const Response& response = mResponses.itemAt(mResponseIndex++);
            int ident = response.request.ident;
            if (ident >= 0) {
                int fd = response.request.fd;
                int events = response.events;
                void* data = response.request.data;
                if (outFd != NULL) *outFd = fd;
                if (outEvents != NULL) *outEvents = events;
                if (outData != NULL) *outData = data;
                return ident;
            }
        }

在pollOnce中,會先處理沒有callback的response(ALOOPER_POLL_CALLBACK = -2),處理完后會執行pollInner方法
// 移除了部分細節處理和日志代碼
// 添加了分析源碼的日志
int Looper::pollInner(int timeoutMillis) {
	if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
	        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
	        int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
	        if (messageTimeoutMillis >= 0
	                && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
	            timeoutMillis = messageTimeoutMillis;
	        }
	  }

	// Poll.
    int result = ALOOPER_POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;

    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
	// 等待事件發生或者超時
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

    // Acquire lock.
    mLock.lock();


	// Check for poll error.
	// epoll 事件小于0, 發生錯誤
    if (eventCount < 0) {
        if (errno == EINTR) {
            goto Done;
        }
        result = ALOOPER_POLL_ERROR;
        goto Done;
    }

	if (eventCount == 0) {
		// epoll事件為0,超時,直接跳轉到Done
        result = ALOOPER_POLL_TIMEOUT;
        goto Done;
    }

	//循環遍歷,處理所有的事件
	for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeReadPipeFd) {
            if (epollEvents & EPOLLIN) {
                awoken();  //喚醒,讀取管道里面的事件
            } else {
            }
        } else {
            ssize_t requestIndex = mRequests.indexOfKey(fd);
            if (requestIndex >= 0) {
                int events = 0;         
                // 處理request,生成response對象,push到相應的Vector
                pushResponse(events, mRequests.valueAt(requestIndex));
            } else {               
            }
        }
    }

Done: ;

	// Invoke pending message callbacks.
	// 發生超時的邏輯處理
    mNextMessageUptime = LLONG_MAX;
    while (mMessageEnvelopes.size() != 0) {
	    // 處理Native端的Message
        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
        const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
        if (messageEnvelope.uptime <= now) {
            // Remove the envelope from the list.
            // We keep a strong reference to the handler until the call to handleMessage
            // finishes.  Then we drop it so that the handler can be deleted *before*
            // we reacquire our lock.
            { // obtain handler
                sp<MessageHandler> handler = messageEnvelope.handler;
                Message message = messageEnvelope.message;
                mMessageEnvelopes.removeAt(0);
                mSendingMessage = true;
                mLock.unlock();
                handler->handleMessage(message);   // 處理消息事件
            } // release handler

            mLock.lock();
            mSendingMessage = false;
            result = ALOOPER_POLL_CALLBACK;   // 設置回調
        } else {
            // The last message left at the head of the queue determines the next wakeup time.
            mNextMessageUptime = messageEnvelope.uptime;
            break;
        }
    }

    // Release lock.
    mLock.unlock();

    // Invoke all response callbacks.
    // 執行回調
    for (size_t i = 0; i < mResponses.size(); i++) {
        Response& response = mResponses.editItemAt(i);
        if (response.request.ident == ALOOPER_POLL_CALLBACK) {
            int fd = response.request.fd;
            int events = response.events;
            void* data = response.request.data;
            int callbackResult = response.request.callback->handleEvent(fd, events, data);
            if (callbackResult == 0) {
                removeFd(fd);  //移除fd
            }
            // Clear the callback reference in the response structure promptly because we
            // will not clear the response vector itself until the next poll.
            response.request.callback.clear();  // 清除reponse引用的回調方法
            result = ALOOPER_POLL_CALLBACK;  // 發生回調
        }
    }
    return result;
}

看到這里,我們其實可以看出來整體消息模型由 native 和 Java 2層組成,2層各自有自己的消息系統。 Java層通過調用 pollonce 來達到調用底層epoll 讓死循環進入阻塞休眠的狀態,以避免浪費CPU, 所以這也解釋了為什么Android Looper的死循環為什么不會讓主線程CPU占用率飆升。

java層和native層的對應圖如下:

備注
  • Java 層和 native 層通過 MessageQueue 里面持有一個 native 的MessageQueue 對象進行交互。WeakMessageHandler 繼承自MessageHandler,NativeMessageQueue 繼承自 MessageQueue
  • Java 層和 native 層實質是各自維護了一套相似的消息系統。C層發出的消息和Java層發出的消息可以沒有任何關系。所以 Framework 層只是很巧的利用了底層 epoll 的機制達到阻塞的目的。
  • 通過 pollOnce 的分析,可以發現消息的處理其實是有順序的,首先是處理native message,然后處理native request,最后才會執行java層,處理java層的message
可以在子線程中創建Handler嗎?為什么每個線程只會有一個Looper?

在很多時候,我們可以遇到這2個問題。既然看了 Handler 的源碼,那么,我們就順便分析一下這 2 個問題。

查看Handler的構造方法,無參構造方法最后會調用
public Handler(Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

可以看到,這里會直接獲取Looper
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

這里會把每個 Looper 存到相應的ThreadLocal對象中,如果子線程直接創建了Handler,Looper 就會是一個null,所以會直接跑出一個"Can't create handler inside thread that has not called Looper.prepare()"的RuntimeException

那么我們是何時把Looper放入ThreadLocal對象的呢?可以在Looper.prepare()中找到答案
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));
}

這也解釋了,在每個 Thread 中,只會存在一個 Looper 對象。如果我們想在子線程中正常創建 Handler,就需要提前運行當前線程的 Looper,調用
Looper.prepare()

就不會拋出異常了。

總結

消息機制作為 Android 的基礎,還是非常有深入了解的必要。對于我們遇到Handler發送消息的時候跑出的系統異常的排查也很有意義。

特別感謝

本次源碼的閱讀過程中,遇到了很多不了解的問題例如epoll,這里非常感謝IO哥(查看IO哥大佬)助和指導。讓我在某些細節問題上暫時繞過和恍然大悟。
  • 大小: 13.9 KB
來自: github
1
0
評論 共 0 條 請登錄后發表評論

發表評論

您還沒有登錄,請您登錄后再發表評論

相關推薦

  • 深入理解Android:卷2,中文完整掃描版

    深入理解Android:卷2》是“深入理解Android”系列的第2本,第1本書上市后獲得廣大讀者高度評價,在Android開發者社群內口口相傳。《深入理解Android:卷2》不僅繼承了第1本書的

  • 深入理解Android:卷II 鄧凡平著 PDF掃描版

    深入理解Android:卷II 圖書簡介: 《深入理解Android:卷2》是“深入理解Android”系列的第2本,第1本書上市后獲得廣大讀者高度評價,在Android開發者社群內口口相傳。本書不僅

  • 深入理解Android內核設計思想 - 林學森 - 人民郵電出版社

      《深入理解Android內核設計思想》適用于Android 4.3以上的版本。全書從操作系統的基礎知識入手,全面剖析進程/線程、內存管理、Binder機制、GUI顯示系統、多媒體管理、輸入系統等核

  • 深入理解Android:Telephony原理剖析與最佳實踐》迷你書

    深入理解Android:Telephony原理剖析與最佳實踐》是“深入理解Android”系列的第3本,本書從源代碼角度深入解析了Android Telephony的架構設計與實現原理,深刻揭示了A

  • android】手寫一套Java的Handler程序,深入理解Android消息機制

    發現無論是Windows還是Android,它們都是利用消息機制來運行一個程序,使得程序能夠持久的運行下去,那它們之間都有共同的特點,都屬于CS端 那么為了更好的深入理解android消息機制,我打算手寫一個Java版的Handler來模擬Android程序運行 1.首先我創建了一些空白類,其中Main類是我們程序的入口 2.接著我們開始手寫Looper 在Looper中我們知道有兩...

  • 深入理解android消息機制(五)——主線程handler和looper

    分析Handler的構造方法后,我們知道了如果不傳入Looper對象,那么將會使用本線程的Looper。這也就解釋了在非UI線程中需要首先調用Looper.prepare(),因為在那里創建了Looper對象并保存在了sThreadLocal中;那么問題來了: 在UI線程中初始化的Handler,它的Looper是在哪里賦值的? 主線程ActivityThread中main方法 public...

  • Android Looper和Handler

    Android中的Looper , Handler , Message Message:消息,其中包含了消息ID,消息處理對象以及處理的數據等,由MessageQueue統一列隊,終由Handler處理。 Handler:處理者,負責Message的發送及處理。使用Handler時,需要實現handleMessag

  • 從源碼一次徹底理解Android消息機制

    情景重現 button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { Thread.sleep(5 * 1000);

  • Android消息機制和應用

    本文主要講的是Android消息機制的Java層,Android消息機制Android開發者來說是一個基礎知識,網絡上介紹Android消息機制的文章很多,為了本文不顯得多余,我爭取從不同的角度來做一個解析,包括一些基礎和源碼分析。 我們知道Android消息機制主要指Handler、Mes...

  • 深入理解Android渲染機制

    基礎知識CPU: 中央處理器,它集成了運算,緩沖,控制等單元,包括繪圖功能.CPU將對象處理為多維圖形,紋理(Bitmaps、Drawables等都是一起打包到統一的紋理)。GPU:一個類似于CPU的專門用來處理Graphics的處理器, 作用用來幫助加快格柵化操作,當然,也有相應的緩存數據(例如緩存已經光柵化過的bitmap等)機制。OpenGL ES:是手持嵌入式設備的3DAPI,跨平臺的、功能

  • 推薦鄧凡平新作《深入理解Android:Java虛擬機ART》

    自定義View系列教程00–推翻自己和過往,重學自定義View 自定義View系列教程01–常用工具介紹 自定義View系列教程02–onMeasure源碼詳盡分析 自定義View系列教程03–onLayout源碼詳盡分析 自定義View系列教程04–Draw源碼分析及其實踐 自定義View系列教程05–示例分析 自定義View系列教程06–詳解View的Touch事件處理 自定義View系列教程

  • [深入理解Android卷一全文-第八章]深入理解Surface系統

    由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN博客中全文轉發這兩本書的全部內容。第8章 &nbsp;深入理解Surface系統本章主要內容·&nbsp; 詳細分析一個Activity的顯示過程。·&nbsp; 詳細分析Surface。·&nbsp; 詳細分析SurfaceFlinger。本章涉及的源代碼文

  • [深入理解Android卷一全文-第六章]深入理解Binder

    由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN博客中全文轉發這兩本書的全部內容。第6章 深入理解Binder本章主要內容·? 以MediaServer為切入點,對Binder的工作機制進行分析。·? 剖析ServiceManager的原理。 ·? 以MediaPlayerService為切入點對Clien

  • 深入理解Android 卷III》推薦序

    深入理解Android 卷III》即將發布,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framework中和UI相關的部分。在一個特別講究顏值的時代,本書分析了Android 4.2中WindowManagerService、ViewRoot、Input系統、StatusBar、Wallpaper等重要“顏值繪制/處理”模塊推薦序回顧

  • [深入理解Android卷一全文-第四章]深入理解zygote

    由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN博客中全文轉發這兩本書的全部內容。第4章 ?深入理解Zygote本章主要內容·? 深入分析zygote,并介紹system_server進程的初始化工作。本章涉及的源代碼文件名及位置下面是我們本章分析的源碼文件名及其位置。·? App_main.cppfram

  • 深入理解Android IPC機制之Binder機制

    Binder是Android系統進程間通信(IPC)方式之一。Linux已經擁有的進程間通信IPC手段包括(Internet Process Connection): 管道(Pipe)、信號(Signal)和跟蹤(Trace)、插口(Socket)、報文隊列(Message)、共享內存(Share Memory)和信號量(Semaphore)。本文詳...

  • Android消息機制-深入理解消息隊列的工作模式

    前言 隊列:隊列是一種簡單的先進先出的數據結構。 在代碼中, 對隊列的操作應該加入同步機制,因為隊列可以接受多個線程同時向隊列發送消息Android消息機制中的成員 這里可以用一句話描述消息機制設計到的成員:消息發送者發送消息給隊列傳遞到消息處理。 1.消息承載體(Message) 這個不用解釋了,就是裝消息的包。值得注意的是,Android消息機制中的消息傾向適合數據量比較小的場合

  • 深入理解Android

    術與道_移動應用UI設計必修課余振華 歡迎使用Markdown編輯器 這是一本系統介紹移動設計知識的書籍, 全書分為上下兩篇。 上篇為術之篇,主要講解了移動應用設計相關的基礎知識。通過基礎知識的學習,使大家了解手機屏幕顯示的原理及各種手機硬件的傳感器使用方法。通過講解基本的設計規范及圖標設計方法,使讀者初步掌握移動應用設計知識。 下篇為道之篇,是進階應用設計知識,詳細講解了從如何組建設計規范,到各...

  • Android之handler異步消息處理機制完全解析

    開始進入正題,我們都知道,Android UI是線程不安全的,如果在子線程中嘗試進行UI操作,程序就有可能會崩潰。相信大家在日常的工作當中都會經常遇到這個問題,解決的方案應該也是早已爛熟于心,即創建一個Message對象,然后借助Handler發送出去,之后在Handler的handleMessage()方法中獲得剛才發送的Message對象,然后在這里進行UI操作就不會再出現崩潰了。 這種處理...

  • MySQL基礎入門視頻課程

    本課程從零開始,以通俗易懂的方式講解MySQL技術,手把手教你掌握每一個知識點。課程中使用的所有英文單詞都會逐一查詢并記錄,真正做到零基礎入門學習,適合初學者的教程! 課程內容包括: 1.MySQL簡介、安裝MySQL 2.查詢操作 3.聚合函數和分組統計 4.更新操作 5.表和庫的管理 6.約束 7.用戶和權限管理 8.事務處理 教學全程采用筆記+代碼案例的形式講解,通俗易懂!!!

Global site tag (gtag.js) - Google Analytics 开心农场种蔬菜赚钱 广东快乐10分钟开奖历史号码 诸葛亮一波中特 陕西11选五玩法技巧 股票配资开户流程 内蒙古11选5遗漏号查 山西十一选五走势图一定牛 江西多乐彩11选五即时走势图 北京快三开奖号码查询 好彩1精准命中 #NAME? 山西省十一选五走势图 多乐彩走势图 澳门五分彩是合法的吗? 炒股k线图解 快乐十分任三稳赚技巧 幸运28在线预测尽享网