主頁 > 移動端開發 > 我所知道的Handler

我所知道的Handler

2023-05-18 09:03:21 移動端開發

簡單講,handler就是兩個功能

插入訊息,enqueuemessage,msg,when
從訊息佇列中遍歷所有訊息,比對msg.when和當前的when,找到合適的位置插入

處理訊息,looper.loop會從messagequeue中呼叫next,取訊息,如果訊息還沒到時間該執行,就會比對時間,下次輪詢就通過binder寫入,native函式休眠,到時間喚醒執行,

handler記憶體泄漏

GCRoot 一般是靜態變數或者常量可以作為GCROOT
GCROOT 是ThreadLocal,存在于Looper中,Looper被加載就存在,
handler持有activity或者fragment,handler又被message持有,message的target屬性,message被messagequeue持有,messagequeue被looper中的threadlocal持有

java中匿名內部類會默認持有外部類的參考

打斷持有鏈

  1. handler.removemesage handler.removecallbacks
  2. handler使用static修飾,

主執行緒的Looper不允許退出

處理訊息,looper取出來后,呼叫message.tager.dispatchemesage后面呼叫了handler的handlemessage方法,

還有個callback物件,如果有callback,dispatch會先執行callback的處理,calllback回傳true,后面就不處理了,callback回傳false就給handler的handlemessage處理了

Meesage物件創建

message創建用的obtain,池化,頻繁的創建銷毀會導致記憶體不穩定,抖動,造成卡頓 oom等問題

message pool的最大快取50

阻塞和休眠,阻塞是被動的,休眠是主動的,阻塞不會讓出cpu,休眠會,thread.yield會讓出cpu,

子執行緒主執行緒通信

handler,livedata,eventbus,flow,rxjava,broadcast,觀察者模式不能跨執行緒

最終都是handler完成的,

Handler監聽卡頓

原理是在looper內完成的,looper處理訊息的時候,會列印內容,就是Printer,looper可以設定它,

Message訊息的分類

同步訊息,普通的訊息都是同步訊息
異步訊息,創建handler的時候設定async為true即可
同步屏障,需要通過反射呼叫,app層無法直接呼叫,是messagequeue提供的posSyncBarrier方法實作的,回傳一個token,時msg的arg1值,用它來取消同步屏障,和普通訊息的區別是,msg,target屬性為null,

重繪UI的訊息是異步訊息,發送前先插入了一個同步屏障訊息,異步訊息處理完成后,要將同步屏障訊息移除佇列

訊息入隊

handler訊息加入佇列,有一系列方法,如下:

// 發送空訊息
public final boolean sendEmptyMessage(int what){}
public final boolean sendEmptyMessageDelayed(int what,long delay){}
public final boolean sendEmptyMessageAtTime(int what,long when){}
// 發送訊息
public final boolean sendMessage(@NonNull Message msg){}
public final boolean sendMessageDelayed(@NonNull Message msg,long time){}
public final boolean sendMessageAtTime(@NonNull Message msg,long when){}

public final boolean sendMessageAtFrontOfQueue(Message msg) {}
// post發送
public final boolean post(@NonNull Runnable r) {}
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {}
public final boolean postAtTime(  
@NonNull Runnable r, @Nullable Object token, long uptimeMillis) {}
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {}
public final boolean postDelayed(Runnable r, int what, long delayMillis) {}
public final boolean postDelayed(  
        @NonNull Runnable r, @Nullable Object token, long delayMillis) {}
public final boolean postAtFrontOfQueue(@NonNull Runnable r) {}

// enqueue
public final boolean executeOrSendMessage(@NonNull Message msg) {}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,  
        long uptimeMillis) {}

最終都是掉用的enqueueMessage加入佇列

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,  
        long uptimeMillis) {  
    msg.target = this;  // 系結訊息處理物件
    msg.workSourceUid = ThreadLocalWorkSource.getUid();  
  
    if (mAsynchronous) { // 根據handler創建是否是異步的,來將訊息標記為異步訊息 
        msg.setAsynchronous(true);  
    }  
    // 呼叫messagequeue的方法,加入佇列,
    return queue.enqueueMessage(msg, uptimeMillis);  
}

下面看下訊息如何入隊的

