主頁 > 移動端開發 > 能否讓APP永不崩潰—小光與我的對決

能否讓APP永不崩潰—小光與我的對決

2020-12-11 08:36:55 移動端開發

前言

關于攔截例外,想必大家都知道可以通過Thread.setDefaultUncaughtExceptionHandler來攔截App中發生的例外,然后再進行處理,

于是,我有了一個不成熟的想法,,,

讓我的APP永不崩潰

既然我們可以攔截崩潰,那我們直接把APP中所有的例外攔截了,不殺死程式,這樣一個不會崩潰的APP用戶體驗不是杠杠的?

  • 有人聽了搖搖頭表示不贊同,這不小光跑來問我了:

“老鐵,出現崩潰是要你解決它不是掩蓋它!!”

  • 我拿把扇子扇了幾下,有點冷但是故作鎮定的說:

“這位老哥,你可以把例外上傳到自己的服務器處理啊,你能拿到你的崩潰原因,用戶也不會因為例外導致APP崩潰,這不挺好?”

  • 小光有點生氣的說:

“這樣肯定有問題,聽著就不靠譜,哼,我去試試看”

小光的實驗

于是小光按照網上一個小博主—積木的文章,寫出了以下捕獲例外的代碼:

//定義CrashHandler
class CrashHandler private constructor(): Thread.UncaughtExceptionHandler {
    private var context: Context? = null
    fun init(context: Context?) {
        this.context = context
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(t: Thread, e: Throwable) {}

    companion object {
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler() }
    }
}

//Application中初始化
class MyApplication : Application(){
    override fun onCreate() {
        super.onCreate()
        CrashHandler.instance.init(this)
    }
}

//Activity中觸發例外
class ExceptionActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exception)
        
        btn.setOnClickListener {
            throw RuntimeException("主執行緒例外")
        }
        btn2.setOnClickListener {
            thread {
                throw RuntimeException("子執行緒例外")
            }
        }
    }
}

小光一頓操作,寫下了整套代碼,為了驗證它的猜想,寫了兩種觸發例外的情況:子執行緒崩潰和主執行緒崩潰

  • 運行,點擊按鈕2,觸發子執行緒例外崩潰:

“咦,還真沒啥影響,程式能繼續正常運行”

  • 然后點擊按鈕1,觸發主執行緒例外崩潰:

“嘿嘿,卡住了,再點幾下,直接ANR了”

主執行緒崩潰

“果然有問題,但是為啥主執行緒會出問題呢?我得先搞懂再去找老鐵對峙,”

小光的思考(例外原始碼分析)

首先科普下java中的例外,包括運行時例外非運行時例外

  • 運行時例外,是RuntimeException類及其子類的例外,是非受檢例外,比如系統例外或者是程式邏輯例外,我們常遇到的有NullPointerException、IndexOutOfBoundsException等,遇到這種例外,Java Runtime會停止執行緒,列印例外,并且會停止程式運行,也就是我們常說的程式崩潰,

  • 非運行時例外,是屬于Exception類及其子類,是受檢例外,RuntimeException以外的例外,這類例外在程式中必須進行處理,如果不處理程式都無法正常編譯,比如NoSuchFieldException,IllegalAccessException這種,

ok,也就是說我們拋出一個RuntimeException例外之后,所在的執行緒會被停止,如果主執行緒中拋出這個例外,那么主執行緒就會被停止,所以APP就會卡住無法正常操作,時間久了就會ANR,而子執行緒崩潰了并不會影響主執行緒也就是UI執行緒的操作,所以用戶還能正常使用,

這樣好像就說的通了,

等等,那為什么遇到setDefaultUncaughtExceptionHandler就不會崩潰了呢?

我們還得從例外的原始碼開始說起:

一般情況下,一個應用中所使用的執行緒都是在同一個執行緒組,而在這個執行緒組里只要有一個執行緒出現未被捕獲例外的時候,JAVA 虛擬機就會呼叫當前執行緒所在執行緒組中的 uncaughtException()方法,

// ThreadGroup.java
  private final ThreadGroup parent;

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

parent表示當前執行緒組的父級執行緒組,所以最后還是會呼叫到這個方法中,接著看后面的代碼,通過getDefaultUncaughtExceptionHandler獲取到了系統默認的例外處理器,然后呼叫了uncaughtException方法,那么我們就去找找本來系統中的這個例外處理器——UncaughtExceptionHandler

