主頁 > 移動端開發 > 019 Android加固之APK加固的原理和實作

019 Android加固之APK加固的原理和實作

2021-10-03 08:41:05 移動端開發

文章目錄

    • 前言
    • 加載Activity遇到的問題
      • APK的啟動程序
      • 替換ClassLoader流程
        • 獲取ActivityThread類物件
        • 獲取AppBindData類物件mBoundApplication
        • 獲取LoadedApk類物件info
        • 獲取info物件中的ClassLoader
    • 設計傀儡dex檔案
    • 手工加固APK
    • 代碼實作APK加固
      • 實作步驟
    • 總結

前言

動態加載dex之后,我們會想說,能不能將整個程式的dex都進行動態加載,如果將加載的dex事先加密,加載前解密,這樣就完成了對程式完整的解密了,但這里面遇到一個問題,那就是Android中很多組件其實是事先在清單檔案中注冊過的,我們需要在不多修改清單檔案的前提下,完成對藏匿在資源中加密的dex檔案,完成了這個程序,也就完成了apk的加固

那做到在不過多修改清單檔案的前提下,完成對藏匿在資源中加密的dex檔案,我們將分為以下幾步完成:

  1. 完成對已注冊的Activity的加載
  2. 找尋apk啟動時最開始啟動的代碼,插入自己的代碼
  3. 設計傀儡dex檔案,啟動apk之后,將傀儡dex代碼替換為目標dex代碼

接下來就是按照這個思路,開始加載Activity

加載Activity遇到的問題

我們已經學會了如何動態加載一個類,那動態加載一個Activity呢?為了能讓Activity免除資源困擾,我們在資源中先創建一個Activity類,資源同樣也建立好,

先看Activity類代碼

package com.example.apkdemo2;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity2 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
    }
}

再看資源檔案

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity2">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第二個Activity"
        ></TextView>
</LinearLayout>

接著將這個Activity反編譯后再轉成dex檔案

在這里插入圖片描述

然后將生成的dex放到assets目錄下,

在這里插入圖片描述

然后洗掉MainActivity2這個類,

我們在這個專案中去加載Activity,因為當前專案已經有Activity2的資源檔案了,這樣就可以不受資源的困擾,難度會低很多,

接著完成動態加載dex的步驟:

  1. 拷貝自定義資源中的dex到程式中
  2. 創建一個DexClassLoader,加載dex
  3. 呼叫加載dex中的class方法

這個步驟我們之前已經完成了

區別在于第三步,這里需要獲取型別別,設定Intent資訊,啟動Activity,代碼如下:

