主頁 > 移動端開發 > 微信Android客戶端的ANR監控方案

微信Android客戶端的ANR監控方案

2021-08-09 08:18:26 移動端開發

https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649288031&idx=1&sn=91c94e16460a4685a9c0c8e1b9c362a6&chksm=8334c9ddb44340cb66e6ce512ca41592fb483c148419737dbe21f9bbc2bfc2f872d1e54d1641&scene=178&cur_album_id=1955379809983741955#rd

微信公眾號,WeMobileDev 2021年7月19日發布的 微信Android客戶端的ANR監控方案

該方案的所有代碼已經在Matrix(https://github.com/Tencent/matrix)中開源,這篇文章將詳細講解原始碼實作

1.SignalAnrTracer onAlive方法里呼叫nativeInitSignalAnrDetective方法監聽SIGQUIT信號

public class SignalAnrTracer extends Tracer {
    //region 引數
    private static final String TAG = "SignalAnrTracer";
    //檢測anr執行緒名字
    //監控到SIGQUIT后,我們在20秒內(20秒是ANR dump的timeout時間)不斷輪詢自己是否有NOT_RESPONDING flag
    //一旦發現有這個flag,那么馬上就可以認定發生了一次ANR,
    private static final String CHECK_ANR_STATE_THREAD_NAME = "Check-ANR-State-Thread";
    //檢測NOT_RESPONDING flag間隔時間
    private static final int CHECK_ERROR_STATE_INTERVAL = 500;
    //dump最長時間20s
    private static final int ANR_DUMP_MAX_TIME = 20000;
    //檢測error次數
    private static final int CHECK_ERROR_STATE_COUNT =
            ANR_DUMP_MAX_TIME / CHECK_ERROR_STATE_INTERVAL;
    //前臺訊息,超時2s的時候,說明卡住了
    private static final long FOREGROUND_MSG_THRESHOLD = -2000;
    //后臺訊息,超時2s的時候,說明卡住了
    private static final long BACKGROUND_MSG_THRESHOLD = -10000;
    //是否hasInstance
    public static boolean hasInstance = false;
    //是否是前臺狀態
    private static boolean currentForeground = false;
    //anr trace 檔案路徑
    private static String sAnrTraceFilePath = "";
    //    這個Hook Trace的方案,不僅僅可以用來查ANR問題,任何時候我們都可以手動向自己發送一個SIGQUIT信號,
//    從而hook到當時的Trace,Trace的內容對于我們排查執行緒死鎖,執行緒例外,耗電等問題都非常有幫助,
    //列印trace 檔案路徑 ,自己觸發的
    private static String sPrintTraceFilePath = "";
    //監聽
    private static SignalAnrDetectedListener sSignalAnrDetectedListener;
    //sApplication
    private static Application sApplication;
    //是否初始化了
    private static boolean hasInit = false;
    //anr發生時間,負值
    private static long anrMessageWhen = 0L;
    //anr發生時主執行緒處理的訊息
    private static String anrMessageString = "";
    //endregion

    static {
        //加載trace-canary lib
        System.loadLibrary("trace-canary");
    }

    //region 建構式
    public SignalAnrTracer(TraceConfig traceConfig) {
        hasInstance = true;
        sAnrTraceFilePath = traceConfig.anrTraceFilePath;
        sPrintTraceFilePath = traceConfig.printTraceFilePath;
    }

    public SignalAnrTracer(Application application) {
        hasInstance = true;
        sApplication = application;
    }

    public SignalAnrTracer(Application application, String anrTraceFilePath, String printTraceFilePath) {
        hasInstance = true;
        sAnrTraceFilePath = anrTraceFilePath;
        sPrintTraceFilePath = printTraceFilePath;
        sApplication = application;
    }
    //endregion

    /**
     * AnrDumper.cc里 handleSignal
     */
    @RequiresApi(api = Build.VERSION_CODES.M)
    @Keep
    private static void onANRDumped() {
        //是否是前臺
        currentForeground = AppForegroundUtil.isInterestingToUser();
        //是否是主執行緒堵塞了,需要report
        boolean needReport = isMainThreadBlocked();

        //有兩種情況,主執行緒訊息已經堵住了,或者開啟一個執行緒檢測狀態 NOT_RESPONDING
        //需要report
        if (needReport) {
            report(false);
        } else {
//            監控到SIGQUIT后,我們在20秒內(20秒是ANR dump的timeout時間)不斷輪詢自己是否有NOT_RESPONDING flag
//            ,一旦發現有這個flag,那么馬上就可以認定發生了一次ANR,
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //開啟了一個執行緒檢查
                    checkErrorStateCycle();
                }
            }, CHECK_ANR_STATE_THREAD_NAME).start();
        }
    }

    @Keep
    private static void onANRDumpTrace() {
        try {
            MatrixUtil.printFileByLine(TAG, sAnrTraceFilePath);
        } catch (Throwable t) {
            MatrixLog.e(TAG, "onANRDumpTrace error: %s", t.getMessage());
        }
    }
    //endregion

    @Keep
    private static void onPrintTrace() {
        try {
            MatrixUtil.printFileByLine(TAG, sPrintTraceFilePath);
        } catch (Throwable t) {
            MatrixLog.e(TAG, "onPrintTrace error: %s", t.getMessage());
        }
    }

    /**
     * @param fromProcessErrorState false代表主執行緒阻塞了
     */
    private static void report(boolean fromProcessErrorState) {
        try {
            String stackTrace = Utils.getMainThreadJavaStackTrace();
            if (sSignalAnrDetectedListener != null) {
                sSignalAnrDetectedListener.onAnrDetected(stackTrace, anrMessageString, anrMessageWhen, fromProcessErrorState);
                return;
            }

            TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
            if (null == plugin) {
                return;
            }

            String scene = AppMethodBeat.getVisibleScene();

            JSONObject jsonObject = new JSONObject();
            jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication());
            jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.SIGNAL_ANR);
            jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene);
            jsonObject.put(SharePluginInfo.ISSUE_THREAD_STACK, stackTrace);
            jsonObject.put(SharePluginInfo.ISSUE_PROCESS_FOREGROUND, currentForeground);

            Issue issue = new Issue();
            issue.setTag(SharePluginInfo.TAG_PLUGIN_EVIL_METHOD);
            issue.setContent(jsonObject);
            plugin.onDetectIssue(issue);
            MatrixLog.e(TAG, "happens real ANR : %s ", jsonObject.toString());

        } catch (JSONException e) {
            MatrixLog.e(TAG, "[JSONException error: %s", e);
        }
    }

    //通過訊息時間,來判斷是否到超出閾值
    @RequiresApi(api = Build.VERSION_CODES.M)
    private static boolean isMainThreadBlocked() {
        try {
            MessageQueue mainQueue = Looper.getMainLooper().getQueue();
            Field field = mainQueue.getClass().getDeclaredField("mMessages");
            field.setAccessible(true);
            final Message mMessage = (Message) field.get(mainQueue);
            if (mMessage != null) {
                anrMessageString = mMessage.toString();
                long when = mMessage.getWhen();
                if (when == 0) {
                    return false;
                }
                long time = when - SystemClock.uptimeMillis();
                anrMessageWhen = time;
                long timeThreshold = BACKGROUND_MSG_THRESHOLD;
                if (currentForeground) {
                    timeThreshold = FOREGROUND_MSG_THRESHOLD;
                }
                return time < timeThreshold;
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    private static void checkErrorStateCycle() {
        int checkErrorStateCount = 0;
        //開啟一個回圈檢測
        while (checkErrorStateCount < CHECK_ERROR_STATE_COUNT) {
            try {
                checkErrorStateCount++;
                boolean myAnr = checkErrorState();
                if (myAnr) {
                    report(true);
                    break;
                }

                Thread.sleep(CHECK_ERROR_STATE_INTERVAL);
            } catch (Throwable t) {
                MatrixLog.e(TAG, "checkErrorStateCycle error, e : " + t.getMessage());
                break;
            }
        }
    }

    //用來判斷anr發生了
//    在ANR彈窗前,會執行到makeAppNotRespondingLocked方法中,在這里會給發生ANR行程標記一個NOT_RESPONDING的flag,
//    而這個flag我們可以通過ActivityManager來獲取:
    private static boolean checkErrorState() {
        try {
            Application application =
                    sApplication == null ? Matrix.with().getApplication() : sApplication;
            ActivityManager am = (ActivityManager) application
                    .getSystemService(Context.ACTIVITY_SERVICE);
            //從ActivityManager 獲取ProcessErrorStateInfo
            List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
            if (procs == null) return false;

            for (ActivityManager.ProcessErrorStateInfo proc : procs) {
                MatrixLog.i(TAG, "[checkErrorState] found Error State proccessName = %s, proc.condition = %d", proc.processName, proc.condition);

                if (proc.uid != android.os.Process.myUid()
                        && proc.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                    MatrixLog.i(TAG, "maybe received other apps ANR signal");
                }

                if (proc.pid != android.os.Process.myPid()) continue;

                if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                    continue;
                }
                //只有是自己行程,并且是NOT_RESPONDING的時候,才回傳true
                return true;
            }
            return false;
        } catch (Throwable t) {
            MatrixLog.e(TAG, "[checkErrorState] error : %s", t.getMessage());
        }
        return false;
    }

    //ok
    public static void printTrace() {
        if (!hasInstance) {
            MatrixLog.e(TAG, "SignalAnrTracer has not been initialize");
            return;
        }
        if (sPrintTraceFilePath.equals("")) {
            MatrixLog.e(TAG, "PrintTraceFilePath has not been set");
            return;
        }
        nativePrintTrace();
    }

    private static native void nativeInitSignalAnrDetective(String anrPrintTraceFilePath, String printTraceFilePath);

    private static native void nativeFreeSignalAnrDetective();

    private static native void nativePrintTrace();

    @Override
    protected void onAlive() {
        super.onAlive();
        if (!hasInit) {
            //呼叫native方法啟動監聽
            nativeInitSignalAnrDetective(sAnrTraceFilePath, sPrintTraceFilePath);
            //主要用來判斷是否是前臺
            AppForegroundUtil.INSTANCE.init();
            hasInit = true;
        }
    }

    @Override
    protected void onDead() {
        super.onDead();
        //free anr檢測
        nativeFreeSignalAnrDetective();
    }

    public void setSignalAnrDetectedListener(SignalAnrDetectedListener listener) {
        sSignalAnrDetectedListener = listener;
    }

    public interface SignalAnrDetectedListener {
        void onAnrDetected(String stackTrace, String mMessageString, long mMessageWhen, boolean fromProcessErrorState);
    }
}

2.MatrixTracer.cc

2.1 JNI_OnLoad初始化,雙向系結函式

2.2 nativeInitSignalAnrDetective,開啟檢測,真正檢測的地方在AnrDumper.cc

2.3 AnrDumper.cc 里handleSignal里呼叫MatrixTracer anrDumpCallback ,表示anr可能發生了,通知SignalAnrTracer檢測ui執行緒是否block或者狀態為NOT_RESPONDING,并呼叫hookAnrTraceWrite方法,開啟hook,為了找到write trace的點

2.4 my_connect,my_open是開始socket通信了,主要為了檢測socket通信之后的write方法

2.5 my_write是我們的write方法


#define PROP_VALUE_MAX  92                      //用于求getApiLevel
#define PROP_SDK_NAME "ro.build.version.sdk"    //用于求getApiLevel
#define HOOK_CONNECT_PATH "/dev/socket/tombstoned_java_trace"   //socket檔案地址
#define HOOK_OPEN_PATH "/data/anr/traces.txt"                   //socket檔案地址

using namespace MatrixTracer;

static std::optional<AnrDumper> sAnrDumper; //AnrDumper,是自定義的SignalHandler
static bool isTraceWrite = false;           //isTraceWrite my_connect my_open設定為true,my_write設定為false
static bool fromMyPrintTrace = false;       //fromMyPrintTrace 是否是自己想打的
static bool isHooking = false;              //是否hooking,unHookAnrTraceWrite設定為false
static std::string anrTracePathstring;      //新的anrTracePathstring,系統用的
static std::string printTracePathstring;    //新的printTracePathstring,我自己想列印的時候用的
static int signalCatcherTid;                //signalCatcherTid的執行緒id

//一個結構體,用來保存java層 類,方法地址
static struct StacktraceJNI {
    jclass AnrDetective;                    //SignalAnrTracer
    jclass ThreadPriorityDetective;
    jmethodID AnrDetector_onANRDumped;      //SignalAnrTracer 里的
    jmethodID AnrDetector_onANRDumpTrace;   //SignalAnrTracer 里的
    jmethodID AnrDetector_onPrintTrace;     //SignalAnrTracer 里的

    jmethodID ThreadPriorityDetective_onMainThreadPriorityModified;
    jmethodID ThreadPriorityDetective_onMainThreadTimerSlackModified;
} gJ;

//region MainThreadPriorityModified相關的東西
int (*original_setpriority)(int __which, id_t __who, int __priority);

int my_setpriority(int __which, id_t __who, int __priority) {

    if (__priority <= 0) {
        return original_setpriority(__which, __who, __priority);
    }
    if (__who == 0 && getpid() == gettid()) {
        JNIEnv *env = JniInvocation::getEnv();
        env->CallStaticVoidMethod(gJ.ThreadPriorityDetective,
                                  gJ.ThreadPriorityDetective_onMainThreadPriorityModified,
                                  __priority);
    } else if (__who == getpid()) {
        JNIEnv *env = JniInvocation::getEnv();
        env->CallStaticVoidMethod(gJ.ThreadPriorityDetective,
                                  gJ.ThreadPriorityDetective_onMainThreadPriorityModified,
                                  __priority);
    }

    return original_setpriority(__which, __who, __priority);
}

int (*original_prctl)(int option, unsigned long arg2, unsigned long arg3,
                      unsigned long arg4, unsigned long arg5);

int my_prctl(int option, unsigned long arg2, unsigned long arg3,
             unsigned long arg4, unsigned long arg5) {

    if (option == PR_SET_TIMERSLACK) {
        if (gettid() == getpid() && arg2 > 50000) {
            JNIEnv *env = JniInvocation::getEnv();
            env->CallStaticVoidMethod(gJ.ThreadPriorityDetective,
                                      gJ.ThreadPriorityDetective_onMainThreadTimerSlackModified,
                                      arg2);

        }
    }

    return original_prctl(option, arg2, arg3, arg4, arg5);
}
//endregion

/**
 *
 * @param content 內容
 * @param filePath 檔案地址
 */
void writeAnr(const std::string &content, const std::string &filePath) {
    //unhook write
    unHookAnrTraceWrite();
    std::stringstream stringStream(content);
    std::string to;
    std::ofstream outfile;
    outfile.open(filePath);
    outfile << content;
}

//region my_connect  original_connect
int (*original_connect)(int __fd, const struct sockaddr *__addr, socklen_t __addr_length);

int my_connect(int __fd, const struct sockaddr *__addr, socklen_t __addr_length) {
    if (__addr != nullptr) {
        //hook connect方法,檢測sockaddr地址是否為HOOK_CONNECT_PATH,表明是signal檢測執行緒
        if (strcmp(__addr->sa_data, HOOK_CONNECT_PATH) == 0) {
            //設定signal檢測執行緒id
            signalCatcherTid = gettid();
            //標記開始列印
            isTraceWrite = true;
        }
    }
    return original_connect(__fd, __addr, __addr_length);
}
//endregion

//region my_open original_open
int (*original_open)(const char *pathname, int flags, mode_t mode);

int my_open(const char *pathname, int flags, mode_t mode) {
    if (pathname != nullptr) {
        //hook connect方法,檢測sockaddr地址是否為HOOK_OPEN_PATH,表明是signal檢測執行緒
        if (strcmp(pathname, HOOK_OPEN_PATH) == 0) {
            //設定signal檢測執行緒id
            signalCatcherTid = gettid();
            //標記開始列印
            isTraceWrite = true;
        }
    }
    return original_open(pathname, flags, mode);
}
//endregion

//region original_write my_write
ssize_t (*original_write)(int fd, const void *const __pass_object_size0 buf, size_t count);

ssize_t my_write(int fd, const void *const buf, size_t count) {
    //如果標記為isTraceWrite為true,第一個signalCatcher執行緒,write呼叫即為列印trace的地方
    if (isTraceWrite && gettid() == signalCatcherTid) {
        isTraceWrite = false;
        signalCatcherTid = 0;
        if (buf != nullptr) {
            std::string targetFilePath;
            if (fromMyPrintTrace) {
                targetFilePath = printTracePathstring;
            } else {
                targetFilePath = anrTracePathstring;
            }
            if (!targetFilePath.empty()) {
                char *content = (char *) buf;
                writeAnr(content, targetFilePath);
                if (!fromMyPrintTrace) {
                    anrDumpTraceCallback();
                } else {
                    printTraceCallback();
                }
                fromMyPrintTrace = false;
            }
        }
    }
    return original_write(fd, buf, count);
}
//endregion

//呼叫java的onANRDumped,AnrDumper.cc 里handleSignal里呼叫anrCallback然后呼叫這個anrDumpCallback回呼
bool anrDumpCallback() {
    JNIEnv *env = JniInvocation::getEnv();
    if (!env) return false;
    env->CallStaticVoidMethod(gJ.AnrDetective, gJ.AnrDetector_onANRDumped);
    return true;
}

//呼叫java的onANRDumpTrace,my_write里呼叫
bool anrDumpTraceCallback() {
    JNIEnv *env = JniInvocation::getEnv();
    if (!env) return false;
    env->CallStaticVoidMethod(gJ.AnrDetective, gJ.AnrDetector_onANRDumpTrace);
    return true;
}

//呼叫java的onPrintTrace,my_write里呼叫
bool printTraceCallback() {
    JNIEnv *env = JniInvocation::getEnv();
    if (!env) return false;
    env->CallStaticVoidMethod(gJ.AnrDetective, gJ.AnrDetector_onPrintTrace);
    return true;
}

//ok
int getApiLevel() {
    char buf[PROP_VALUE_MAX];
    int len = __system_property_get(PROP_SDK_NAME, buf);
    if (len <= 0)
        return 0;

    return atoi(buf);
}

/**
 * @param isSiUser true為自己的行程
 * AnrDumper.cc 里handleSignal里呼叫anrCallback方法,或者呼叫siUserCallback,然后呼叫這個hookAnrTraceWrite回呼
 */
void hookAnrTraceWrite(bool isSiUser) {
    int apiLevel = getApiLevel();
    if (apiLevel < 19) {
        return;
    }

    //isSiUser為true,表示自己行程發的時候是通過kill發的,此處不符合邏輯,回傳
    if (!fromMyPrintTrace && isSiUser) {
        return;
    }

    if (isHooking) {
        return;
    }

    isHooking = true;

    if (apiLevel >= 27) {
        void *libcutils_info = xhook_elf_open("/system/lib64/libcutils.so");
        if (!libcutils_info) {
            libcutils_info = xhook_elf_open("/system/lib/libcutils.so");
        }
        xhook_hook_symbol(libcutils_info, "connect", (void *) my_connect,
                          (void **) (&original_connect));
    } else {
        void *libart_info = xhook_elf_open("libart.so");
        xhook_hook_symbol(libart_info, "open", (void *) my_open, (void **) (&original_open));
    }

    if (apiLevel >= 30 || apiLevel == 25 || apiLevel == 24) {
        void *libc_info = xhook_elf_open("libc.so");
        xhook_hook_symbol(libc_info, "write", (void *) my_write, (void **) (&original_write));
    } else if (apiLevel == 29) {
        void *libbase_info = xhook_elf_open("/system/lib64/libbase.so");
        if (!libbase_info) {
            libbase_info = xhook_elf_open("/system/lib/libbase.so");
        }
        xhook_hook_symbol(libbase_info, "write", (void *) my_write, (void **) (&original_write));
        xhook_elf_close(libbase_info);
    } else {
        void *libart_info = xhook_elf_open("libart.so");
        xhook_hook_symbol(libart_info, "write", (void *) my_write, (void **) (&original_write));
    }
}

//unhook
void unHookAnrTraceWrite() {
    int apiLevel = getApiLevel();
    if (apiLevel >= 27) {
        void *libcutils_info = xhook_elf_open("/system/lib64/libcutils.so");
        xhook_hook_symbol(libcutils_info, "connect", (void *) original_connect, nullptr);
    } else {
        void *libart_info = xhook_elf_open("libart.so");
        xhook_hook_symbol(libart_info, "open", (void *) original_connect, nullptr);
    }

    if (apiLevel >= 30 || apiLevel == 25 || apiLevel == 24) {
        void *libc_info = xhook_elf_open("libc.so");
        xhook_hook_symbol(libc_info, "write", (void *) original_write, nullptr);
    } else if (apiLevel == 29) {
        void *libbase_info = xhook_elf_open("/system/lib64/libbase.so");
        xhook_hook_symbol(libbase_info, "write", (void *) original_write, nullptr);
    } else {
        void *libart_info = xhook_elf_open("libart.so");
        xhook_hook_symbol(libart_info, "write", (void *) original_write, nullptr);
    }
    isHooking = false;
}

//初始化,開啟檢測Signalanr檢測,真正檢測的地方在AnrDumper.cc
static void
nativeInitSignalAnrDetective(JNIEnv *env, jclass, jstring anrTracePath, jstring printTracePath) {
    //anr發生時,列印path
    const char *anrTracePathChar = env->GetStringUTFChars(anrTracePath, nullptr);
    //手動發送SIGQUIT,列印的trace地址
    const char *printTracePathChar = env->GetStringUTFChars(printTracePath, nullptr);
    anrTracePathstring = std::string(anrTracePathChar);
    printTracePathstring = std::string(printTracePathChar);
    //開啟檢測,真正檢測的地方在AnrDumper.cc
    sAnrDumper.emplace(anrTracePathChar, printTracePathChar, anrDumpCallback);
}

//Free Signal Anr Detective 重置,釋放
static void nativeFreeSignalAnrDetective(JNIEnv *env, jclass) {
    //重置,釋放
    sAnrDumper.reset();
}

//region MainThreadPriority相關 ,先不看
static void nativeInitMainThreadPriorityDetective(JNIEnv *env, jclass) {
    xhook_register(".*\\.so$", "setpriority", (void *) my_setpriority,
                   (void **) (&original_setpriority));
    xhook_register(".*\\.so$", "prctl", (void *) my_prctl, (void **) (&original_prctl));
    xhook_refresh(true);
}
//endregion

//自己列印trace,發送自己的行程發送SIGQUIT
static void nativePrintTrace() {
    fromMyPrintTrace = true;
    kill(getpid(), SIGQUIT);
}

template<typename T, std::size_t sz>//todo
static inline constexpr std::size_t NELEM(const T(&)[sz]) { return sz; }//todo

//JNINativeMethod 陣列 anr相關的
static const JNINativeMethod ANR_METHODS[] = {
        {"nativeInitSignalAnrDetective", "(Ljava/lang/String;Ljava/lang/String;)V", (void *) nativeInitSignalAnrDetective},
        {"nativeFreeSignalAnrDetective", "()V",                                     (void *) nativeFreeSignalAnrDetective},
        {"nativePrintTrace",             "()V",                                     (void *) nativePrintTrace},
};

//MainThreadPriority相關的,先不看
static const JNINativeMethod THREAD_PRIORITY_METHODS[] = {
        {"nativeInitMainThreadPriorityDetective", "()V", (void *) nativeInitMainThreadPriorityDetective},
};

//JNI_OnLoad 初始化jni環境
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
    JniInvocation::init(vm);

    JNIEnv *env;
    //獲取env環境,如果env環境沒有獲取成功,回傳-1
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK)
        return -1;

    //獲取SignalAnrTracer變為jclass
    jclass anrDetectiveCls = env->FindClass("com/tencent/matrix/trace/tracer/SignalAnrTracer");
    if (!anrDetectiveCls)
        return -1;
    //保存SignalAnrTracer為jclass
    gJ.AnrDetective = static_cast<jclass>(env->NewGlobalRef(anrDetectiveCls));
    //保存方法
    gJ.AnrDetector_onANRDumped =
            env->GetStaticMethodID(anrDetectiveCls, "onANRDumped", "()V");
    gJ.AnrDetector_onANRDumpTrace =
            env->GetStaticMethodID(anrDetectiveCls, "onANRDumpTrace", "()V");
    gJ.AnrDetector_onPrintTrace =
            env->GetStaticMethodID(anrDetectiveCls, "onPrintTrace", "()V");

    //注冊native方法,使得java可以呼叫native
    if (env->RegisterNatives(
            anrDetectiveCls, ANR_METHODS, static_cast<jint>(NELEM(ANR_METHODS))) != 0)
        return -1;

    //洗掉anrDetectiveCls
    env->DeleteLocalRef(anrDetectiveCls);


    jclass threadPriorityDetectiveCls = env->FindClass(
            "com/tencent/matrix/trace/tracer/ThreadPriorityTracer");
    if (!threadPriorityDetectiveCls)
        return -1;
    gJ.ThreadPriorityDetective = static_cast<jclass>(env->NewGlobalRef(threadPriorityDetectiveCls));
    gJ.ThreadPriorityDetective_onMainThreadPriorityModified =
            env->GetStaticMethodID(threadPriorityDetectiveCls, "onMainThreadPriorityModified",
                                   "(I)V");
    gJ.ThreadPriorityDetective_onMainThreadTimerSlackModified =
            env->GetStaticMethodID(threadPriorityDetectiveCls, "onMainThreadTimerSlackModified",
                                   "(J)V");


    if (env->RegisterNatives(
            threadPriorityDetectiveCls, THREAD_PRIORITY_METHODS,
            static_cast<jint>(NELEM(THREAD_PRIORITY_METHODS))) != 0)
        return -1;

    env->DeleteLocalRef(threadPriorityDetectiveCls);


    return JNI_VERSION_1_6;
}   // namespace MatrixTracer