boolean enqueueMessage(Message msg, long when) {  
    if (msg.target == null) {  
        throw new IllegalArgumentException("Message must have a target.");  
    }  
  
    synchronized (this) {  
        if (msg.isInUse()) {  
            throw new IllegalStateException(msg + " This message is already in use.");  
        }  
  
        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;  
        }  
  
        msg.markInUse();  
        msg.when = when;  
        // 當前佇列的頭
        Message p = mMessages;  
        boolean needWake;  
        if (p == null || when == 0 || when < p.when) {  
            // New head, wake up the event queue if blocked.  
            // 訊息佇列為null,或者訊息是立刻執行的,或者要執行的時間先于頭訊息的時間,將當前訊息作為新的佇列的頭
            msg.next = p;  
            mMessages = msg;  
            needWake = mBlocked;  
        } else {  
            // Inserted within the middle of the queue.  Usually we don't have to wake  
            // up the event queue unless there is a barrier at the head of the queue            // and the message is the earliest asynchronous message in the queue.            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;  
        }  
		// 如果需要的話,喚醒佇列,開始處理訊息,就是MessageQueue的next方法開始執行
        // We can assume mPtr != 0 because mQuitting is false.  
        if (needWake) {  
            nativeWake(mPtr);  
        }  
    }  
    return true;  
}

取訊息以及訊息處理

取訊息1

取訊息入口是Looper的loop方法處理的

public static void loop() {  
    final Looper me = myLooper();  
    if (me == null) {  
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");  
    }  
    if (me.mInLoop) {  
        Slog.w(TAG, "Loop again would have the queued messages be executed"  
                + " before this one completed.");  
    }  
  
    me.mInLoop = true;  
  
    // Make sure the identity of this thread is that of the local process,  
    // and keep track of what that identity token actually is.    Binder.clearCallingIdentity();  
    final long ident = Binder.clearCallingIdentity();  
  
    // Allow overriding a threshold with a system prop. e.g.  
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'    final int thresholdOverride =  
            SystemProperties.getInt("log.looper."  
                    + Process.myUid() + "."  
                    + Thread.currentThread().getName()  
                    + ".slow", 0);  
  
    me.mSlowDeliveryDetected = false;  
	// 無限回圈,通過loopOnce取訊息
    for (;;) {  
        if (!loopOnce(me, ident, thresholdOverride)) {  
            return;  
        }  
    }  
}

處理訊息

private static boolean loopOnce(final Looper me,  
        final long ident, final int thresholdOverride) {  
    // 取訊息
    Message msg = me.mQueue.next(); // might block  
    if (msg == null) {  
        // No message indicates that the message queue is quitting.  
        return false;  
    }  
  
    // This must be in a local variable, in case a UI event sets the logger  
    final Printer logging = me.mLogging;  
    if (logging != null) {  
        logging.println(">>>>> Dispatching to " + msg.target + " "  
                + msg.callback + ": " + msg.what);  
    }  
    // Make sure the observer won't change while processing a transaction.  
    final Observer observer = sObserver;  
  
    final long traceTag = me.mTraceTag;  
    long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;  
    long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;  
    if (thresholdOverride > 0) {  
        slowDispatchThresholdMs = thresholdOverride;  
        slowDeliveryThresholdMs = thresholdOverride;  
    }  
    final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);  
    final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);  
  
    final boolean needStartTime = logSlowDelivery || logSlowDispatch;  
    final boolean needEndTime = logSlowDispatch;  
  
    if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {  
        Trace.traceBegin(traceTag, msg.target.getTraceName(msg));  
    }  
  
    final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;  
    final long dispatchEnd;  
    Object token = null;  
    if (observer != null) {  
        token = observer.messageDispatchStarting();  
    }  
    long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);  
    try {  
	    // 這里取出msg的target屬性,就是handler物件,進行訊息的處理,注意:屏障訊息是沒有target的,
        msg.target.dispatchMessage(msg);  
        if (observer != null) {  
            observer.messageDispatched(token, msg);  
        }  
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;  
    } catch (Exception exception) {  
        if (observer != null) {  
            observer.dispatchingThrewException(token, msg, exception);  
        }  
        throw exception;  
    } finally {  
        ThreadLocalWorkSource.restore(origWorkSource);  
        if (traceTag != 0) {  
            Trace.traceEnd(traceTag);  
        }  
    }  
    if (logSlowDelivery) {  
        if (me.mSlowDeliveryDetected) {  
            if ((dispatchStart - msg.when) <= 10) {  
                Slog.w(TAG, "Drained");  
                me.mSlowDeliveryDetected = false;  
            }  
        } else {  
            if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",  
                    msg)) {  
                // Once we write a slow delivery log, suppress until the queue drains.  
                me.mSlowDeliveryDetected = true;  
            }  
        }  
    }  
    // 這里會列印那些執行慢的訊息
    if (logSlowDispatch) {  
        showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);  
    }  
  
    if (logging != null) {  
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);  
    }  
  
    // Make sure that during the course of dispatching the  
    // identity of the thread wasn't corrupted.    final long newIdent = Binder.clearCallingIdentity();  
    if (ident != newIdent) {  
        Log.wtf(TAG, "Thread identity changed from 0x"  
                + Long.toHexString(ident) + " to 0x"  
                + Long.toHexString(newIdent) + " while dispatching to "  
                + msg.target.getClass().getName() + " "  
                + msg.callback + " what=" + msg.what);  
    }  
  
    msg.recycleUnchecked();  
  
    return true;  
}