Class clz=null;
        try {

           clz=dexClassLoader.loadClass("com.example.apkdemo2.MainActivity2");

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        Intent intent=new Intent(this,clz);
        startActivity(intent);

完成后我們在界面中增加按鈕,然后在按鈕中呼叫上面的方法,

在這里插入圖片描述

運行程式,點擊按鈕,發現出現了錯誤,報錯資訊如下:

unable to instantiate activity

無法實體化Activity,

我們使用創建了DexClassLoader加載器,加載了我們需要的類,但有一個問題,那就是程式當前的ClassLoader是哪個?答案是PathClassLoader,但是PathClassLoader沒有我們加載的類,

針對這個問題有兩個解決方案

  1. 直接使用PathClassLoader加載我們需要的類
  2. 使用我們自己的ClassLoader替換掉PathClassLoader

第一種方法顯然不行,除非我們能先將dex檔案安裝到apk中,既然都能安裝到apk中了,那就不能做到我們想要做的隱藏效果了,所以排除第一種方案

第二種方法,替換掉PathClassLoader,這種方法應該是可行的,因為只有ClassLoader換了,用對應的ClassLoader肯定能加載對應的類

那怎么才能替換掉ClassLoader呢?需要我們進一步分析APK的啟動程序,找到保存ClassLoader的變數,變數所在的類,使用反射修改,

接下來從分析APK的啟動程序開始

APK的啟動程序

關于APK的啟動程序,這里推薦一篇文章:

《Android應用程式啟動程序源代碼分析》https://blog.csdn.net/Luoshengyang/article/details/6689748

這篇文章詳細的分析了Android應用程式的啟動程序,這里就不再贅述,做一個簡單總結

Step 1. Launcher.startActivitySafely
Step 2. Activity.startActivity
Step 3. Activity.startActivityForResult
Step 4. Instrumentation.execStartActivity
Step 5. ActivityManagerProxy.startActivity
Step 6. ActivityManagerService.startActivity
Step 7. ActivityStack.startActivityMayWait
Step 8. ActivityStack.startActivityLocked
Step 9. ActivityStack.startActivityUncheckedLocked
Step 10. Activity.resumeTopActivityLocked
Step 11. ActivityStack.startPausingLocked
Step 12. ApplicationThreadProxy.schedulePauseActivity
Step 13. ApplicationThread.schedulePauseActivity
Step 14. ActivityThread.queueOrSendMessage
Step 15. H.handleMessage
Step 16. ActivityThread.handlePauseActivity
Step 17. ActivityManagerProxy.activityPaused
Step 18. ActivityManagerService.activityPaused
Step 19. ActivityStack.activityPaused
Step 20. ActivityStack.completePauseLocked
Step 21. ActivityStack.resumeTopActivityLokced
Step 22. ActivityStack.startSpecificActivityLocked
Step 23. ActivityManagerService.startProcessLocked
Step 24. ActivityThread.main
Step 25. ActivityManagerProxy.attachApplication
Step 26. ActivityManagerService.attachApplication
Step 27. ActivityManagerService.attachApplicationLocked
Step 28. ActivityStack.realStartActivityLocked
Step 29. ApplicationThreadProxy.scheduleLaunchActivity
Step 30. ApplicationThread.scheduleLaunchActivity
Step 31. ActivityThread.queueOrSendMessage
Step 32. H.handleMessage
Step 33. ActivityThread.handleLaunchActivity
Step 34. ActivityThread.performLaunchActivity
Step 35. MainActivity.onCreate

apk的啟動程序相對比較復雜,我們的分析目的是為了找到PathClassLoader,所以不需要對每一個步驟進行詳細了解,整個啟動程序可以簡化為下面的步驟

Step 2. Activity.startActivity--->CreateProcess
......
Step 24. ActivityThread.main----->入口點
......
Step 35. MainActivity.onCreate--->main函式

用Windows系統來對比參照的話,Step 2可以看作是創建行程,一直到Step 24中間的步驟就等于是創建行程的準備作業,而真正的程式入口則是ActivityThread.main,相當于Windows的程式入口,接著Step 35才是真正執行用戶代碼的地方,相當于是main函式了

然后就可以進入Android原始碼網站http://androidxref.com/,開始分析app啟動程序

在這里插入圖片描述

另外一種分析的方法就是在onCreate函式下斷,然后查看呼叫堆疊

替換ClassLoader流程

獲取ActivityThread類物件

這個原始碼分析起來還是比較吃力的,這里直接看結論,替換ClassLoader的流程如下

在這里插入圖片描述

首先獲取ActivityThread型別別,然后呼叫這個類的currentActivityThread方法,目的是為了拿到sCurrentActivityThread物件

在這里插入圖片描述

 private static ActivityThread sCurrentActivityThread;

sCurrentActivityThread是ActivityThread類的類物件,拿到了這個類的類物件,我們就可以操作整個類的資料

獲取AppBindData類物件mBoundApplication

在這里插入圖片描述

然后.通過類物件獲取成員變數mBoundApplication

獲取LoadedApk類物件info

在這里插入圖片描述

獲取到了AppBindData的類物件以后,就可以拿到AppBindData的類物件內的成員變數info,也就是LoadedApk類物件

獲取info物件中的ClassLoader

在這里插入圖片描述

接著就能獲取到LoadedApk類物件內的ClassLoader,最后需要將這一段替換ClassLoader的邏輯轉換成代碼,就解決了這個問題,

完整代碼如下:

 //替換app啟動時的classloader為加載的dexclassloader
    private void replaceClassLoader(DexClassLoader dexClassLoader) {

        try {

            //1.獲取ActivityThread類物件
            //獲取型別別
            Class clzActivityThread=Class.forName("android.app.ActivityThread");

            //獲取類方法
            Method methodcurrentActivityThread=clzActivityThread.getDeclaredMethod("currentActivityThread");

            //呼叫方法
            Object objectActivityThread = methodcurrentActivityThread.invoke(null,new Object[]{});
            //2.通過類物件獲取成員變數mBoundApplication

            //獲取欄位
            Field fieldmBoundApplication=clzActivityThread.getDeclaredField("mBoundApplication");
            //取消訪問檢查
            fieldmBoundApplication.setAccessible(true);
            //獲取欄位的值
            Object objBoundApplication= fieldmBoundApplication.get(objectActivityThread);

            //3.獲取mBoundApplication物件中的成員變數info
            //獲取型別別
            Class clzAppBindData=Class.forName("android.app.ActivityThread$AppBindData");
            //獲取欄位
            Field fieldInfo=clzAppBindData.getDeclaredField("info");
            fieldInfo.setAccessible(true);
            //獲取欄位的值
            Object objInfo = fieldInfo.get(objBoundApplication);

            //4.獲取info物件中的mClassLoader
            //獲取型別別
            Class clzLoadedApk=Class.forName("android.app.LoadedApk");
            //獲取欄位
            Field fieldmClassLoader=clzLoadedApk.getDeclaredField("mClassLoader");
            fieldmClassLoader.setAccessible(true);

            //設定欄位 替換ClassLoader
            fieldmClassLoader.set(objInfo,dexClassLoader);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

設計傀儡dex檔案

經過對APK啟動的分析,可知在APK啟動時在創建MainActivity前,會創建Application物件,呼叫其attachBaseContext方法,以及onCreate方法 ,所以我們設計的傀儡dex只需要先原APK清單檔案中添加Application的節點,創建一個MyApplication類,實作attachBaseContext方法以及onCreate方法,在這兩個方法中初始化原dex檔案,加載dex檔案即可

我們可以在attachBaseContext方法,加載源dex檔案回傳dex加載器,替換系統默認的加載器,然后剩下的交給系統處理即可

先撰寫傀儡Application代碼,繼承自Application,重寫attachBaseContext方法和onCreate方法

package com.example.dummydex;

import android.app.Application;
import android.content.Context;


public class DummyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //code
    }


    @Override
    public void onCreate() {
        super.onCreate();
        //code
    }
}

在attachBaseContext方法中添加加載源dex檔案和替換ClassLoader的代碼

protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //拷貝自定義資源中的dex檔案到程式目錄下
        String path=CopyDex("src.dex");

        //創建一個DexClassLoader 加載dex
        DexClassLoader dexClassLoader=GetLoader(path);

        //替換ClassLoader
        replaceClassLoader(dexClassLoader);
    }

