主頁 > 移動端開發 > Qt for Android(九) ——APP 崩潰卡死拉起保活實戰

Qt for Android(九) ——APP 崩潰卡死拉起保活實戰

2021-01-02 11:43:03 移動端開發

這篇文章要基于前面的基礎,我們才能繼續下面的內容,建議閱讀,

Qt for Android(一) —— QT 中如何呼叫android方法
Qt for Android(二) —— QT 中呼叫自定義Android方法詳細教程(獲取Android設備的SN號)

背景

首先,本文的案例環境基于一些特殊的 android 設備,比如瑞星微的RK系列,在該設備上不會熄屏,沒有鎖屏鍵,運行的應用也僅限于幾個 APP,大部分不會存在應用被系統殺死的可能,

應用拉起說白了就是行程保活,關于Android 的行程保活文章有很多,但是本文是基于 QT for Android 的開發,因此程序可能有些許不同,同時針對的場景也不同,因此在操作上可能更有針對性,

由于我們的應用屬于廣告播放類 APP, 需要長時間的穩定運行,但不可避免的由于某種原因 APP 發生崩潰或者界面卡死,為了盡可能的減小損失,因此我們需要在發生上述情況時重新啟動我們的APP,

分析

假設我們的主應用稱為A,而為了做到行程保活,我們需要另一個行程B,稱之為Monitor,即監視行程,也可以稱為守護行程(“守護”,這個詞在2020年顯得很特別),這決定了我們的方案需要安裝兩個應用,

方法和思路:

  1. A啟動后向B發送登錄請求,建立通信,B開啟定時器,開始監測A的資料,通信的實作方式不限,可以是socket,或者廣播
  2. 通信建立后A立即開始向B發送心跳,每1s一個心跳包,
  3. 假如發生崩潰,B沒有收到A的心跳包,則重新拉起A,
  4. 假如發生卡死,B沒有收到A的心跳包,則重新拉起A,
  5. 正常退出A的時候向B發送登出請求,停止心跳,防止B誤以為A死亡而被拉起,

其實思路很簡單,但是其實在開發的時候碰到一個問題,QT的事件回圈和Android的事件回圈互不干擾,即QT的卡死不會影響到Android層的事件,為了解決這個問題,就往下看具體的代碼,

代碼詳述

應用B之MonitorServices:

package com.qht.b;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Calendar;
import java.util.Timer;
import java.util.TimerTask;