3.AnrDumper.h 定義AnrDumper,繼承SignalHandler

namespace MatrixTracer {

class AnrDumper : public SignalHandler {
 public:
    //定義回呼方法
    using DumpCallbackFunction = std::function<bool()>;

    AnrDumper(const char* anrTraceFile, const char* printTraceFile, DumpCallbackFunction&& callback);//&&參考,這個功能是C++的補充,常用在函式傳參(C中一般用指標)、臨時變數參考等,
    virtual ~AnrDumper();

 private:
    //處理signal地方
    Result handleSignal(int sig, const siginfo_t *info, void *uc) final;
    const DumpCallbackFunction mCallback;
};
}   // namespace MatrixTracer

#endif  // LAGDETECTOR_LAG_DETECTOR_MAIN_CPP_ANRDUMPER_H_

4.AnrDumper.cc handleSignal方法監聽 SIGQUIT信號,并根據其他行程還是自己行程來呼叫anrCallback 或者siUserCallback,

4.1 anr是system_server行程發來的SIGQUIT,anrCallback代表可能發生了anr,之后會呼叫anrDumpCallback,讓SignalAnrTracer檢測ui執行緒是否block或者狀態為NOT_RESPONDING


#define SIGNAL_CATCHER_THREAD_NAME "Signal Catcher"
#define SIGNAL_CATCHER_THREAD_SIGBLK 0x1000 //得到SignalCatcherThreadId,todo 沒看明白
#define O_WRONLY 00000001
#define O_CREAT 00000100
#define O_TRUNC 00001000