代碼中的三個方法和之前的一致,到此DummyApplication類已經寫完,注意需要將DummyApplication類的完整類(帶包名)寫入待加密的清單檔案

使用smali.jar將DummyApplication類編譯成class.dex

在這里插入圖片描述

至此,傀儡dex檔案生成完畢

手工加固APK

有了以上的準備,我們基本上可以手工完成對一個APK的加固,大致步驟:

  1. 獲取待加固的apk,隨便寫一個hello world
  2. 使用apktool反編譯apk,修改AndroidManifest.xml,添加DummyApplication類資訊,讓程式運行的第一個類為DummyApplication類
  3. 將源classes.dex放入assets目錄,修改檔案名為src.dex
  4. 使用apktool重打包修改后的資訊
  5. 替換重打包后的classes.dex為傀儡dex
  6. 為新打包的apk進行簽名

實際操作如下:

在這里插入圖片描述

java -jar .\apktool.jar d .\test.apk

首先用apktool將需要加固的apk進行反編譯,反編譯后會生成檔案夾

在這里插入圖片描述

然后修改清單檔案,在application中添加name屬性,類名為dummydex的完整包名+類名

在這里插入圖片描述

修改完成之后 新建assets檔案夾,將apk原本的dex檔案放到assets檔案夾下,然后修改名稱為src.dex

在這里插入圖片描述

java -jar .\apktool.jar b .\test -o app.apk

再將apk進行回編譯,如果回編譯的時候報了下面的錯誤

No resource identifier found for attribute ‘compileSdkVersion’ in package ‘android’

那么需要執行一下這條命令

java -jar apktool.jar empty-framework-dir --force

清空一下framework目錄

在這里插入圖片描述

然后打開壓縮包,將原來的classes.dex洗掉,然后把dummy.dex放到apk里面