這就要從APP的啟動流程說起了,之前也說過,所有的Android行程都是由zygote行程fork而來的,在一個新行程被啟動的時候就會呼叫zygoteInit方法,這個方法里會進行一些應用的初始化作業:

    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
        if (RuntimeInit.DEBUG) {
            Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        //日志重定向
        RuntimeInit.redirectLogStreams();
        //通用的配置初始化  
        RuntimeInit.commonInit();
        // zygote初始化
        ZygoteInit.nativeZygoteInit();
        //應用相關初始化
        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
    }

而關于例外處理器,就在這個通用的配置初始化方法當中:

    protected static final void commonInit() {
        if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

       //設定例外處理器
        LoggingHandler loggingHandler = new LoggingHandler();
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

        //設定時區
        TimezoneGetter.setInstance(new TimezoneGetter() {
            @Override
            public String getId() {
                return SystemProperties.get("persist.sys.timezone");
            }
        });
        TimeZone.setDefault(null);

        //log配置
        LogManager.getLogManager().reset();
        //***    

        initialized = true;
    }

找到了吧,這里就設定了應用默認的例外處理器——KillApplicationHandler

private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
        private final LoggingHandler mLoggingHandler;

        
        public KillApplicationHandler(LoggingHandler loggingHandler) {
            this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
        }

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
                ensureLogging(t, e);
                //...    
                // Bring up crash dialog, wait for it to be dismissed
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } catch (Throwable t2) {
                if (t2 instanceof DeadObjectException) {
                    // System process is dead; ignore
                } else {
                    try {
                        Clog_e(TAG, "Error reporting crash", t2);
                    } catch (Throwable t3) {
                        // Even Clog_e() fails!  Oh well.
                    }
                }
            } finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
        }

        private void ensureLogging(Thread t, Throwable e) {
            if (!mLoggingHandler.mTriggered) {
                try {
                    mLoggingHandler.uncaughtException(t, e);
                } catch (Throwable loggingThrowable) {
                    // Ignored.
                }
            }
        }

看到這里,小光欣慰一笑,被我逮到了吧,在uncaughtException回呼方法中,會執行一個handleApplicationCrash方法進行例外處理,并且最后都會走到finally中進行行程銷毀,Try everything to make sure this process goes away,所以程式就崩潰了,

關于我們平時在手機上看到的崩潰提示彈窗,就是在這個handleApplicationCrash方法中彈出來的,不僅僅是java崩潰,還有我們平時遇到的native_crash、ANR等例外都會最后走到handleApplicationCrash方法中進行崩潰處理,

另外有的朋友可能發現了構造方法中,傳入了一個LoggingHandler,并且在uncaughtException回呼方法中還呼叫了這個LoggingHandleruncaughtException方法,難道這個LoggingHandler就是我們平時遇到崩潰問題,所看到的崩潰日志?進去瞅瞅:

private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
        public volatile boolean mTriggered = false;

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            mTriggered = true;
            if (mCrashing) return;

            if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
                Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
            } else {
                StringBuilder message = new StringBuilder();
                message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
                final String processName = ActivityThread.currentProcessName();
                if (processName != null) {
                    message.append("Process: ").append(processName).append(", ");
                }
                message.append("PID: ").append(Process.myPid());
                Clog_e(TAG, message.toString(), e);
            }
        }
    }

    private static int Clog_e(String tag, String msg, Throwable tr) {
        return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
    }

這可不就是嗎?將崩潰的一些資訊——比如執行緒,行程,行程id,崩潰原因等等通過Log列印出來了,來張崩潰日志圖給大家對對看:

崩潰日志圖

好了,回到正軌,所以我們通過setDefaultUncaughtExceptionHandler方法設定了我們自己的崩潰處理器,就把之前應用設定的這個崩潰處理器給頂掉了,然后我們又沒有做任何處理,自然程式就不會崩潰了,來張總結圖,

崩潰呼叫圖

小光又來找我對峙了

  • 搞清楚這一切的小光又來找我了:

“老鐵,你瞅瞅,這是我寫的Demo和總結的資料,你那套根本行不通,主執行緒崩潰就GG了,我就說有問題吧”

  • 我繼續故作鎮定

“老哥,我上次忘記說了,只加這個UncaughtExceptionHandler可不行,還得加一段代碼,發給你,回去試試吧”

    Handler(Looper.getMainLooper()).post {
        while (true) {
            try {
                Looper.loop()
            } catch (e: Throwable) {
            }
        }
    }

“這,,能行嗎”

小光再次的實驗

小光把上述代碼加到了程式里面(Application—onCreate),再次運行:

我去,真的沒問題了,點擊主執行緒崩潰后,還是可以正常操作app,這又是什么原理呢?

小光的再次思考(攔截主執行緒崩潰的方案思想)

