主頁 > 移動端開發 > 【Android食用指南-01】Android虛擬機&類加載機制

【Android食用指南-01】Android虛擬機&類加載機制

2021-09-01 14:47:47 移動端開發

前言

在上一篇文章中【JVM入門食用指南-03】JVM垃圾回收器以及性能調優[1],對垃圾回收器以及JVM性能調優進行了敘述,并對JVM中CMS垃圾回收器進行了重點闡述,通過之前在JVM中的鋪墊,本文我們回到Android中,本文我們主要對 Android虛擬機類加載機制進行敘述,加深對 Android 的理解,

即將學會

  • ART 和 Dalvik
  • Android 類加載流程
  • 熱修復實作原理

ART 與 Dalvik

JVM 與 DVM

JVM 是我們的 Java 虛擬機,而我們的Android應用運行在 Dalvik/ART 虛擬機上的,每一個應用對應一個 Dalvik 虛擬機,雖然 Dalvik 虛擬機也算 Java 虛擬機,不過執行的不是.class檔案,而是dex檔案,

這里對上述名詞進行一些敘述

虛擬機分類

基于堆疊的虛擬機 每一個運行的執行緒,都有一個獨立的堆疊,堆疊中記錄了方法呼叫的歷史,每一個方法的呼叫,都對應一個堆疊幀,并且將堆疊幀壓入堆疊中,最頂部的堆疊幀為當前堆疊幀,既 當前執行的方法,基于堆疊幀 與 運算元堆疊進行所有操作,

暫存器 CPU的組成部分,暫存器是有限存盤容量的高速存盤容器(計算機中各級存盤速度 暫存器-> 一級快取->二級快取 -> 記憶體 -> 硬碟),可以用來管理指令、資料的地址,

基于暫存器的虛擬機 (模擬暫存器的操作流程)

基于暫存器的虛擬機中沒有運算元堆疊,不過有很多虛擬暫存器,類似運算元堆疊,這些暫存器也存放在運行時堆疊中,本質上就是一個陣列,與JVM相似,在Dalvik 虛擬機中每個執行緒都有自己的PC 和 呼叫堆疊,方法呼叫的活動記錄以幀為單位保存在呼叫堆疊上,

不同虛擬機流程

我們對比一下 基于堆疊的流程 和 基于暫存器的流程

JVM流程分析參考之前文章分析,通常需要經過以下操作

  • 如將堆疊頂某型別值存入區域變數某處
  • 將區域變數中某處某型別常量壓入運算元堆疊
  • 執行相關指令