在這里插入圖片描述

并修改為classes.dex,最近將apk打上簽名,整個加固程序就完成了

在這里插入圖片描述

最后安裝,運行成功,那么整個加固程序就是成功的,

在這里插入圖片描述

此時我們再用AndroidKiller進行反編譯,MainActivity已經無法找到

在這里插入圖片描述

工程檔案中只有DummyApplication.smali檔案,到此就完成了整個手工加固的程序

代碼實作APK加固

實作步驟

根據手工加固APK的步驟得出將其轉出代碼的步驟:

  1. 獲取待加密的apk路徑
  2. 呼叫apktool,反編譯目標apk
  3. 修改AndroidManifest.xml添加DummyApplication的資訊
  4. 將源classes.dex復制到assets目錄,修改檔案名為src.dex
  5. 呼叫apktool重新打包,生成新的apk
  6. 修改新的apk中的calsses.dex將其替換為傀儡dex或者將反編譯后的smali代碼替換為傀儡dex的smali代碼
  7. 為新的apk進行簽名

主要流程代碼如下:

public static void Pack(){
        //需要反編譯的apk檔案名
        String fileName="test.apk";
        //獲取當前路徑
        String currentdir=System.getProperty("user.dir");

        //構造全路徑
        String filepath=currentdir+ File.separator+fileName;

        //去掉擴展名
        String NoExtenDir=StringsUtils.getFileNameNoEx(filepath);

        //運行apktool 反編譯apk
        System.err.println("1.反編譯apk...");
        CMDUtils.runCMD("java -jar apktool.jar d "+filepath);
        System.err.println("1.反編譯apk完成...");

        //修改xml檔案
        String xmlPath=NoExtenDir+File.separator+"AndroidManifest.xml";
        System.err.println("2.修改清單檔案...");
        XMLUtils.ChagenApplication(xmlPath);
        System.err.println("2.修改清單檔案完成...");

        //拷貝源dex到assets目錄
        String assetsDir=NoExtenDir+ File.separator+"assets";
        System.err.println("3.拷貝源dex到assets目錄...");
        FileUtils.copyFileFromZip(filepath,assetsDir);
        System.err.println("3.拷貝源dex到assets目錄完成...");

        //洗掉smali檔案 拷貝dummydex的smali
        String smaliDir=NoExtenDir+ File.separator+"smali";
        System.err.println("4.洗掉smali檔案 拷貝dummydex的smali...");
        FileUtils.deleteFolder(smaliDir);

        String oldDir=currentdir+ File.separator+"dummySmali";
        FileUtils.copyFolder(oldDir,smaliDir);
        System.err.println("4.洗掉smali檔案 拷貝dummydex的smali完成...");

        //重打包
        System.err.println("5.重打包apk...");
        CMDUtils.runCMD("java -jar apktool.jar b "+NoExtenDir+" -o pack.apk");
        System.err.println("5.重打包apk完成...");

        //簽名
        System.err.println("6.對apk進行簽名...");
        String outpath =currentdir+File.separator+"pack.apk";
        String signPath =currentdir+File.separator+"pack_sigin.apk";
        CMDUtils.runCMD("java -jar signapk.jar testkey.x509.pem testkey.pk8 "+outpath+" "+signPath,"sign");
        System.err.println("6.對apk進行簽名完成...");


        //收尾作業
        System.err.println("7.收尾作業...");
        FileUtils.deleteFolder(NoExtenDir);
        File file=new File(outpath);
        file.delete();
        System.err.println("7.收尾作業完成...");

        //輸出檔案路徑
        System.out.println("8. 已加固檔案:"+signPath);

    }

運行效果如圖:

在這里插入圖片描述

運行完成之后,

在這里插入圖片描述

APK加固也成功了,完整工程代碼如下:

https://download.csdn.net/download/qq_38474570/23560575?spm=1001.2014.3001.5503

總結

通過回顧思路,寫代碼對APK加固有了一定的認識,在完全實作自動化的那一刻,感嘆程式的魅力,不過這只是一個Demo,還有很多可以完善的地方,比如記憶體加載dex檔案,合并源dex和傀儡dex等等,

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

標籤:其他

上一篇:現代圖片選擇器(PHPicker)在 SwiftUI 應用

下一篇:Arouter實作界面跳轉和傳參

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