主頁 > 移動端開發 > 你確定你的Glide不會發生記憶體泄漏嗎?

你確定你的Glide不會發生記憶體泄漏嗎?

2021-12-07 11:00:16 移動端開發

最近正在閱讀Glide原始碼,今天我們要研究的部分是Glide RequestManager 生命周期管理, 本來這個也是這篇文章應該是Glide生命周期管理,但是在原始碼閱讀中我發現原來我以前的專案對于Glide的使用存在著一些記憶體泄漏的可能,因此臨時決定更改了文章的名字,希望能夠引起大家的重視,

這個是我們的主界面樣式

通過最下面的一排選項卡,控制主界面的一級fragment ,一級Fragment下面又有若干的子Fragment,fragment又包含一些其它的View,以RecyclerView舉例,在對應的Adapter創建的時候會傳遞Context物件,加載的時候

Glide.with(context).load("path").into(imagerView) 這樣做會存在記憶體泄漏的可能,

下面正式分析內因為Glide使用不當造成記憶體泄漏的原理,

Glide生命周期

作為一個android開發者,說到生命周期,最先想到的應該是activity的生命周期了吧,activity的生命周期是android系統開發者給我們設定的一些模板方法,我們只需要在對應的方法中實作對應的業務邏輯即可,那么Glide的生命周期是怎么來的呢?

Glide生命生命周期主要分為兩個:

  1. activity/fragment 生命周期方法呼叫,影響到整個頁面所有請求,
  2. 網路狀態變化引起整個requestManager 所管理的所有請求發生改變,

頁面管理

Glide#with方法回傳的是一個RequestManager物件,而RequestManager的獲取實際上都呼叫了RequestManagerRetriever#get來獲取RequestManager物件的,

RequestManagerRetriever用于創建新的 RequestManager 或從Activity和Fragment中檢索現有的,

RequestManagerRetriever的構建

public RequestManagerRetriever(@Nullable RequestManagerFactory factory) {
  this.factory = factory != null ? factory : DEFAULT_FACTORY;
  handler = new Handler(Looper.getMainLooper(), this /* Callback */);
}

它的factory由Glide傳遞過來,如果我們不進行配置默認為空,就是使用DEFAULT_FACTORY進行創建

private static final RequestManagerFactory DEFAULT_FACTORY = new RequestManagerFactory() {
  @NonNull
  @Override
  public RequestManager build(@NonNull Glide glide, @NonNull Lifecycle lifecycle,
      @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context) {
    return new RequestManager(glide, lifecycle, requestManagerTreeNode, context);
  }
};

RequestManagerRetriever獲取對應的RequestManager

RequestManagerRetriever#get傳遞的引數有下面幾類,

  1. Context 會嘗試將其轉換成對應的activity否則獲取的是Application 級別的RequestManager
  2. Activity/fragment RequestManagerRetriever會嘗試通過他們的FragmentManager獲取一個不可見的子fragment,如果沒有獲取成功則新建一個,并添加到activity/fragment中,
  3. View 當傳遞一個View進來的時候,會先獲取對應的activity,如果獲取不到則直接使用Application級別的RequestManager,如果獲取到了activity,會查看當前View是否在某一個activity中,如果在使用fragment獲取對應的ReauestManager 如果不在則使用Activity的RequestManager,

需要特別注意的是:不論傳遞什么引數,在子執行緒進行圖片加載都會統一使用Application級別的RequestManager,

這里以RequestManagerRetriever#get(View view)來說明其流程