namespace MatrixTracer {
    static sigset_t old_sigSet;
    const char *mAnrTraceFile;
    const char *mPrintTraceFile;

//建立了Signal Handler之后,我們發現在同時有sigwait和signal handler的情況下,
// 信號沒有走到我們的signal handler而是依然被系統的Signal Catcher執行緒捕獲到了,這是什么原因呢?
//
//原來是Android默認把SIGQUIT設定成了BLOCKED,所以只會回應sigwait而不會進入到我們設定的handler方法中,
// 我們通過pthread_sigmask或者sigprocmask把SIGQUIT設定為UNBLOCK,那么再次收到SIGQUIT時,就一定會進入到我們的handler方法中,需要這樣設定:
    AnrDumper::AnrDumper(const char *anrTraceFile, const char *printTraceFile,
                         AnrDumper::DumpCallbackFunction &&callback) : mCallback(callback) {
        // must unblocked SIGQUIT, otherwise the signal handler can not capture SIGQUIT
        // 必須unblock,否則signal handler無法接收到信號,而是由signal_cahcher執行緒中的sigwait接收信號,走一般的ANR流程
        mAnrTraceFile = anrTraceFile;
        mPrintTraceFile = printTraceFile;
        sigset_t sigSet;
        sigemptyset(&sigSet);
        sigaddset(&sigSet, SIGQUIT);
        pthread_sigmask(SIG_UNBLOCK, &sigSet, &old_sigSet);
    }

