主頁 > 移動端開發 > Android Handler機制

Android Handler機制

2021-11-04 11:37:53 移動端開發

Looper是整個跨執行緒通信的管理者

    // 內部持有的變數如下:
    ThreadLocal<Looper>
    MainLooper
    Observer
    MessageQueue
    Thread

1.首先先回憶一下Handler怎么用

Android執行緒通信分為以下兩種情況
1.子執行緒發訊息給UI執行緒
2.UI執行緒發訊息給子執行緒
3.子執行緒發訊息給另個子執行緒

1.子執行緒發訊息給UI執行緒

class FragmentContentActivity : AppCompatActivity() {
    val FLAG = 1
    lateinit var handler: Handler
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        handler = object : Handler(Looper.getMainLooper()) {
            override fun handleMessage(msg: Message) {
                when (msg.what) {
                    FLAG -> {
                        findViewById<TextView>(R.id.text).text = msg.data["Text"].toString()
                    }
                }
            }
        }
        thread {
            Thread.sleep(2000L)
            handler.sendMessage(Message.obtain().apply {
                what = FLAG
                data = Bundle().apply {
                    this.putString("Text", "ThreadMessage")
                }
            })
        }
    }
}

2.UI執行緒/子執行緒發訊息給子執行緒

class FragmentContentActivity : AppCompatActivity() {
    val THREAD_FLAG =2
    lateinit var threadHandler: Handler
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        thread {
            Looper.prepare()
            threadHandler = object :Handler(Looper.myLooper()!!){
                override fun handleMessage(msg: Message) {
                    when(msg.what){
                        THREAD_FLAG -> {
                            Toast.makeText(
                                this@FragmentContentActivity,
                                "${msg.data["Text"]}",
                                Toast.LENGTH_SHORT
                            ).show()
                        }
                    }
                }
            }
            Looper.loop()
        }
    }

    override fun onResume() {
        super.onResume()
        findViewById<TextView>(R.id.text).postDelayed({
           threadHandler.sendMessage(Message.obtain().apply {
               what = THREAD_FLAG
               data = Bundle().apply {
                   putString("Text","UI Message")
               }
           })
        },2000L)
    }
}

**在子執行緒的使用中,我們發現必須要進行Looper.prepare()和Looper.loop()前后這兩個操作,因此,帶著這個疑問來看一下Looper的邏輯
**

// 在呼叫prepare()之后一定要呼叫loop(),最后結束訊息回圈的時候呼叫quit()
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));
}
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

prepare()就是將初始化一個Looper物件放入到ThreadLocal中,初始化Looper,同時mQueue

    public static void loop(){
    
    Binder.clearCallingIdentity()
for (;;) {
    Message msg = queue.next(); // might block
 
    long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
    try {
        // 其實 loop()只做了這一個呼叫,其他的都是監控當前訊息回圈時間是否超時,應該和ANR有關
        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 (slowDeliveryDetected) {
            if ((dispatchStart - msg.when) <= 10) {
                Slog.w(TAG, "Drained");
                slowDeliveryDetected = false;
            }
        } else {
            if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                    msg)) {
                // Once we write a slow delivery log, suppress until the queue drains.
                slowDeliveryDetected = true;
            }
        }
    }
    if (logSlowDispatch) {
        showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
    }
    //訊息物體回收
    msg.recycleUnchecked();

可以看到Looper.loop其實只是在for回圈中,獲取mQueue的下一個msg節點,然后呼叫msg.target.dispatchMessage(msg),乍看只是msg物件內部的操作,

因為loop()其實邏輯上算死回圈,這意味著,當前執行緒的自發的順序執行命令到此結束了,只能通過其他執行緒觸發handler機制,來被動的在當前執行緒執行命令,當前執行緒完全變成了一個回應執行緒

Looper類只是初始化并開啟執行緒死回圈的一個開關,具體作業在MessageQueue中進行