@NonNull
public RequestManager get(@NonNull View view) {
    //如果在子執行緒,使用Application級別的RequestManager
  if (Util.isOnBackgroundThread()) {
    return get(view.getContext().getApplicationContext());
  }
    //進行非空判斷
  Preconditions.checkNotNull(view);
  Preconditions.checkNotNull(view.getContext(),
      "Unable to obtain a request manager for a view without a Context");
  //查找對應的Activity
  Activity activity = findActivity(view.getContext());
 //如果activity為空則直接使用
  if (activity == null) {
    return get(view.getContext().getApplicationContext());
  }
?
  //查找到view所屬的fragment,則使用fragment  查找不到則使用activity,
  if (activity instanceof FragmentActivity) {
    Fragment fragment = findSupportFragment(view, (FragmentActivity) activity);
    return fragment != null ? get(fragment) : get(activity);
  }
?
  // Standard Fragments.
  android.app.Fragment fragment = findFragment(view, activity);
  if (fragment == null) {
    return get(activity);
  }
  return get(fragment);
}

通過activity查找當前View所屬的fragment

private android.app.Fragment findFragment(@NonNull View target, @NonNull Activity activity) {
    //是以View作為key Fragment作為value的ArrayMap
  tempViewToFragment.clear();
    //將所有的fragment  包含activity下的fragment和fragment中的子Fragment
    //通過遞回的方式全部添加到 tempViewToFragment
  findAllFragmentsWithViews(activity.getFragmentManager(), tempViewToFragment);
?
  android.app.Fragment result = null;
?
  View activityRoot = activity.findViewById(android.R.id.content);
  View current = target;
  //不斷對比直到當前的view為contentView 則停止查找fragment
  while (!current.equals(activityRoot)) {
    result = tempViewToFragment.get(current);
      //查找到了對應的fragment,退出當前回圈
    if (result != null) {
      break;
    }
    if (current.getParent() instanceof View) {
      current = (View) current.getParent();
    } else {
      break;
    }
  }
  tempViewToFragment.clear();
  return result;
}

通過fragment獲取RequestManager

RequestManagerRetriever#get(Fragment fragment) 會呼叫supportFragmentGet來獲取RequestManager,

 @NonNull
  private RequestManager supportFragmentGet(
      @NonNull Context context,
      @NonNull FragmentManager fm,
      @Nullable Fragment parentHint,
      boolean isParentVisible) {
      //獲取當前FragmentManager 下的SupportRequestManagerFragment  在getSupportRequestManagerFragment內部,如果沒有對應的fragment,會為其添加,
    SupportRequestManagerFragment current =
        getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
    RequestManager requestManager = current.getRequestManager();
     //當前SupportRequestManagerFragment  沒有RequestManager 則創建一個RequestManager與其生命周期系結,
    if (requestManager == null) {
           Glide glide = Glide.get(context);
      requestManager =
          factory.build(
              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
      current.setRequestManager(requestManager);
    }
    return requestManager;
  }

RequestManager的構建程序

RequestManager有兩個構造方法,但是最終都會執行下面這個,

RequestManager(
    Glide glide,
    Lifecycle lifecycle,
    RequestManagerTreeNode treeNode,
    RequestTracker requestTracker,
    ConnectivityMonitorFactory factory,
    Context context) {
  this.glide = glide;
  this.lifecycle = lifecycle;
  this.treeNode = treeNode;
  this.requestTracker = requestTracker;
  this.context = context;
//創建一個網咯變化的監聽  網路監聽是Glide默認實作的,我們也可以通過指定factory實作其它的一些業務邏輯,
//當網路連接上后會將所有請求失敗的重新嘗試,
  connectivityMonitor =
      factory.build(
          context.getApplicationContext(),
          new RequestManagerConnectivityListener(requestTracker));
?
  //實作
  if (Util.isOnBackgroundThread()) {
    mainHandler.post(addSelfToLifecycle);
  } else {
    lifecycle.addListener(this);
  }
  //將網路變化的監聽與生命周期進行系結
  lifecycle.addListener(connectivityMonitor);
?
  defaultRequestListeners =
      new CopyOnWriteArrayList<>(glide.getGlideContext().getDefaultRequestListeners());
  setRequestOptions(glide.getGlideContext().getDefaultRequestOptions());
//將當前RequestManager添加到Glide方便統一進行管理
  glide.registerRequestManager(this);
}

網路監聽

在構造方法中,創建connectivityMonitor的時候,將requestTracker傳遞給了RequestManagerConnectivityListener,他的實作如下:

private class RequestManagerConnectivityListener
    implements ConnectivityMonitor.ConnectivityListener {
  @GuardedBy("RequestManager.this")
  private final RequestTracker requestTracker;
?
  RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) {
    this.requestTracker = requestTracker;
  }
?
  @Override
  public void onConnectivityChanged(boolean isConnected) {
    if (isConnected) {//網路連接上會重新開始請求,
      synchronized (RequestManager.this) {
        requestTracker.restartRequests();
      }
    }
  }
}