    //得到SignalCatcherThreadId,todo 沒看明白
    static int getSignalCatcherThreadId() {
        char taskDirPath[128];
        DIR *taskDir;
        long long sigblk;
        int signalCatcherTid = -1;
        int firstSignalCatcherTid = -1;

        snprintf(taskDirPath, sizeof(taskDirPath), "/proc/%d/task", getpid());
        if ((taskDir = opendir(taskDirPath)) == nullptr) {
            return -1;
        }
        struct dirent *dent;
        pid_t tid;
        while ((dent = readdir(taskDir)) != nullptr) {
            tid = atoi(dent->d_name);
            if (tid <= 0) {
                continue;
            }

            char threadName[1024];
            char commFilePath[1024];
            snprintf(commFilePath, sizeof(commFilePath), "/proc/%d/task/%d/comm", getpid(), tid);

            Support::readFileAsString(commFilePath, threadName, sizeof(threadName));

            if (strncmp(SIGNAL_CATCHER_THREAD_NAME, threadName,
                        sizeof(SIGNAL_CATCHER_THREAD_NAME) - 1) != 0) {
                continue;
            }

            if (firstSignalCatcherTid == -1) {
                firstSignalCatcherTid = tid;
            }

            sigblk = 0;
            char taskPath[128];
            snprintf(taskPath, sizeof(taskPath), "/proc/%d/status", tid);

            ScopedFileDescriptor fd(open(taskPath, O_RDONLY, 0));
            LineReader lr(fd.get());
            const char *line;
            size_t len;
            while (lr.getNextLine(&line, &len)) {
                if (1 == sscanf(line, "SigBlk: %" SCNx64, &sigblk)) {
                    break;
                }
                lr.popLine(len);
            }
            if (SIGNAL_CATCHER_THREAD_SIGBLK != sigblk) {
                continue;
            }
            signalCatcherTid = tid;
            break;
        }
        closedir(taskDir);

        if (signalCatcherTid == -1) {
            signalCatcherTid = firstSignalCatcherTid;
        }
        return signalCatcherTid;
    }

//我們通過Signal Handler搶到了SIGQUIT后,原本的Signal Catcher執行緒中的sigwait就不再能收到SIGQUIT了,
// 原本的dump堆疊的邏輯就無法完成了,我們為了ANR的整個邏輯和流程跟原來完全一致,需要在Signal Handler里面重新向Signal Catcher執行緒發送一個SIGQUIT:
    static void sendSigToSignalCatcher() {
        //遍歷/proc/[pid]目錄,找到SignalCatcher執行緒的tid
        int tid = getSignalCatcherThreadId();
        syscall(SYS_tgkill, getpid(), tid, SIGQUIT);
    }