我們都知道,在主執行緒中維護著Handler的一套機制,在應用啟動時就做好了Looper的創建和初始化,并且呼叫了loop方法開始了訊息的回圈處理,應用在使用程序中,主執行緒的所有操作比如事件點擊,串列滑動等等都是在這個回圈中完成處理的,其本質就是將訊息加入MessageQueue佇列,然后回圈從這個佇列中取出訊息并處理,如果沒有訊息處理的時候,就會依靠epoll機制掛起等待喚醒,貼一下我濃縮的loop代碼:

    public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); 
            msg.target.dispatchMessage(msg);
        }
    }

一個死回圈,不斷取訊息處理訊息,再回頭看看剛才加的代碼:

    Handler(Looper.getMainLooper()).post {
        while (true) {
            //主執行緒例外攔截
            try {
                Looper.loop()
            } catch (e: Throwable) {
            }
        }
    }

我們通過Handler往主執行緒發送了一個runnable任務,然后在這個runnable中加了一個死回圈,死回圈中執行了Looper.loop()進行訊息回圈讀取,這樣就會導致后續所有的主執行緒訊息都會走到我們這個loop方法中進行處理,也就是一旦發生了主執行緒崩潰,那么這里就可以進行例外捕獲,同時因為我們寫的是while死回圈,那么捕獲例外后,又會開始新的Looper.loop()方法執行,這樣主執行緒的Looper就可以一直正常讀取訊息,主執行緒就可以一直正常運行了,

文字說不清楚的圖片來幫我們:

同時之前CrashHandler的邏輯可以保證子執行緒也是不受崩潰影響,所以兩段代碼都加上,齊活了,

但是小光還不服氣,他又想到了一種崩潰情況,,,

小光又又又一次實驗

class Test2Activity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exception)

        throw RuntimeException("主執行緒例外")
    }
}

誒,我直接在onCreate里面給你拋出個例外,運行看看:

黑漆漆的一片~沒錯,黑屏了

最后的對話(Cockroach庫思想)

  • 看到這一幕,我主動找到了小光:

“這種情況確實比較麻煩了,如果直接在Activity生命周期內拋出例外,會導致界面繪制無法完成,Activity無法被正確啟動,就會白屏或者黑屏了
這種嚴重影響到用戶體驗的情況還是建議直接殺死APP,因為很有可能會對其他的功能模塊造成影響,或者如果某些Activity不是很重要,也可以只finish這個Activity,”

  • 小光思索地問:
    “那么怎么分辨出這種生命周期內發生崩潰的情況呢?”

“這就要通過反射了,借用Cockroach開源庫中的思想,由于Activity的生命周期都是通過主執行緒的Handler進行訊息處理,所以我們可以通過反射替換掉主執行緒的Handler中的Callback回呼,也就是ActivityThread.mH.mCallback,然后針對每個生命周期對應的訊息進行trycatch捕獲例外,然后就可以進行finishActivity或者殺死行程操作了,”

主要代碼:

		Field mhField = activityThreadClass.getDeclaredField("mH");
        mhField.setAccessible(true);
        final Handler mhHandler = (Handler) mhField.get(activityThread);
        Field callbackField = Handler.class.getDeclaredField("mCallback");
        callbackField.setAccessible(true);
        callbackField.set(mhHandler, new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (Build.VERSION.SDK_INT >= 28) {
                //android 28之后的生命周期處理
                    final int EXECUTE_TRANSACTION = 159;
                    if (msg.what == EXECUTE_TRANSACTION) {
                        try {
                            mhHandler.handleMessage(msg);
                        } catch (Throwable throwable) {
                            //殺死行程或者殺死Activity
                        }
                        return true;
                    }
                    return false;
                }
                
                //android 28之前的生命周期處理
                switch (msg.what) {
                    case RESUME_ACTIVITY:
                    //onRestart onStart onResume回呼這里
                        try {
                            mhHandler.handleMessage(msg);
                        } catch (Throwable throwable) {
                            sActivityKiller.finishResumeActivity(msg);
                            notifyException(throwable);
                        }
                        return true;

代碼貼了一部分,但是原理大家應該都懂了吧,就是通過替換主執行緒HandlerCallback,進行宣告周期的例外捕獲,

接下來就是進行捕獲后的處理作業了,要不殺死行程,要么殺死Activity,

  • 殺死行程,這個應該大家都熟悉
  Process.killProcess(Process.myPid())
  exitProcess(10)
  • finish掉Activity

這里又要分析下Activity的finish流程了,簡單說下,以android29的原始碼為例,

    private void finish(int finishTask) {
        if (mParent == null) {
            
            if (false) Log.v(TAG, "Finishing self: token=" + mToken);
            try {
                if (resultData != null) {
                    resultData.prepareToLeaveProcess(this);
                }
                if (ActivityTaskManager.getService()
                        .finishActivity(mToken, resultCode, resultData, finishTask)) {
                    mFinished = true;
                }
            } 
        } 

    }
    
    
    @Override
    public final boolean finishActivity(IBinder token, int resultCode, Intent resultData,
            int finishTask) {
        return mActivityTaskManager.finishActivity(token, resultCode, resultData, finishTask);
    }    

從Activity的finish原始碼可以得知,最終是呼叫到ActivityTaskManagerServicefinishActivity方法,這個方法有四個引數,其中有個用來標識Activity的引數也就是最重要的引數——token,所以去原始碼里面找找token~

由于我們捕獲的地方是在handleMessage回呼方法中,所以只有一個引數Message可以用,那我么你就從這方面入手,回到剛才我們處理訊息的原始碼中,看看能不能找到什么線索:

 class H extends Handler {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EXECUTE_TRANSACTION: 
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    mTransactionExecutor.execute(transaction);
                    break;              
            }        
        }
    }
    
    public void execute(ClientTransaction transaction) {
        final IBinder token = transaction.getActivityToken();
        executeCallbacks(transaction);
        executeLifecycleState(transaction);
        mPendingActions.clear();
        log("End resolving transaction");
    }    