public class MonitorService extends Service {
    public static final String CLASS_NAME = "MonitorService";
    private Thread thread;
    private DatagramSocket socket = null;
    private Context m_context;
    private long lastTimeMillis = 0; //代表了最后一次收到A應用心跳包的時間戳
    Timer timer = null;
    TimerTask task;
    public MonitorService() {
    }
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(CLASS_NAME, "onBind !!");
        return null;
    }
 @Override
    public void onCreate() {
        super.onCreate();
        m_context = this;
        Log.d(CLASS_NAME, "onCreate !!");
        lastTimeMillis = 0;
        thread=new Thread(new Runnable()
        {
            @Override

           public void run()
            {
                try {
                    System.out.println("監聽埠16667");
                    socket = new DatagramSocket(16667);
                    socket.setSoTimeout(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                while (true) {
                  byte data[] = new byte[1024];
                    DatagramPacket packet = new DatagramPacket(data, data.length);
                    try {
                        socket.receive(packet);
                    } catch (SocketTimeoutException e) {
                        System.out.println("socket 10s 超時:" + e.getMessage());
                    } catch (SocketException e) {
                        System.out.println("socket SocketException:" + e.getMessage());
                       e.printStackTrace();
                    } catch (IOException e) {
                        System.out.println("socket IOException:" + e.getMessage());
                        e.printStackTrace();
                    }
                    String result = new String(packet.getData(), packet.getOffset(), packet.getLength());
                    //校驗包
                    if (result.equals("hertbeat"))
                   {
                        lastTimeMillis =  System.currentTimeMillis();
                        System.out.println("rec : hertbeat");
                    }else if (result.equals("login"))
                    {
                      //login
                        lastTimeMillis =  System.currentTimeMillis();
                        startTimer();
                        System.out.println("rec : login");
                    } else if (result.equals("logout"))
                    {
                                           //退出取消,等待login再開啟
                        System.out.println("rec : logout timer.cancel()");
                        stopTimer();
                    } else if (result.equals("anr"))
                    {
                        //退出取消,等待login再開啟
                        System.out.println("rec : anr restartApp");
                        restartApp();
                    }
                }
            }
        });
        thread.start();
    }
  @Override
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
        Log.d(CLASS_NAME, "onStart !!");
    }
   private void startTimer(){
        if (timer == null) {
            timer = new Timer();
        }

        if (task == null) {
            task = new TimerTask() {
                @Override
                public void run() {
                    System.out.println("run TimerTask");
                    if (lastTimeMillis != 0 &&  System.currentTimeMillis()- lastTimeMillis > 2000)
                    {
                        System.out.println("失去心跳,拉起APPlastTimeMillis :" + lastTimeMillis + ":" + (Calendar.getInstance().getTimeInMillis() - lastTimeMillis) );
                        //  心跳超時,殺死并拉起
                        restartApp();
                    }
                                    }
            };
        }
        if(timer != null && task != null )
            timer.schedule(task,0,1000);
    }
   private void restartApp() {
        killProcess(ConstantUtil.PACKAGE_NAME);
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        PackageManager localObject = m_context.getPackageManager();
        if (PackageUtil.checkPackInfo(m_context, ConstantUtil.PACKAGE_NAME)) {
            Log.i(CLASS_NAME, "find package, ready to lanunch! "+ConstantUtil.PACKAGE_NAME);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
                Log.i(CLASS_NAME, "Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE "+ (int)Build.VERSION.SDK_INT);
                m_context.startActivity((localObject).getLaunchIntentForPackage(ConstantUtil.PACKAGE_NAME));
          }
        } else {
            Log.i(CLASS_NAME, "not find package!" + ConstantUtil.PACKAGE_NAME);
        }
        lastTimeMillis = 0;
        stopTimer();
    }
   private void stopTimer(){
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
        if (task != null) {
            task.cancel();
            task = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(CLASS_NAME, "onDestroy !!");
        stopTimer();
    }
        /**
     * 結束行程
     */
    private void killProcess(String packageName) {
        Process process = Runtime.getRuntime().exec("su");
        OutputStream out = process.getOutputStream();
        String cmd = "am force-stop " + packageName + " \n";
        try {
            out.write(cmd.getBytes());
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在 Monitor內部,我們維護了一個定時器Timer,需要不停的檢測A應用的心跳資料,44行我們首先開啟一個作業執行緒去監聽一個udp埠,我這邊采用的是udp通信,因為我只需要收到A應用的心跳即可,由于socket的receive函式是阻塞式的,因此我們在執行緒內部開啟while回圈接受資料,收到的資料型別分為4種:

login:
代表A應用上線,這個時候我們開啟定時器即可,
logout:
代表A應用下線,這個時候我們關閉定時器即可,
hertbeat:
代表A應用發送的心跳資料,這個時候我們主需要不停的更新 lastTimeMillis (代表了最后一次收到A應用心跳包的時間戳)這個值即可,
anr:
代表A應用發生卡死,這個時候我們需要呼叫restartApp方法強制殺死A應用并重啟它,

當然,services不能自己啟動,需要一個activity去啟動它,同時也要注冊到manifest檔案中,

        //啟動
        Intent intent = new Intent(MainActivity.this, MonitorService.class);
        startService(intent);
        <service android:name="com.qht.b.MonitorService" >
        </service>

上面就是我們MonitorServices的全部內容,再來梳理下它的作業:

  1. 監聽login請求,并開啟心跳檢測,
  2. 隨時注意心跳是否斷開,斷開則拉起A應用,
  3. 監聽logout請求,避免定時器空跑,保證A的正常退出,而不是當做崩潰處理,
  4. 監聽anr訊息,收到anr訊息則重啟A應用,

應用A之TestApp:

最開始我是將A應用通信的代碼放到Android的Service中的,但是經過測驗,在頻繁的崩潰拉起后,有時候會出現拉起失敗的情況,具體原因和A應用包含的服務有關,而通過之前的文章我們已經知道了我們的QT程式都有一個入口Activity,因此我將通信的代碼放到了這個入口Activity中,

package com.qht.a;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.view.WindowManager;
import android.view.KeyEvent;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;


public class MainActivity extends org.qtproject.qt5.android.bindings.QtActivity {

    DatagramSocket socket= null;
    InetAddress serverAddress = null;
    private boolean isStop = false;//logout,停止心跳
    private int lasttick, mTick;//兩次計數器的值
    private Handler mHandler = new Handler();
    private boolean isNotAnr = true;//是否anr標識
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        anrDetection();
        loginAndHert();
    }
private  void loginAndHert() {
  System.out.println("開始 loginAndHert");
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(300);
                        if (socket == null)
                        {
                            System.out.println("系結 16666 埠");
                            socket = new DatagramSocket(16666);
                        }
  						System.out.println("udp 使用回環地址 : 127.0.0.1");
                        serverAddress = InetAddress.getByName("127.0.0.1");
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    } catch (SocketException e) {
                        e.printStackTrace();
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
 					String sendData = "login";
					 byte data[] = sendData.getBytes();
                    DatagramPacket packet = new DatagramPacket(data, data.length, serverAddress, 16667);
                    System.out.println("發送給 16667 埠,被monitor服務監聽");
                    try {
                        socket.send(packet);
                        System.out.println("socket.send:" + sendData + ",登錄后300ms,每隔1s發送一次心跳包");
                        Thread.sleep(300);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
 					//上線后發送心跳
                    while (!isStop) {
                        try {
                            String sendData2 = "hertbeat";
                            byte data2[] = sendData2.getBytes();
                            DatagramPacket packet2 = new DatagramPacket(data2, data2.length, serverAddress, 16667);
                            socket.send(packet2);
                            System.out.println("socket.send:" + sendData2);
                            Thread.sleep(1000);
							 } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
            System.out.println("按下了back鍵   onKeyDown() send logout,500ms after System.exit(0)");
            logout();
            return false;
        }else {
            return super.onKeyDown(keyCode, event);
        }
    }
  private  void logout(){
        System.out.println("退出 MainActivity");
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
 					isStop = true;
                    String sendData = "logout";
                    byte data[] = sendData.getBytes();
                    DatagramPacket packet = new DatagramPacket(data, data.length, serverAddress, 16667);
                    socket.send(packet);
                    System.out.println("socket.send:" + sendData);
  					Thread.sleep(500);
                    System.exit(0);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

 /*
    * 卡死監測原理描述:利用service中的執行緒向主執行緒發送mTick+1,然后執行緒睡眠5s后,再去檢測這個值是否被改變,沒改變的話說明主執行緒卡死了,主執行緒卡死后直接退出行程,等待最多2s后monitor拉起
    * */
    private void anrDetection() {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (isNotAnr) {
                    lasttick = mTick;
                    mHandler.post(tickerRunnable);//向主執行緒發送訊息 計數器值+1
 				try {
                        Thread.sleep(8000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(" mTick :" + mTick + "lasttick:" + lasttick);
                    if (mTick == lasttick) {
 						isNotAnr = false;
                        Log.e("QHT", "anr happned in here");
                        try {
                            handleAnrError();
                        } catch (SocketException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }
 //發生anr的時候,在此處寫邏輯
    private void handleAnrError() throws SocketException {
        System.out.println("ANR exit ,wait monitor 拉起");
        System.exit(0);
    }

    private final Runnable tickerRunnable= new Runnable() {
        @Override
        public void run() {

            mTick = (mTick + 1) % 10;
        }
    };
}

在上面30行的時候我們從Activity的onCreate方法開始,也就是從應用A啟動那一刻開始,就呼叫loginAndHert方法向應用B發送login請求,因為應用A不需要接受資料,因此無法確認login是否發送成功,但是使用回環地址不會存在失敗的情況,因此我們延遲300ms后再去每一秒發送一次心跳,

在MainActivity中我們也監聽了回傳鍵,當收到回傳鍵時我們認為應用被正常退出,因此我們呼叫了logout方法,告訴MonitorServices程式是正常退出的,

到這兒,其實就已經完成了應用A和MonitorServices的基礎通信了,假如此時應用A突發崩潰,則自然而然的沒有心跳包了,MonitorServices就會拉起應用A,

沒錯,關于崩潰拉起的作業算是完了,但是Android Activity ANR呢? QT程式block呢?

重點

oncreate()方法中,我們還呼叫了一個anrDetection()方法,這便是我們Android層的ANR檢測方法,它的原理是這樣的:

在應用一開始UI執行緒中初始化兩個變數tick1和tick2為同一個值,然后開啟一個作業執行緒,并向UI執行緒post一個tick1的+1請求,tick2不變,然后作業執行緒sleep幾秒鐘,模擬anr的發生,sleep結束后,再去判斷這兩個值是否相等,如果相等,則說明tick1沒有被+1,也就是說主執行緒沒有處理這個+1請求,那必然是主執行緒卡住了,則我們認為此時應用發生了ANR;若這兩個值不相等,或者說tick1=tick2+1,則說明主執行緒處理了這個+1請求,主執行緒作業正常,,程式繼續運行,

在上面的代碼中我偷懶了,當發生anr時我強制通過system.exit函式退出行程,然后MonitorServices檢測不到心跳了就會拉起應用A,其實在這兒也可以向MonitorServices發送一個"anr"訊息,讓MonitorServices主動去處理,

上面的代碼解決了我們QT程式 Android 層的卡死問題,但往往這是不多見的,因為這個Activity沒有什么高負荷的作業,一般是不會卡死的,出問題總是會出在我們的QT程式內部,碰巧的是,QT程式內部卡死,MainActivity卻不會卡死,即呼應了我上面提到的兩者的事件回圈是獨立的,

但我認為,這個檢測卡死的思想是想通的,因此我嘗試將anrDetection()方法移植到QT程式中,發現完全可行,

void AndroidDaemonMonitor::start()
{
     qDebug() << "QHT udp client thread start";
    m_thread = std::thread([this]()
    {
        while (isNotAnr) {
            qDebug() << "QHT isNotAnr threadID:" << QThread::currentThreadId();
            lasttick = mTick;
            emit signalTickChange();
            std::this_thread::sleep_for(std::chrono::milliseconds(8000));
            qDebug() << "QHT  mTick :" <<  mTick << ",lasttick:" << lasttick;
            if (mTick == lasttick) {
                isNotAnr = false;
                qDebug() << "QHT anr happned in here";
                std::string str = "anr";
                int sendres = m_udpClient->send(str.data(), str.length(), "127.0.0.1", 16667);
                std::this_thread::sleep_for(std::chrono::milliseconds(300));
            }
        }
    });
    m_thread.detach();
}

void AndroidDaemonMonitor::slotTickChange()
{
    qDebug() << "QHT slotTickChange threadID:" << QThread::currentThreadId();
   //向主執行緒發送訊息 計數器值+1
   mTick = (mTick + 1) % 10;
}

看見沒,兩者的代碼幾乎是一樣的,不同的是,使用QT中的信號槽取代了Android中的handler.post方法,但都是在主執行緒中去執行+1操作,

在 QT 代碼中,我沒有像android那樣直接退出程式,比如:qApp.exit() 等,因為你會發現根本退不了,因此我只能向MonitorServices發送"anr"訊息,等待MonitorServices殺死并重啟應用A,

下面我附上本次測驗的兩個APP原始碼,希望對你有所幫助,如有問題,添加我的微信,q2nAmor,歡迎交流,

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

標籤:其他

上一篇:干貨

下一篇:干貨-ubuntu 16.04

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