    //SIGQUIT發生了,其他行程發來的,anr是system_server行程發來的訊息,不是自己行程發來的
    static void *anrCallback(void *arg) {
        //anr可能發生了,通知SignalAnrTracer檢測ui執行緒是否block或者狀態為NOT_RESPONDING
        anrDumpCallback();

        if (strlen(mAnrTraceFile) > 0) {
            //開始hook write socket
            hookAnrTraceWrite(false);
        }
        //轉發SIGQUIT
        sendSigToSignalCatcher();
        return nullptr;
    }

    //SIGQUIT發生了,自己行程發來的,不是anr
    static void *siUserCallback(void *arg) {
        //這里沒有呼叫anrDumpCallback,因為是自己觸發的
        if (strlen(mPrintTraceFile) > 0) {
            //開始hook write socket
            hookAnrTraceWrite(true);
        }
        //轉發SIGQUIT
        sendSigToSignalCatcher();
        return nullptr;
    }

//另外,Signal Handler回呼的第二個引數siginfo_t,也包含了一些有用的資訊,該結構體的第三個欄位si_code表示該信號被
// 發送的方法,SI_USER表示信號是通過kill發送的,SI_QUEUE表示信號是通過sigqueue發送的,但在Android的ANR流程中,
// 高版本使用的是sigqueue發送的信號,某些低版本使用的是kill發送的信號,并不統一,
//
//而第五個欄位(極少數機型上是第四個欄位)si_pid表示的是發送該信號的行程的pid,這里適用幾乎所有Android版本和機型的
// 一個條件是:如果發送信號的行程是自己的行程,那么一定不是一個ANR,可以通過這個條件排除自己發送SIGQUIT,
// 而導致誤報的情況,
    SignalHandler::Result AnrDumper::handleSignal(int sig, const siginfo_t *info, void *uc) {
        // Only process SIGQUIT, which indicates an ANR.
        if (sig != SIGQUIT) return NOT_HANDLED;
        //Got An ANR
        int fromPid1 = info->_si_pad[3];
        int fromPid2 = info->_si_pad[4];
        int myPid = getpid();

        pthread_t thd;

        if (fromPid1 != myPid && fromPid2 != myPid) {
            //一個條件是:如果發送信號的行程是自己的行程,那么一定不是一個ANR,可以通過這個條件排除自己發送SIGQUIT,
            pthread_create(&thd, nullptr, anrCallback, nullptr);
        } else {
            //自己的行程
            pthread_create(&thd, nullptr, siUserCallback, nullptr);
        }
        pthread_detach(thd);

        return HANDLED_NO_RETRIGGER;
    }