RequestManager#onDestory

@Override
public synchronized void onDestroy() {
    //通知所有target呼叫onDestory  
  targetTracker.onDestroy();
    //通知每一個target 進行清除
  for (Target<?> target : targetTracker.getAll()) {
    clear(target);
  }
    //清除集合
  targetTracker.clear();
    //清除當前requestManager管理的request
  requestTracker.clearRequests();
  lifecycle.removeListener(this);
  lifecycle.removeListener(connectivityMonitor);
  mainHandler.removeCallbacks(addSelfToLifecycle);
  glide.unregisterRequestManager(this);
}

target的清除程序

RequestManger的clear(Target<?> target)會內用untrackOrDelegate

private void untrackOrDelegate(@NonNull Target<?> target) {
  //清除對應的request,并對request進行重置,放進物件重用池,
  boolean isOwnedByUs = untrack(target);
//如果當前的target不歸自己管理,會遍歷所有的requestManager查找到合適的requestManager進行處理,
  if (!isOwnedByUs && !glide.removeFromManagers(target) && target.getRequest() != null) {
    Request request = target.getRequest();
    target.setRequest(null);
    request.clear();
  }
}

request清除程序

synchronized boolean untrack(@NonNull Target<?> target) {
  Request request = target.getRequest();
  // If the Target doesn't have a request, it's already been cleared.
  if (request == null) {
    return true;
  }
?
  if (requestTracker.clearRemoveAndRecycle(request)) {
    //從對應的集合中移除
    targetTracker.untrack(target);
    target.setRequest(null);
    return true;
  } else {
    return false;
  }
}
?
public boolean clearRemoveAndRecycle(@Nullable Request request) {
    return clearRemoveAndMaybeRecycle(request, /*isSafeToRecycle=*/ true);
  }
?
  private boolean clearRemoveAndMaybeRecycle(@Nullable Request request, boolean isSafeToRecycle) {
     if (request == null) {
      return true;
    }
    //如果能夠從集合中移除成功,那么這個request歸屬當前的RequestManager管理
    boolean isOwnedByUs = requests.remove(request);
    // Avoid short circuiting.
    isOwnedByUs = pendingRequests.remove(request) || isOwnedByUs;
    if (isOwnedByUs) {
      //執行request.clear  對request狀態進行轉變,和做相應的通知
      request.clear();
      if (isSafeToRecycle) {
        //這個是不會進行記憶體泄漏的關鍵,將request對應的參考回呼置空,切斷參考關系,
        //對應代碼可以參考SingleRequest
        request.recycle();
      }
    }
    return isOwnedByUs;
  }

Glide真的不會發生記憶體泄漏嗎?

前面我們梳理了Glide的生命周期,知道在生命相關的activity/Fragment銷毀的時候會暫停和回收相關的請求,并且切斷網路請求回呼的參考,那么Glide是不是真的能夠完全避免記憶體內泄漏呢?

這個直接給出我的結論:正常情況下使用Glide不會造成內Activity、Fragment、View記憶體泄漏,但是如果Glide使用不當是可能造成記憶體泄漏的,比如在Fragment使用Glide#with傳遞activity物件, 原因是Fragment結束的時候,Activity幾倍RequestManager并沒有接收到相應的生命周期方法,