取訊息2

再看下實際的取訊息的方法,MessageQueue的next方法

Message next() {  
    // Return here if the message loop has already quit and been disposed.  
    // This can happen if the application tries to restart a looper after quit    // which is not supported.    
    final long ptr = mPtr;  
    if (ptr == 0) {  
        return null;  
    }  
  
    int pendingIdleHandlerCount = -1; // -1 only during first iteration  
    int nextPollTimeoutMillis = 0;  
    for (;;) {  
        if (nextPollTimeoutMillis != 0) {  
            Binder.flushPendingCommands();  
        }  
		// 這個方法會在沒有訊息的時候阻塞
        nativePollOnce(ptr, nextPollTimeoutMillis);  
  
        synchronized (this) {  
            // Try to retrieve the next message.  Return if found.  
            final long now = SystemClock.uptimeMillis();  
            Message prevMsg = null;
            // 訊息頭保存
            Message msg = mMessages;  
            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()); 
                // 回圈的作用,找出佇列里的異步訊息,存盤在msg里,或者將同步屏障訊息存盤在msg中,prevMsg存盤的是同步訊息 
            }  
            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(); 
                    // 將取出的訊息交給handler處理, 
                    return msg;  
                }  
            } else {  
                // No more messages.  
                nextPollTimeoutMillis = -1;  
            }  
  
            // Process the quit message now that all pending messages have been handled.  
            if (mQuitting) {  
                dispose();  
                return null;  
            }  
  
            // If first time idle, then get the number of idlers to run.  
            // Idle handles only run if the queue is empty or if the first message            // in the queue (possibly a barrier) is due to be handled in the future.            
            if (pendingIdleHandlerCount < 0  
                    && (mMessages == null || now < mMessages.when)) {  
                pendingIdleHandlerCount = mIdleHandlers.size();  
            }  
            if (pendingIdleHandlerCount <= 0) {  
                // No idle handlers to run.  Loop and wait some more.  
                mBlocked = true;  
                continue;  
            }  
  
            if (mPendingIdleHandlers == null) {  
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];  
            }  
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);  
        }  
  
        // Run the idle handlers.  
        // We only ever reach this code block during the first iteration.        
        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);  
                }  
            }  
        }  
  
        // Reset the idle handler count to 0 so we do not run them again.  
        pendingIdleHandlerCount = 0;  
  
        // While calling an idle handler, a new message could have been delivered  
        // so go back and look again for a pending message without waiting.        
        nextPollTimeoutMillis = 0;  
    }  
}

handler的訊息處理

是從dispatchMessage方法開始的,里面進行訊息處理

public void dispatchMessage(@NonNull Message msg) {
	// 檢查message是否設定了callback物件(runnable型別的)
	if (msg.callback != null) {
		// 執行其run方法
		handleCallback(msg);
	} else {
		// 檢查handler是否設定了callback物件
		if (mCallback != null) {
			if (mCallback.handleMessage(msg)) {
				// 如果回傳true,后續就不執行了,
				return;
			}
		}
		// 自定義handler的時候,實作該方法處理訊息
		handleMessage(msg);
	}
}