    //沒用到
    static void *anr_trace_callback(void *args) {
        anrDumpTraceCallback();
        return nullptr;
    }

    //沒用到
    static void *print_trace_callback(void *args) {
        printTraceCallback();
        return nullptr;
    }


    AnrDumper::~AnrDumper() {
        pthread_sigmask(SIG_SETMASK, &old_sigSet, nullptr);
    }

}   // namespace MatrixTracer

5.我們的SignalHandler類

5.1 signalHandler方法主要是收到了信號

5.2 handleSignal處理信號

namespace MatrixTracer {

    class SignalHandler {
    public:
        SignalHandler();

        virtual ~SignalHandler();//解構式:
//    當一個類的物件離開作用域時,解構式將被呼叫(系統自動呼叫),解構式的名字和類名一樣,不過要在前面加上 ~ ,
//    對一個類來說,只能允許一個解構式,解構式不能有引數,并且也沒有回傳值,
//    解構式的作用是完成一個清理作業,如釋放從堆中分配的記憶體,

    protected:
        enum Result {
            NOT_HANDLED = 0, HANDLED, HANDLED_NO_RETRIGGER
        };//retrigger
        virtual Result handleSignal(int sig, const siginfo_t *info, void *uc) = 0;

    private:
        static void signalHandler(int sig, siginfo_t *info, void *uc);