實驗證明:

改造我們在Glide資料輸入輸出撰寫的加載音頻封面的ModelLoader,當遇到特定該音頻的時候執行緒休眠300秒

@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
    try {
        Log.d(TAG,"loadData assetPath "+assetPath);
        AssetFileDescriptor fileDescriptor = assetManager.openFd(assetPath);
        //特定路徑 休眠300s  模擬網路加載緩慢
        if(assetPath.contains("DuiMianDeNvHaiKanGuoLai--RenXianQi.mp3")){
            SystemClock.sleep(10*30*1000);//休眠300s
        }
        mediaMetadataRetriever.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(),fileDescriptor.getDeclaredLength());
        byte[] bytes = mediaMetadataRetriever.getEmbeddedPicture();
        if(bytes == null){
            callback.onLoadFailed(new FileNotFoundException("the file not pic"));
            return;
        }
        ByteBuffer buf = ByteBuffer.wrap(bytes);
        Log.d(TAG,"loadData assetPath "+assetPath +" success");
        callback.onDataReady(buf);
    } catch (IOException e) {
        e.printStackTrace();
        callback.onLoadFailed(e);
    }
}

在Activity中添加一個Fragment,當頁面創建成功后,使用Glide#with傳遞activity/context物件,并在activity中移除該Fragment,

將fragment的根View與fragment強制關聯,方便利用LeakCanary進行記憶體泄漏檢測,

public static class MyTestFragment extends Fragment {
    ImageView imageView;
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_glide_source_test,container,false);
        imageView = root.findViewById(R.id.imageView);
        //強制保留參考關系  方便進行檢測
        root.setTag(this);
        Log.d(TAG,"onCreateView finish");
        return root;
    }
?
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
?
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Glide.with(getActivity()).load(Uri.parse("file:///android_asset/DuiMianDeNvHaiKanGuoLai--RenXianQi.mp3")).diskCacheStrategy(DiskCacheStrategy.NONE).into(imageView);
        Log.d(TAG,"onActivityCreated load ");
        imageView.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"onActivityCreated remove ");
                getActivity().getSupportFragmentManager().beginTransaction().remove(MyTestFragment.this).commit();
            }
        },300);
?
    }
?
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }
}

實驗結果:

可以看這里因為Fragment被View持有導致了Fragment記憶體泄漏,這個也就反應了當Glide使用不當,會導致View的記憶體泄漏, 解決:傳遞正確的引數給with,或者呼叫ViewTarget#clearOnDetach,我沒有使用過clearOnDetach 根據Glide注釋,這個是一組實驗性api,后續可能會被移除,

小結Glide使用注意事項

Glide#with方法在引數使用優先級

fragment > view > activity > application

其中view和activity 在明確知道當前使用的頁面是activity優先傳遞activity 因為view會通過多次回圈遍歷查找fragment、activity,正確的使用Glide可以避免因為Glide造成記憶體泄漏,

Glide RequestOptions 可以分為三個級別:

  1. 應用級 可以進行全域配置
  2. 頁面級別 activty/fragment 可以為每一個特殊的頁面進行定制化處理,作用于RequestManager
  3. 單個請求 作用于RequestBuilder 為每一個請求構建請求配置項

Glide如何保證圖片的加載不會出現錯亂

ViewTarget#setRequest會呼叫View的setTag 將request請求物件放在View中,在請求的時候會通過ViewTarget#getRequest,如果回傳的與前一個請求一致則使用原來的請求,否則清除原來的請求,

對于使用application加載和在子執行緒進行圖片加載,需要謹慎使用,除非你明確他們的使用場景與自身的業務契合,

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

標籤:其他

上一篇:整理了Android面中常見的62個Java知識點...

下一篇:從零開始的iOS開發: 20 | 計算器APP

標籤雲
其他(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