IdleHandler是什么?干什么?繼續看MessageQueue類,

IdleHandler是MessageQueue的靜態內部介面,如下

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();  
}

當前執行緒的訊息對立內當前沒有訊息要處理時,會取出idlehander執行任務,因為執行在主執行緒,禁止執行耗時操作,回傳true表示執行完并不會移除該物件,false執行完一次就移除,
而且,執行時機是不確定的,
執行的地方在next方法內部,

Message next() {  
    // Return here if the message loop has already quit and been disposed.  
    // This can happen if the application tries to restart a looper after quit    // which is not supported.   
    final long ptr = mPtr;  
    if (ptr == 0) {  
        return null;  
    }  
  
    int pendingIdleHandlerCount = -1; // -1 only during first iteration  
    int nextPollTimeoutMillis = 0;  
    for (;;) {  
        if (nextPollTimeoutMillis != 0) {  
            Binder.flushPendingCommands();  
        }  
  
        nativePollOnce(ptr, nextPollTimeoutMillis);  
  
        synchronized (this) {  
            // Try to retrieve the next message.  Return if found.  
            final long now = SystemClock.uptimeMillis();  
            Message prevMsg = null;  
            Message msg = mMessages;  
            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());  
            }  
            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;  
            }  
  
            // Process the quit message now that all pending messages have been handled.  
            if (mQuitting) {  
                dispose();  
                return null;  
            }  
			// 執行到這里,說明沒有找到message物件需要執行了,且執行緒沒有退出,
            // If first time idle, then get the number of idlers to run.  
            // Idle handles only run if the queue is empty or if the first message            // in the queue (possibly a barrier) is due to be handled in the future.            
            if (pendingIdleHandlerCount < 0  
                    && (mMessages == null || now < mMessages.when)) {  
                pendingIdleHandlerCount = mIdleHandlers.size();  
            }  
            if (pendingIdleHandlerCount <= 0) {  
                // No idle handlers to run.  Loop and wait some more.  
                mBlocked = true;  
                continue;  
            }  
			// 取出idlehandlers
            if (mPendingIdleHandlers == null) {  
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];  
            }  
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);  
        }  
  
        // Run the idle handlers.  
        // We only ever reach this code block during the first iteration.  
        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 {  
	            // 執行idlehandler的方法
                keep = idler.queueIdle();  
            } catch (Throwable t) {  
                Log.wtf(TAG, "IdleHandler threw exception", t);  
            }  
  
            if (!keep) { 
	            // 需要移除的話,在此處進行移除
                synchronized (this) {  
                    mIdleHandlers.remove(idler);  
                }  
            }  
        }  
  
        // Reset the idle handler count to 0 so we do not run them again.  
        pendingIdleHandlerCount = 0;  
  
        // While calling an idle handler, a new message could have been delivered  
        // so go back and look again for a pending message without waiting.          
        nextPollTimeoutMillis = 0;  
    }  
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/552765.html

標籤:Android

上一篇:這么分析大檔案日志,以后就不用加班卷了!

下一篇:返回列表