        static bool installHandlersLocked();

        //https://blog.csdn.net/lmb1612977696/article/details/80035487
        SignalHandler(const SignalHandler &) = delete;//禁止生成該函式,默認拷貝建構式
        SignalHandler &operator=(const SignalHandler &) = delete;//禁止生成該函式,默認賦值函式
    };

}   // namespace MatrixTracer

#endif  // LAGDETECTOR_LAG_DETECTOR_MAIN_CPP_SIGNALHANDLER_H_

6.SignalHandler.cc

6.1 installHandlersLocked 通過可以sigaction方法,建立一個Signal Handler,sa_sigaction方法地址設定為我們的signalHandler方法

6.2 signalHandler 信號處理的地方,轉發給各SignalHandler的handleSignal

//執行緒名字,todo,得到SignalCatcherThreadId,todo 沒看明白
#define SIGNAL_CATCHER_THREAD_NAME "Signal Catcher"
//退出執行緒標記,todo,得到SignalCatcherThreadId,todo 沒看明白
#define SIGNAL_CATCHER_THREAD_SIGBLK 0x1000

namespace MatrixTracer {
//信號
    const int TARGET_SIG = SIGQUIT;//3
//使用sigaction方法注冊signal handler進行異步監聽,sOldHandlers是保存老的sigaction
    struct sigaction sOldHandlers;//todo
    bool sHandlerInstalled = false;

// The global signal handler stack. This is needed because there may exist
// multiple SignalHandler instances in a process. Each will have itself
// registered in this stack.
    static std::vector<SignalHandler *> *sHandlerStack = nullptr;//todo
// C++11中新增了<mutex>,它是C++標準程式庫中的一個頭檔案,定義了C++11標準中的一些互斥訪問的類與方法等,其中std::mutex就是lock、unlock,std::lock_guard與std::mutex配合使用,把鎖放到lock_guard中時,mutex自動上鎖,lock_guard析構時,同時把mutex解鎖,mutex又稱互斥量,
    static std::mutex sHandlerStackMutex;//todo
    static bool sStackInstalled = false;
// InstallAlternateStackLocked will store the newly installed stack in new_stack
// and (if it exists) the previously installed stack in old_stack.
    static stack_t sOldStack;//todo
    static stack_t sNewStack;//todo