DVM 中,直接依據指令,將某型別值存入虛擬暫存器中,相關的操作也依據指令在虛擬暫存器中通過CPU處理,將處理結果回傳虛擬暫存器(省掉了基于堆疊的流程中區域變數與運算元堆疊中資料的流動

ART 與 Dalvik 區別

ART 和 Dalvik 都是基于暫存器的虛擬機

Dalvik 執行 dex位元組碼解釋執行,在Android2.2 開始,支持即時編譯,既選擇程式中經常執行的代碼(又稱熱點代碼),進行編譯和優化 減少了JVM中解釋的流程,

ART虛擬機 (Android4.4) 直接跑機器碼不進行位元組碼解釋,Android的運行時從Dalvik 無影響過渡ART,因為并不用開發者將應用直接編譯成目標機器碼,APk依舊是一個包含dex位元組碼的檔案,

為什么APK依舊是包含dex位元組碼的檔案,而ART和 Dalvik的執行方式不一樣?

這里ART直接跑位元組碼,而Dalvik進行解釋執行,并在2.2引入JIT,但是都是dex檔案,

因為APK檔案需要在Android系統中運行,需要進行安裝,這個時候,經過虛擬機,會對dex進行優化,Dalvik中,會將dex位元組碼進行優化生成odex檔案,而ART 會將 dex位元組碼翻譯為本地機器碼(引入了預先編譯機制 Ahead of Time),安裝時,ART使用設備自帶的dex2oat工具對應用進行編譯,dex中的位元組碼被編譯成本地機器碼,這也是為什么我們安裝的時候比較慢,

不過后面從Android O 開始,安裝速度變快了,因為這個時候采用混合編譯了,JIT、AOT、和位元組碼解釋, 當應用首次安裝時,不進行AOT編譯,運行程序中采用解釋執行,并將程式中經常要執行的代碼進行即時編譯(JIT),并將JIT編譯后的方法記錄到Profile組態檔,當設備閑置時,利用編譯守護行程,依據Profile檔案對常用檔案進行AOT編譯(生成.art檔案),以便下次運行時直接使用,如果下次程式運行,將檢測該檔案并決議出機器碼中檔案中的類資訊,加載到ClassLoader中,

ClassLoader 及 類加載流程

ClassLoader

每一個Java程式都是有一個或多個class檔案組成,在程式運行時,需要將class檔案加載到JVM中才可以使用,而加載class檔案的是Java的類加載機制,ClassLoader加載class檔案提供程式運行時使用,每個Class物件內部都有一個classLoader欄位來標識由何種classLoader加載,

ClassLoader 類 以及 類結構

  • ClassLoader
  • BootClassLoader 用于加載Android Framework 層 Class檔案(像String.Class)
  • PathClassLoader 繼承自ClassLoader 用于Android應用程式加載類

類加載流程

PathClassLoader

下面我們分析一下PathClassLoader類來分析類加載流程

/**
 * Loads the class with the specified <a href="#name">binary name</a>.  The
 * default implementation of this method searches for classes in the
 */
// Android-removed: Remove references to getClassLoadingLock
//                   Remove perf counters.
//
// <p> Unless overridden, this method synchronizes on the result of
// {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
// during the entire class loading process.
//
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{       
        //找快取,已經加載過的就不會被加載了
        //class檔案需要轉換為class物件 耗時操作
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        //沒有快取進if
        if (c == null) {
            try {
                //如果該類下的parent物件實體不為空,利用該parent物件加載相關類
                //該物件指向 BootClassLoader 如下圖所示
                if (parent != null) {

                    c = parent.loadClass(name, false);
                } else {
                    //parent 為空,回傳空 
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                //如果 parent(BootstrapClass)中加載不到該類
                //則 呼叫自身findClass去加載類(PathClassLoader)
                c = findClass(name);
            }
        }
        //有快取直接回傳
        return c;
}

/**
 * Returns a class loaded by the bootstrap class loader;
 * or return null if not found.
 */
private Class<?> findBootstrapClassOrNull(String name)
{
    return null;
}

我們可以通過上述代碼分析 和 PathClassLoader類結構圖得到該類的loadClass流程

那么為什么我們ClassLoader中要進行這樣的設計呢 ?

首先,這個parent的目的在于實作類加載的雙親委托,首先加載任務委托給父類加載器,依次遞回,如果父類加載器可以完成類加載任務,則成功回傳,只有父類加載器無法完成此加載任務時,才自己去完成加載, 而實作這種機制,可以達到以下效果

  • 避免重復加載,當parent對應的加載器已經加載了該類的時候,就沒有必有當前classloader再加載一次,直接在parent中的快取找,
  • 安全考慮,防止核心API被篡改,像自己定義一個String.java 這個時候classloader會先加載framework中的String.java(加載只要全限定名一樣,可以偽造構建和系統類一樣的全限定名)

上面我們分析了它的設計思想,回歸加載程序中,這里用到了findClass(name) 方法

findClass(name) 又做了什么?

我們可以通過查看其自身和父類尋找findClass方法,通過查看

   @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //通過pathList 去找 相關 類,而pathList 是  DexPathList 是 final 的 
        Class c = pathList.findClass(name, suppressedExceptions);
        //知道了 回傳 Class相關實體物件,沒找到拋例外 關鍵在上一行代碼 
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

  //上述pathList 宣告 是不可變的 我們去查看其賦值
  private final DexPathList pathList;

     //pathList 又是由什么賦值 可以看到相關使用在構造方法中
     // 通過查看其構造方法,可知我們需要傳入 dexPath 去 實體化 DexPathList
     // 因此PathClassLoader才可以加載到程式的所有類
     /**
     * @hide
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
        //我們跳去 DexPathList 看看
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        if (reporter != null) {
            reportClassLoaderChain();
        }
    }

 // 我們看一下DexPathList
DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {       
            //常規 引數判空 
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }

            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }

        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        // 這里使用到了 dexpath 
        // splitDexPath 將dexpath的多地址分離 , PathClassLoader 可以處理多個dex
        // makeDexElements 依據dexpath生成多個dexElement
        // 下面我們去看一下splitDexPath 和 makeDexElements
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);

        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
    }

 //splitDexPath 方法 
    private static List<File> splitDexPath(String path) {
        return splitPaths(path, false);
    }

    /**
     * Splits the given path strings into file elements using the path
     * separator, combining the results and filtering out elements
     * that don't exist, aren't readable, or aren't either a regular
     * file or a directory (as specified). Either string may be empty
     * or {@code null}, in which case it is ignored. If both strings
     * are empty or {@code null}, or all elements get pruned out, then
     * this returns a zero-element list.
     */
    private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
        List<File> result = new ArrayList<>();

        if (searchPath != null) {
            for (String path : searchPath.split(File.pathSeparator)) {
                if (directoriesOnly) {
                    try {
                        StructStat sb = Libcore.os.stat(path);
                        if (!S_ISDIR(sb.st_mode)) {
                            continue;
                        }
                    } catch (ErrnoException ignored) {
                        continue;
                    }
                }
                result.add(new File(path));
            }
        }

        return result;
    }

    //makePathElements 方法
    private static final String zipSeparator = "!/";
    private static NativeLibraryElement[] makePathElements(List<File> files) {
        // 一個dex 生成一個 elements 物件 
        NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
        int elementsPos = 0;
        for (File file : files) {
            String path = file.getPath();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                File zip = new File(split[0]);
                String dir = split[1];
                elements[elementsPos++] = new NativeLibraryElement(zip, dir);
            } else if (file.isDirectory()) {
                // We support directories for looking up native libraries.
                elements[elementsPos++] = new NativeLibraryElement(file);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

這里,我們了解到了pathList這個物件為了findClass做了什么準備,現在,我們回到BaseClassLoader 中的 findClass 方法中,繼續看pathList這一行代碼,pathList這個實體我們已經知曉了,現在看它呼叫的findClass方法(和BaseClassLoader中不一樣)

    //在這里,我們可以看到 該方法使用到了剛才 pathList 中 dexElements陣列 
    //  通過遍歷 每一個 Element 遍歷所有的dex
    //   DexFile dex = element.dexFile; 拿到對應的 DexFile 
    //  dex.loadClassBinaryName(name, definingContext, suppressed); 
    //  再通過每個dex 的 loadClassBinaryName 去將類找出 
  /**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     */
    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

大致的類加載流程就如上所示

熱修復原理

那么,通過類加載流程,我們這個時候可以在這之上進行一下熱修復

當我們程式有Bug時,我們對相關類進行修改,并將修改后的類打包成補丁dex,當客戶端下載時,只要讓補丁包加載在類加載之前,就可以提前加載到虛擬機中,而有錯誤的類,因為類加載機制問題,不會重復加載,從而不會加載到錯誤類,進而實作修復,

熱修復原理大致代碼流程

        //相關正常修復類打補丁包 
        dx --dex --output=output.dex  xxx.jar
        dx --dex --output=output.dex  類全限定名 

        在Application類中 重寫 attachBaseContext 并在super后 呼叫 熱修復
        并傳入Application,補丁dex檔案至熱修復類

        //熱修復類中 
        //1、獲得classloader(PathClassLoader)
        ClassLoader classLoader = application.getClassLoader();
        //2\. 不同的Android 版本 ,熱修復有所差異,需要對其適配 (以6.0為例)

        //3\. 遍歷該類及超類 
        Class<?> clazz = classLoader.getClass(); clazz != null; clazz = clazz.getSuperclass()
        //4.通過反射 獲取 pathList 屬性成員
        Field field = clazz.getDeclaredField("pathList");
        //5.獲取到pathList物件
        Object dexPathList = pathListField.get(classLoader);

        ///將自己的補丁包轉換為Element陣列 參考PathClassLoader 
        // 得到補丁包的Element陣列 
        // 反射拿到 makePathElements 方法 
        Class<?> clazz = dexPathList.getClass(); clazz != null; clazz = clazz.getSuperclass()
        Method method = clazz.getDeclaredMethod("makePathElements", parameterTypes);
        //反射執行方法 回傳 補丁包對應的 Element陣列 
         Object[] patchElements = (Object[]) method.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions)
       //將原本的 dexElements 與 makePathElements生成的陣列合并,并利用反射塞回 dexElements    

       //拿到 classloader中的dexelements 陣列
       Class<?> clazz = dexPathList.getClass(); clazz != null; clazz = clazz.getSuperclass()
        Field dexElementsField =  clazz.getDeclaredField("dexElements");
        //old Element[]
        Object[] dexElements = (Object[]) dexElementsField.get(dexPathList);

        //合并后的陣列
        Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
                dexElements.length + patchElements.length);

        // 先拷貝新陣列
        System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
        System.arraycopy(dexElements, 0, newElements, patchElements.length, dexElements.length);

        //修改 classLoader中 pathList的 dexelements
        dexElementsField.set(dexPathList, newElements);  

上述只是依據類加載原理進而對熱修復原理進行闡述的大致流程,實作熱修復還需要考慮到一下兼容問題、混淆等內容,

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

標籤:其他

上一篇:Android中順轉跳/逆回傳到多個Activity+堆疊(附含GitHub源代碼下載)

下一篇:算下來,Android開發也已發行多時,移動 App 已經趨近飽和,那么 Android 開發還會有那么吃香嗎?

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