MessageQueue 訊息佇列

佇列內訊息的添加不是直接呼叫MessageQueue,而是由與Looper相關聯的Handler呼叫

MessageQueue的內部持有的變數如下: ArrayList mMessages SparseArray IdleHandler[] mBlocked

MessageQueue類的功能主要有:元素插入佇列,獲取佇列的頭部元素,查找佇列中元素,綜述就是對佇列的增刪改查,其中 mMessage就是這個佇列的入口也是這個佇列的頭結點

  boolean enqueueMessage(Message msg,long when) //msg 元素插入佇列
  boolean hasMessages(Handler h,int what,Object object) //查找handler下的msg.what/object相同的Msg
  boolean hasEqualMessages(Handler h,int what,Object obj)//查找 msg.object.equal(obj)的msg
  removeMessages(Handler h,int what,Object obj)/(Handler h,Runnable r,Object obj)
  removeEqualMessages(...) //洗掉與引數msg.object相同或equal的msg
  
  Message next() //獲取佇列中的頭部元素

可以看出,這些方法內部都呼叫了 synchronized(this),佇列的操作都是執行緒同步的

Message next() ->
...

// linux機制下的總線進入輪詢,執行緒相當于掛起狀態,nextPollTimeOut是掛起多長時間
nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {

   final long now = SystemClock.uptimeMillis();
   Message prevMsg = null;
   Message msg = mMessages;
   //先判斷msg.target是否為null,表示當前訊息是不是異步訊息
   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;
}
...

可以看出next()內部主要有兩種獲取msg的邏輯

1.當前訊息都是普通訊息,按照msg.when的大小排序,每一次回圈執行,通過檢測when是否大于now來決定是否獲取msg,或是掛起當前執行緒,
2.當前訊息中有異步訊息,優先獲取msg.isAsynchronous()==true的,或者按照此異步訊息的等待時間,來重新設定掛起執行緒的時間,從而達到精準的獲取異步訊息,

通俗的來講就是說,當前所有普通訊息按照預約的執行時間的先后來排隊,這樣可基本上既可以達到按照預約時間執行訊息,也可以最大效率的在一定時間段內執行最多的訊息,但是這忽略了每個訊息的執行消耗的時間,比如A訊息是佇列內的No.1,A訊息預約執行時間是1s之后,整個佇列是等待狀態的,這個時候來了B訊息,B訊息預約的時間是0.999s之后,按照預約時間的排隊機制,B訊息要插隊到A訊息之前,B成了這個佇列的No.1,A成了No.2,整個佇列的等待時間還是1s(因為之前設定了等待時間,所以不用喚醒),但是B訊息的執行程序長達0.5s,已經超過了之后的很多訊息的預約執行時間點了,這樣就不能保證某些重要的訊息按時執行,