    static void installAlternateStackLocked() {//todo
        if (sStackInstalled)
            return;
        //重置
        memset(&sOldStack, 0, sizeof(sOldStack));
        memset(&sNewStack, 0, sizeof(sNewStack));
        static constexpr unsigned kSigStackSize = std::max(16384, SIGSTKSZ);
        //取到老的sOldStack
        if (sigaltstack(nullptr, &sOldStack) == -1 || !sOldStack.ss_sp ||
            sOldStack.ss_size < kSigStackSize) {
            sNewStack.ss_sp = calloc(1, kSigStackSize);
            sNewStack.ss_size = kSigStackSize;
            //設定新的sNewStack
            if (sigaltstack(&sNewStack, nullptr) == -1) {
                free(sNewStack.ss_sp);
                return;
            }
        }

        sStackInstalled = true;
        ALOGV("Alternative stack installed.");
    }

// Runs before crashing: normal context.
//    我們通過可以sigaction方法,建立一個Signal Handler:ok
    bool SignalHandler::installHandlersLocked() {
        if (sHandlerInstalled) {
            return false;
        }
        // Fail if unable to store all the old handlers.
        //取到老的sOldHandlers
        if (sigaction(TARGET_SIG, nullptr, &sOldHandlers) == -1) {
            return false;
        }

        struct sigaction sa{};//sigaction結構體
        sa.sa_sigaction = signalHandler;//方法地址,收到信號的地方
        sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTART;
        //我們通過可以sigaction方法,建立一個Signal Handler
        if (sigaction(TARGET_SIG, &sa, nullptr) == -1) {//sigaction方法,將sa設定為Signal Handler
            ALOGV("Signal handler cannot be installed");

            // At this point it is impractical to back out changes, and so failure to
            // install a signal is intentionally ignored.
        }

        sHandlerInstalled = true;
        ALOGV("Signal handler installed.");
        return true;
    }

    //todo
    static void installDefaultHandler(int sig) {

        // Android L+ expose signal and sigaction symbols that override the system
        // ones. There is a bug in these functions where a request to set the handler
        // to SIG_DFL is ignored. In that case, an infinite loop is entered as the
        // signal is repeatedly sent to breakpad's signal handler.
        // To work around this, directly call the system's sigaction.
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sigemptyset(&sa.sa_mask);
        sa.sa_handler = SIG_DFL;
        sa.sa_flags = SA_RESTART;
        sigaction(sig, &sa, nullptr);
    }

// This function runs in a compromised context: see the top of the file.
// Runs on the crashing thread.
    static void restoreHandlersLocked() {//todo
        if (!sHandlerInstalled)
            return;
        //將老的sOldHandlers重新sigaction上
        if (sigaction(TARGET_SIG, &sOldHandlers, nullptr) == -1) {
            //todo
            installDefaultHandler(TARGET_SIG);
        }

        sHandlerInstalled = false;
        ALOGV("Signal handler restored.");
    }
    
    static void restoreAlternateStackLocked() {//todo
        if (!sStackInstalled)
            return;

        stack_t current_stack;
        if (sigaltstack(nullptr, &current_stack) == -1)
            return;
        // Only restore the old_stack if the current alternative stack is the one
        // installed by the call to InstallAlternateStackLocked.
        if (current_stack.ss_sp == sNewStack.ss_sp) {
            if (sOldStack.ss_sp) {
                if (sigaltstack(&sOldStack, nullptr) == -1)
                    return;
            } else {
                stack_t disable_stack;
                disable_stack.ss_flags = SS_DISABLE;
                if (sigaltstack(&disable_stack, nullptr) == -1)
                    return;
            }
        }

        free(sNewStack.ss_sp);
        sStackInstalled = false;
    }

// This function runs in a compromised context: see the top of the file.
// Runs on the crashing thread.
// 發生信號處理的地方,轉發給各sHandlerStack的handleSignal ok
    void SignalHandler::signalHandler(int sig, siginfo_t *info, void *uc) {
        ALOGV("Entered signal handler.");
// All the exception signals are blocked at this point.
        std::unique_lock<std::mutex> lock(sHandlerStackMutex);

        for (auto it = sHandlerStack->rbegin(); it != sHandlerStack->rend(); ++it) {
            (*it)->handleSignal(sig, info, uc);
        }

        lock.unlock();
    }


    SignalHandler::SignalHandler() {
        //上鎖,todo
        std::lock_guard<std::mutex> lock(sHandlerStackMutex);

        //建一個sHandlerStack
        if (!sHandlerStack)
            sHandlerStack = new std::vector<SignalHandler *>;

        //todo
        installAlternateStackLocked();
        //todo
        installHandlersLocked();
        //將自己放進去
        sHandlerStack->push_back(this);
    }

    SignalHandler::~SignalHandler() {
        std::lock_guard<std::mutex> lock(sHandlerStackMutex);

        auto it = std::find(sHandlerStack->begin(), sHandlerStack->end(), this);
        sHandlerStack->erase(it);
        if (sHandlerStack->empty()) {
            delete sHandlerStack;
            sHandlerStack = nullptr;
            restoreAlternateStackLocked();
            restoreHandlersLocked();
        }
    }

}   // namespace MatrixTracer

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

標籤:其他

上一篇:Android開發、adb、monkey測驗

下一篇:【Android逆向】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