標籤雲
其他(159251) Python(38148) JavaScript(25433) Java(18055) C(15228) 區塊鏈(8267) C#(7972) AI(7469) 爪哇(7425) MySQL(7197) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4573) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1975) 功能(1967) Web開發(1951) HtmlCss(1938) python-3.x(1918) C++(1917) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1861) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 我所知道的Handler

    簡單講,handler就是兩個功能 插入訊息,enqueuemessage,msg,when 從訊息佇列中遍歷所有訊息,比對msg.when和當前的when,找到合適的位置插入 處理訊息,looper.loop會從messagequeue中呼叫next。取訊息,如果訊息還沒到時間該執行,就會比對時間 ......

    uj5u.com 2023-05-18 09:03:21 more
  • 這么分析大檔案日志,以后就不用加班卷了!

    有沒有熟悉這樣的場景: 時間已過十一點,空蕩蕩的辦公室只剩自己孤身一人。陪你伏案忙碌的只有電腦風扇被迫營業的“嗡嗡”聲, 窗外的夜正黑得帶勁,仿佛巨獸的口吞噬自己的無奈。 天性善良不善言辭的你,容易被人頤指氣使,加班對你來說是家常便飯。 作為一名碼農,“我到底哪里錯了,我需要怎么解決?”是我的座右銘 ......

    uj5u.com 2023-05-17 08:01:18 more
  • 跑步課程匯入能力,助力科學訓練

    HUAWEI Health Kit為開發者提供用戶自定義的跑步課程匯入介面,便于用戶在華為運動健康App和華為智能穿戴設備上查看來自生態應用的訓練課表,開啟科學、適度的運動訓練。 跑步課程匯入能力支持生態應用在獲取用戶的華為帳號授權后,將跑步課程資料寫入至華為運動健康App,并在已有的華為智能穿戴設 ......

    uj5u.com 2023-05-12 10:43:01 more
  • 京喜APP - 圖片庫優化

    京喜APP早期開發主要是快速原生化迭代替代原有H5,提高用戶體驗,在這期間也積累了不少性能問題。之后我們開始進行一些性能優化相關的作業,本文主要是介紹京喜圖片庫相關優化策略以及關于圖片相關的一些關聯知識。 ......

    uj5u.com 2023-05-12 10:42:54 more
  • 鯨鴻動能廣告接入如何高效變現流量?

    廣告是App開發者最常用的流量變現方法之一,當App擁有一定數量用戶時,開發者就需要考慮如何進行流量變現,幫助App實作商業可持續增長。 鯨鴻動能流量變現服務是廣告服務依托華為終端強大的平臺與資料能力為開發者提供的App流量變現服務,開發者通過該服務可以在自己的App中獲取并向用戶展示精美的、高價值 ......

    uj5u.com 2023-05-12 10:42:17 more
  • 京喜APP - 圖片庫優化

    京喜APP早期開發主要是快速原生化迭代替代原有H5,提高用戶體驗,在這期間也積累了不少性能問題。之后我們開始進行一些性能優化相關的作業,本文主要是介紹京喜圖片庫相關優化策略以及關于圖片相關的一些關聯知識。 ......

    uj5u.com 2023-05-12 10:41:55 more
  • 跑步課程匯入能力,助力科學訓練

    HUAWEI Health Kit為開發者提供用戶自定義的跑步課程匯入介面,便于用戶在華為運動健康App和華為智能穿戴設備上查看來自生態應用的訓練課表,開啟科學、適度的運動訓練。 跑步課程匯入能力支持生態應用在獲取用戶的華為帳號授權后,將跑步課程資料寫入至華為運動健康App,并在已有的華為智能穿戴設 ......

    uj5u.com 2023-05-12 10:41:36 more
  • 鯨鴻動能廣告接入如何高效變現流量?

    廣告是App開發者最常用的流量變現方法之一,當App擁有一定數量用戶時,開發者就需要考慮如何進行流量變現,幫助App實作商業可持續增長。 鯨鴻動能流量變現服務是廣告服務依托華為終端強大的平臺與資料能力為開發者提供的App流量變現服務,開發者通過該服務可以在自己的App中獲取并向用戶展示精美的、高價值 ......

    uj5u.com 2023-05-12 10:40:52 more
  • 一統天下 flutter - 插件: flutter 使用 web 原生控制元件,并做資

    原始碼 https://github.com/webabcd/flutter_demo 作者 webabcd 一統天下 flutter - 插件: flutter 使用 web 原生控制元件,并做資料通信 示例如下: lib\plugin\plugin2.dart /* * 插件 * 本例用于演示 flu ......

    uj5u.com 2023-05-11 08:43:32 more
  • 汽車之家Unity前端通用架構升級實踐

    隨著之家3D虛擬化需求的增加,各產品線使用Unity引擎的專案也越來越多,新老專案共存,代碼維護成本也隨之增加。代碼質量參差加之代碼規范仍沒有完全統一產生高昂學習成本進一步加重了專案維護負擔。
    為應對這些問題,我們決定借助主機廠數科產品線銷冠神器VR版本大升級為貧訓,開發一套移動端通用Unity代碼... ......

    uj5u.com 2023-05-10 10:01:15 more