于是就有了異步訊息同步屏障的機制,這相當于普通訊息排隊時來了一個VIP訊息,先按照預約時間找到自己的位置,然后大喝一聲:“都把腳給我挪開,我的前面不允許有人”,這個時候排在他之前的普通訊息就只能全部挪到佇列的一邊,然后佇列重新按照這位VIP訊息,設定等待時間,期間新來的普通訊息也插到隊邊等待,保證精準按時執行VIP訊息,等VIP訊息執行完,之后再把之前等待普通訊息的佇列合并執行,當然之前等待的訊息全耽誤了,但畢竟是普通訊息不重要,

 // 同步屏障的方法,此方法只在 ViewRootImpl類中呼叫
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++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;
        //沒有設定target
        Message prev = null;
        Message p = mMessages;
        if (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 {
            msg.next = p;
        //mMessages變為同步屏障訊息,next()下一次回圈,首先獲取到的是同步屏障
            mMessages = msg;
        }
        return token;
    }
    // ViewRootImpl
 void scheduleTraversals() {
        if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
    
//設定同步屏障之后,通過設定了Aysnc標記位的Handler發送的Msg都是異步訊息,
//MessageQueue也優先處理此類異步訊息,直到移除同步屏障標記位,再恢復到普通訊息佇列,

由此可見,同步屏障的設定和View重繪機制有關,因為要保證Vsync信號按時完成重繪操作,具體分析待續…
綜述,異步訊息可以保證精準的執行,但也因此訊息事件的先后順序被打亂,有可能在代碼執行中執行了Handler.sendMsg(1,time0.2)->AsyncHandler.sendMsg(2,time0.5),但是實際執行的是 2->1,

再看Handler
Handler的成員變數如下

mLooper :初始化時獲取當前執行緒的Looper物件參考 mQueue :通過Looper.mQueue 獲取到的MessageQueue佇列參考mAsynchronous :標記當前Handler是否發送異步訊息 mCallback : Handler自身的callback介面,此callback呼叫在Message.callback之前mMessenger :IMessager 和行程通信相關

以上成員變數大都是final型別,表示Handler也是在其使用上也是final型別,也就是說,沒有辦法通過將Handler與context的生命周期相剝離來避免記憶體泄漏

Handler的方法如下

    //Handler 發送Message第一種方法,設定Message的what,data
    //不設定 runnable:Callback 
    boolean sendMessage(Message msg) -> boolean sendMessageDelayed(Message msg,long delayTime)
    -> boolean sendMessageAtTime(Message msg,SystemClock.uptimeMillis()+delayTime)
    -> mQueue.enqueueMessage(msg,uptime)
    //第二種方法,Message只設定runnable:Callback
    boolean postAtTime(Runnable r,Object token,long uptime)
    -> sendMessageAtTime(getPostMessage(r,token),uptime)
    --> Message getPostMessage(Runnable r,Object token){
        Message.obtain().callback=r
        ...
        }
   //移除Message和檢驗Message
   removeMessages() 
   hasMessages()
   ...
   //Message 回呼執行
   void dispatchMessage(Message msg){
       if(msg.callback!=null){
           handleCallback(msg) ->
       }else{
           if(mCallback!=null){
            mCallback.handleMessage(msg)
           }
           handleMessage(msg)
   }
   //可以看到 Message的回呼分為三個等級
   //No.1 msg自身的callback
   //No.2 Handler自身的mCallback成員變數,mCallback是final型別
   //No.3 Handler的子類多載的handleMessage方法

Message

Message 實作了Parcelable介面,也就是說可以作為行程間通信的載體

Message成員變數如下

    int what //Handler發送主體下的Message的訊息碼
    int arg1 //低成本的引數傳遞
    int arg2 
    Object obj //可以為空的token物件,一般在行程通信中用到
    Bundle data //執行緒通信中常用的引數容器
    Handler target //發送主體
    Runnable callback //Message自身回呼
    Messenger replyTo   //行程通信,一般在AMS中用到
    ------
    // Message快取池相關
    Object sPoolSync = new Object() // 同步標記
    Messsage next 
    static Message sPool
    static int sPoolSize

Message方法如下

    //可以看出這是一個非常巧妙的方法
    static Message obtain(){
        synchronized(sPoolsSync){
            if(sPools!=null){
                Message m= sPool;
                sPool = m.next;
                m.next = null;
                sPoolSize--;
                return m;
              }
        }
        return new Message();
    }
    //主體上是一個帶快取池鏈表的同步工廠模式,同時也考慮到較多執行緒阻塞時
    //可以直接宣告初始化物件
    
    //回收Message物件到快取池鏈表
    void recycleUnchecked(){
        ...引數=null
        synchronized(sPoolSync){
            if(sPoolSize < MAX_SIZE){
                next = sPool;
                sPools = this;
                sPoolSize++;
          }
       }
    }   

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

標籤:其他

上一篇:準備3個月,騰訊音樂Android高級崗第三面掛了...

下一篇:Swift 陣列及常用方法

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(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
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more