可以看到在原始碼中,Handler是怎么處理EXECUTE_TRANSACTION訊息的,獲取到msg.obj物件,也就是ClientTransaction類實體,然后呼叫了execute方法,而在execute方法中,,,咦咦咦,這不就是token嗎?

(找到的過于快速了哈,主要是activity啟動銷毀這部分的原始碼解說并不是今天的重點,所以就一筆帶過了)

找到token,那我們就通過反射進行Activity的銷毀就行啦:

    private void finishMyCatchActivity(Message message) throws Throwable {
        ClientTransaction clientTransaction = (ClientTransaction) message.obj;
        IBinder binder = clientTransaction.getActivityToken();
       
       Method getServiceMethod = ActivityManager.class.getDeclaredMethod("getService");
        Object activityManager = getServiceMethod.invoke(null);

        Method finishActivityMethod = activityManager.getClass().getDeclaredMethod("finishActivity", IBinder.class, int.class, Intent.class, int.class);
        finishActivityMethod.setAccessible(true);
        finishActivityMethod.invoke(activityManager, binder, Activity.RESULT_CANCELED, null, 0);
    }

啊,終于搞定了,但是小光還是一臉疑惑的看著我:

“我還是去看Cockroach庫的原始碼吧~”

“我去,,”

總結

今天主要就說了一件事:如何捕獲程式中的例外不讓APP崩潰,從而給用戶帶來最好的體驗,主要有以下做法:

  • 通過在主執行緒里面發送一個訊息,捕獲主執行緒的例外,并在例外發生后繼續呼叫Looper.loop方法,使得主執行緒繼續處理訊息,
  • 對于子執行緒的例外,可以通過Thread.setDefaultUncaughtExceptionHandler來攔截,并且子執行緒的停止不會給用戶帶來感知,
  • 對于在生命周期內發生的例外,可以通過替換ActivityThread.mH.mCallback的方法來捕獲,并且通過token來結束Activity或者直接殺死行程,但是這種辦法要適配不同SDK版本的原始碼才行,所以慎用,需要的可以看文末Cockroach庫原始碼,

可能有的朋友會問,為什么要讓程式不崩潰呢?會有哪些情況需要我們進行這樣操作呢?

其實還是有很多時候,有些例外我們無法預料或者給用戶帶來幾乎是無感知的例外,比如:

  • 系統的一些bug
  • 第三方庫的一些bug
  • 不同廠商的手機帶來的一些bug

等等這些情況,我們就可以通過這樣的操作來讓APP犧牲掉這部分的功能來維護系統的穩定性,

參考

Cockroach
一文讀懂 Handler 機制全家桶
zyogte行程(Java篇)
wanAndroid

拜拜

好了,到了說再見的時候了,

最后給大家推薦一個劇—棋魂,嘿嘿,小光就是里面的主角,

這些優秀的開源庫又何嘗不是指引我們前行進步的光呢~

有一起學習的小伙伴可以關注下??我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識,公眾號回復111可獲得面試題《思考與解答》以往期刊,

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

標籤:Android

上一篇:微信小程式在手機上除錯為何抓不到資料

下一篇:iOS-for回圈快捷創建按鈕(隨意配置適配)

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