├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── kotlinc.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── anr-library ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── publish_anr.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── me │ └── bytebeats │ └── anr │ ├── AnrError.kt │ ├── AnrInterceptor.kt │ ├── AnrListener.kt │ ├── AnrLog.kt │ ├── AnrMonitor.kt │ ├── AnrMonitor2.kt │ ├── OnInterruptedListener.kt │ ├── StackTraceCollector.kt │ └── ThreadComparator.kt ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── bytebeats │ │ └── anrmonitor │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── me │ │ │ └── bytebeats │ │ │ └── anrmonitor │ │ │ ├── APMApplication.kt │ │ │ ├── DeadLockUtils.kt │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── me │ └── bytebeats │ └── anrmonitor │ └── ExampleUnitTest.kt ├── build.gradle ├── deadlock-library ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── publish_deadlock.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ ├── CMakeLists.txt │ ├── deadlock_lib.cpp │ ├── dl_open.c │ └── dl_open.h │ └── java │ └── me │ └── bytebeats │ └── deadlock │ ├── DeadLockError.java │ ├── DeadLockListener.java │ ├── DeadLockMonitor.java │ ├── DeadLockStackTraces.java │ └── ReflectUtil.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lags-library ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── publish_lags.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── me │ └── bytebeats │ └── lag │ ├── AnrProsecutor.kt │ ├── FrameJankDetector.kt │ ├── LagLog.kt │ ├── LagMonitor.kt │ ├── LagMonitorLifecycle.kt │ ├── OnFrameJankListener.kt │ ├── OnProcessNotRespondingListener.kt │ ├── OnUIThreadBlockListener.kt │ └── UILooperPrinter.kt ├── local_setting_reader.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Anr-Monitor -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Chen Pan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anr-Monitor 2 | Android Performance Tools. ANR, Dead locks and lags. 3 | 4 |
Android性能监控工具. 包括了对 ANR 信息的收集, 死锁时线程信息的收集以及卡顿的监测. 5 | 6 | ## ANR信息收集原理 7 |
通过在后台开启一条线程, 只要没有被打断, 就一直在循环监测. 8 |
每一次循环开始的时候, 向`Handler(Looper.getMainLooper())`发送一条消息, 用于取消收集 ANR 的开始的标签. 9 |
然后子线程休息指定时间(可以自由设置). 10 |
如果此时主线程卡顿了指定时间, 那么子线程从休眠中唤醒, 此时收集 ANR 的标签没有取消, 则开始收集线程(全部线程或者主线程或者特定类型的线程)的堆栈信息, 通过回调接收. 11 | 12 | ## 卡顿的监测原理 13 |
通过 `Choreographer#postFrameCallback` 或者 `Looper.gerMainLooper().setPrinter` 方式, 14 |
传入自己的 `FrameCallback` 或者 `Printer. 15 |
通过对`doFrame(frameTimeNanos)/println(message)` 计算每一次 `doFrame/println` 的时间间隔, 16 |
计算丢帧或者 `Looper` 中每一个 `Message` 执行的耗时 17 |
从而判断应用的卡顿情况. 18 | 19 | ## 死锁时线程信息的收集原理 20 |
主要是在发生死锁时, 通过 Android 系统底层函数 `Monitor#GetLockOwnerThreadId` 或者锁此时的持有者线程(通过反射该线程的 `nativePeer` 从当前线程查找) 21 |
通过 `Thread#enumerate(Thread[])` 获取全部线程, 并利用算法查找此时竞争同一把锁的线程竞争闭环, 从而找出死锁的信息. 22 |
因为需要 Hook 底层 `Monitor#GetLockOwnerThreadId` 函数, 所以使用了 `JNI` 和 `NDK` 技术. 23 | 24 | ## How to use? 25 |
使用指定地址的 `MavenCentral`: 26 |
在根`build.gradle`文件中, 添加: 27 | ``` 28 | repositories { 29 | maven { url('https://repo1.maven.org/maven2/') } 30 | ... 31 | } 32 | ``` 33 |
在 module `build.gradle` 文件中, 添加: 34 | ``` 35 | //load from maven central 36 | implementation('io.github.bytebeats:anr:1.0.0') 37 | implementation('io.github.bytebeats:lags:1.0.1') 38 | implementation('io.github.bytebeats:deadlock:1.0.0') 39 | ``` 40 |
在自定义的 `Application` 中: 41 | ``` 42 | class APMApplication : Application() { 43 | val anrMonitor = AnrMonitor(3000) 44 | val anrMonitor = AnrMonitor2(3000) 45 | 46 | val silentAnrListener = object : AnrListener { 47 | override fun onAppNotResponding(error: AnrError) { 48 | Log.d("anr-log", "onAppNotResponding", error) 49 | } 50 | } 51 | 52 | var duration = 4L 53 | 54 | override fun onCreate() { 55 | super.onCreate() 56 | AnrLog.logStackTrace = false 57 | anrMonitor.setIgnoreDebugger(true) 58 | .setReportAllThreads() 59 | .setAnrListener(object : AnrListener { 60 | override fun onAppNotResponding(error: AnrError) { 61 | AnrLog.logd("onAppNotResponding") 62 | AnrLog.logd(error) 63 | try { 64 | ObjectOutputStream(ByteArrayOutputStream()).writeObject(error) 65 | } catch (e: IOException) { 66 | throw RuntimeException(e) 67 | } 68 | AnrLog.logd("Anr Error was successfully serialized") 69 | throw error 70 | } 71 | }).setAnrInterceptor(object : AnrInterceptor { 72 | override fun intercept(duration: Long): Long { 73 | val ret = this@APMApplication.duration - duration 74 | if (ret > 0) { 75 | AnrLog.logd( 76 | "Intercepted ANR that is too short ($duration ms), postponing for $ret ms." 77 | ) 78 | } 79 | return ret 80 | } 81 | }) 82 | .setOnInterruptedListener(object : OnInterruptedListener { 83 | override fun onInterrupted(e: InterruptedException) { 84 | throw e 85 | } 86 | }) 87 | ProcessLifecycleOwner.get().lifecycle.addObserver(anrMonitor) 88 | 89 | val lagMonitor = LagMonitor.Builder(this.applicationContext) 90 | .setThresholdTimeMillis(3000L) 91 | .setLagLogEnabled(true) 92 | .setMonitorMode(LagMonitor.MonitorMode.UI) 93 | .setOnFrameJankListener(object : OnFrameJankListener { 94 | override fun onJank(janks: Int) { 95 | Log.d("lag-log", "janks: $janks") 96 | } 97 | }) 98 | .setOnUIThreadRunListener(object : OnUIThreadBlockListener { 99 | override fun onBlock(lagTime: Long, uiRunTime: Long) { 100 | Log.d("lag-log", "lagTime: $lagTime, uiRunTime: $uiRunTime") 101 | } 102 | }) 103 | .setOnProcessNotRespondingListener(object : OnProcessNotRespondingListener { 104 | override fun onNotResponding(processInfo: String?) { 105 | Log.d("lag-log", "processInfo: $processInfo") 106 | } 107 | }) 108 | .build() 109 | ProcessLifecycleOwner.get().lifecycle.addObserver(lagMonitor) 110 | } 111 | 112 | override fun onTerminate() { 113 | super.onTerminate() 114 | anrMonitor.onAppTerminate() 115 | } 116 | } 117 | ``` 118 | 119 | ## Stargazers over time 120 | 121 | [![Stargazers over time](https://starchart.cc/bytebeats/Anr-Monitor.svg)](https://starchart.cc/bytebeats/Anr-Monitor) 122 | 123 | ## MIT License 124 | 125 | Copyright (c) 2021 Chen Pan 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy 128 | of this software and associated documentation files (the "Software"), to deal 129 | in the Software without restriction, including without limitation the rights 130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 131 | copies of the Software, and to permit persons to whom the Software is 132 | furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in all 135 | copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 143 | SOFTWARE. 144 | 145 | -------------------------------------------------------------------------------- /anr-library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /anr-library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | ext { 7 | ANR_ARTIFACT_ID = 'anr' 8 | ANR_VERSION = '1.0.0' 9 | ANR_POM_DESCRIPTION = 'Android ANR Manager' 10 | } 11 | 12 | apply from: 'publish_anr.gradle' 13 | 14 | android { 15 | compileSdkVersion 30 16 | buildToolsVersion "30.0.3" 17 | 18 | defaultConfig { 19 | minSdkVersion 19 20 | targetSdkVersion 30 21 | versionCode 1 22 | versionName "1.0" 23 | 24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 25 | consumerProguardFiles "consumer-rules.pro" 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | } 39 | 40 | dependencies { 41 | 42 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 43 | implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" 44 | } -------------------------------------------------------------------------------- /anr-library/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/anr-library/consumer-rules.pro -------------------------------------------------------------------------------- /anr-library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /anr-library/publish_anr.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | //only build and setting gradle can use `plugins` 5 | //plugins { 6 | // id('signing') 7 | // id('maven-publish') 8 | //} 9 | 10 | task androidSourcesJar(type: Jar) { 11 | from android.sourceSets.main.java.source 12 | classifier('sources') 13 | } 14 | 15 | apply from: '../local_setting_reader.gradle' 16 | 17 | project.publishing { 18 | publications { 19 | release(MavenPublication) { 20 | // The coordinates of the library, being set from variables that 21 | // we'll set up in a moment 22 | groupId GROUP_ID 23 | artifactId ANR_ARTIFACT_ID 24 | version ANR_VERSION 25 | 26 | // Two artifacts, the `aar` and the sources 27 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") 28 | artifact androidSourcesJar 29 | 30 | // Self-explanatory metadata for the most part 31 | pom { 32 | name = ANR_ARTIFACT_ID 33 | description = ANR_POM_DESCRIPTION 34 | // If your project has a dedicated site, use its URL here 35 | url = POM_URL 36 | licenses { 37 | license { 38 | //协议类型,一般默认Apache License2.0的话不用改: 39 | name = LICENSE_NAME 40 | url = LICENSE_URL 41 | } 42 | } 43 | developers { 44 | developer { 45 | id = DEVELOPER_ID 46 | name = DEVELOPER_NAME 47 | email = DEVELOPER_EMAIL 48 | } 49 | } 50 | // Version control info, if you're using GitHub, follow the format as seen here 51 | scm { 52 | //修改成你的Git地址: 53 | connection = SCM_CONNECTION 54 | developerConnection = SCM_DEVELOPER_CONNECTION 55 | //分支地址: 56 | url = SCM_URL 57 | } 58 | // A slightly hacky fix so that your POM will include any transitive dependencies 59 | // that your library builds upon 60 | withXml { 61 | def dependenciesNode = asNode().appendNode('dependencies') 62 | 63 | project.configurations.implementation.allDependencies.each { 64 | def dependencyNode = dependenciesNode.appendNode('dependency') 65 | dependencyNode.appendNode('groupId', it.group) 66 | dependencyNode.appendNode('artifactId', it.name) 67 | dependencyNode.appendNode('version', it.version) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | repositories { 74 | // The repository to publish to, Sonatype/MavenCentral 75 | maven { 76 | // This is an arbitrary name, you may also use "mavencentral" or 77 | // any other name that's descriptive for you 78 | name = "anr" 79 | 80 | // You only need this if you want to publish snapshots, otherwise just set the URL 81 | // to the release repo directly 82 | url = version.endsWith('SNAPSHOT') ? SNAPSHOTS_REPO_URL : RELEASES_REPO_URL 83 | 84 | // The username and password we've fetched earlier 85 | credentials { 86 | username ossrhUsername 87 | password ossrhPassword 88 | } 89 | } 90 | } 91 | } 92 | signing { 93 | sign publishing.publications 94 | } -------------------------------------------------------------------------------- /anr-library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/AnrError.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | import android.os.Looper 4 | 5 | /** 6 | * Created by bytebeats on 2021/6/30 : 11:42 7 | * E-mail: happychinapc@gmail.com 8 | * Quote: Peasant. Educated. Worker 9 | */ 10 | 11 | class AnrError( 12 | private val stackTraceThrowable: StackTraceCollector.StackTraceThrowable, 13 | private val duration: Long 14 | ) : Error("Application Not Responding for at least $duration ms.", stackTraceThrowable) { 15 | 16 | override fun fillInStackTrace(): Throwable { 17 | stackTrace = emptyArray() 18 | return this 19 | } 20 | 21 | companion object { 22 | private fun threadTitle(thread: Thread): String { 23 | return "${thread.name} (state = ${thread.state})" 24 | } 25 | 26 | fun newMainInstance(duration: Long): AnrError { 27 | val mainThread = Looper.getMainLooper().thread 28 | val stackTraces = mainThread.stackTrace 29 | return AnrError( 30 | StackTraceCollector( 31 | threadTitle(mainThread), 32 | stackTraces 33 | ).StackTraceThrowable(null), duration 34 | ) 35 | } 36 | 37 | fun newInstance( 38 | duration: Long, 39 | prefix: String, 40 | logThreadsWithoutStackTrace: Boolean 41 | ): AnrError { 42 | val mainThread = Looper.getMainLooper().thread 43 | val threadStackTraces = 44 | sortedMapOf>(ThreadComparator()) 45 | for (entry in Thread.getAllStackTraces().entries) { 46 | if (entry.key == mainThread || entry.key.name.startsWith(prefix) && (logThreadsWithoutStackTrace || !entry.value.isNullOrEmpty())) { 47 | threadStackTraces[entry.key] = entry.value 48 | } 49 | } 50 | /** 51 | * Sometimes main thread is not returned by {@link Thread#getAllStackTraces()} -- ensure that we have it. 52 | */ 53 | if (!threadStackTraces.containsKey(mainThread)) { 54 | threadStackTraces[mainThread] = mainThread.stackTrace 55 | } 56 | var throwable: StackTraceCollector.StackTraceThrowable? = null 57 | for (entry in threadStackTraces.entries) { 58 | throwable = StackTraceCollector(threadTitle(entry.key), entry.value).StackTraceThrowable(throwable) 59 | } 60 | return AnrError(throwable!!, duration) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/AnrInterceptor.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | /** 4 | * Created by bytebeats on 2021/6/30 : 11:29 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | /** 10 | * To report ANR immediately or lazily when ANRs is detected. 11 | */ 12 | interface AnrInterceptor { 13 | /** 14 | * 15 | * Called when main thread has froze more time than defined by the timeout. 16 | * @param duration The minimum time (in ms) the main thread has been frozen (may be more). 17 | * @return 0 or negative if the ANR should be reported immediately. A positive number of ms to postpone the reporting. 18 | */ 19 | fun intercept(duration: Long): Long 20 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/AnrListener.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | /** 4 | * Created by bytebeats on 2021/6/30 : 12:24 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | /** 10 | * To detect whether ANR happened 11 | */ 12 | interface AnrListener { 13 | /** 14 | * Called when an ANR is detected. 15 | * 16 | * @param error The error describing the ANR. 17 | */ 18 | fun onAppNotResponding(error: AnrError) 19 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/AnrLog.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * Created by bytebeats on 2021/6/30 : 14:27 7 | * E-mail: happychinapc@gmail.com 8 | * Quote: Peasant. Educated. Worker 9 | */ 10 | 11 | object AnrLog { 12 | var logStackTrace = true 13 | private const val TAG = "anr-log" 14 | 15 | private enum class LEVEL { 16 | V, D, I, W, E; 17 | } 18 | 19 | fun logv(message: Any?) = logv(TAG, message) 20 | fun logd(message: Any?) = logd(TAG, message) 21 | fun logi(message: Any?) = logi(TAG, message) 22 | fun logw(message: Any?) = logw(TAG, message) 23 | fun loge(message: Any?) = loge(TAG, message) 24 | 25 | fun logv(tag: String, message: Any?) = log(LEVEL.V, tag, message.toString()) 26 | fun logd(tag: String, message: Any?) = log(LEVEL.D, tag, message.toString()) 27 | fun logi(tag: String, message: Any?) = log(LEVEL.I, tag, message.toString()) 28 | fun logw(tag: String, message: Any?) = log(LEVEL.W, tag, message.toString()) 29 | fun loge(tag: String, message: Any?) = log(LEVEL.E, tag, message.toString()) 30 | 31 | private fun log(level: LEVEL, tag: String, message: String) { 32 | val tagBuilder = StringBuilder() 33 | tagBuilder.append(tag) 34 | if (logStackTrace) { 35 | val stackTrace = Thread.currentThread().stackTrace[5] 36 | tagBuilder.append(" ${stackTrace.methodName}(${stackTrace.fileName}:${stackTrace.lineNumber})") 37 | } 38 | when (level) { 39 | LEVEL.V -> Log.v(tagBuilder.toString(), message) 40 | LEVEL.D -> Log.d(tagBuilder.toString(), message) 41 | LEVEL.I -> Log.i(tagBuilder.toString(), message) 42 | LEVEL.W -> Log.w(tagBuilder.toString(), message) 43 | LEVEL.E -> Log.e(tagBuilder.toString(), message) 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/AnrMonitor.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | import android.os.Debug 4 | import android.os.Handler 5 | import android.os.Looper 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleEventObserver 8 | import androidx.lifecycle.LifecycleOwner 9 | 10 | /** 11 | * Created by bytebeats on 2021/6/30 : 12:26 12 | * E-mail: happychinapc@gmail.com 13 | * Quote: Peasant. Educated. Worker 14 | */ 15 | 16 | /** 17 | * Constructs a watchdog that checks the ui thread every given interval 18 | * 19 | * @param timeoutInterval The interval, in milliseconds, between to checks of the UI thread. 20 | * It is therefore the maximum time the UI may freeze before being reported as ANR. 21 | */ 22 | class AnrMonitor(private val timeoutInterval: Long = DEFAULT_ANR_TIMEOUT) : Thread(), 23 | LifecycleEventObserver { 24 | 25 | init { 26 | name = "||ANR-Monitor||" 27 | } 28 | 29 | private var mAnrInterceptor: AnrInterceptor? = DEFAULT_ANR_INTERCEPTOR 30 | private var mAnrListener: AnrListener? = DEFAULT_ANR_LISTENER 31 | private var mInterruptedListener: OnInterruptedListener? = DEFAULT_INTERRUPTION_LISTENER 32 | 33 | private val mainHandler = Handler(Looper.getMainLooper()) 34 | 35 | private var mPrefix: String? = "" 36 | private var mLogThreadWithoutStackTrace = false 37 | private var mIgnoreDebugger = false 38 | 39 | @Volatile 40 | private var mTick = 0L 41 | 42 | @Volatile 43 | private var mReported = false 44 | 45 | @Volatile 46 | private var mStopped = false 47 | 48 | private val mTicker = Runnable { 49 | mTick = 0 50 | mReported = false 51 | } 52 | 53 | fun setAnrListener(anrListener: AnrListener?): AnrMonitor { 54 | mAnrListener = anrListener ?: DEFAULT_ANR_LISTENER 55 | return this 56 | } 57 | 58 | fun setAnrInterceptor(anrInterceptor: AnrInterceptor?): AnrMonitor { 59 | mAnrInterceptor = anrInterceptor ?: DEFAULT_ANR_INTERCEPTOR 60 | return this 61 | } 62 | 63 | fun setOnInterruptedListener(onInterruptedListener: OnInterruptedListener?): AnrMonitor { 64 | mInterruptedListener = onInterruptedListener ?: DEFAULT_INTERRUPTION_LISTENER 65 | return this 66 | } 67 | 68 | /** 69 | * Set the prefix that a thread's name must have for the thread to be reported. 70 | * Note that the main thread is always reported. 71 | * Default "". 72 | * 73 | * @param prefix The thread name's prefix for a thread to be reported. 74 | * @return itself for chaining. 75 | */ 76 | fun setReportThreadNamePrefix(prefix: String): AnrMonitor { 77 | mPrefix = prefix 78 | return this 79 | } 80 | 81 | /** 82 | * Set that only the main thread will be reported. 83 | * 84 | * @return itself for chaining. 85 | */ 86 | fun setReportMainThreadOnly(): AnrMonitor { 87 | mPrefix = null 88 | return this 89 | } 90 | 91 | /** 92 | * Set that all threads will be reported (default behaviour). 93 | * 94 | * @return itself for chaining. 95 | */ 96 | fun setReportAllThreads(): AnrMonitor { 97 | mPrefix = "" 98 | return this 99 | } 100 | 101 | /** 102 | * Set that all running threads will be reported, 103 | * even those from which no stack trace could be extracted. 104 | * Default false. 105 | * 106 | * @param logThreadsWithoutStackTrace Whether or not all running threads should be reported 107 | * @return itself for chaining. 108 | */ 109 | fun setLogThreadWithoutStackTrace(logThreadsWithoutStackTrace: Boolean): AnrMonitor { 110 | mLogThreadWithoutStackTrace = logThreadsWithoutStackTrace 111 | return this 112 | } 113 | 114 | /** 115 | * Set whether to ignore the debugger when detecting ANRs. 116 | * When ignoring the debugger, ANRWatchdog will detect ANRs even if the debugger is connected. 117 | * By default, it does not, to avoid interpreting debugging pauses as ANRs. 118 | * Default false. 119 | * 120 | * @param ignoreDebugger Whether to ignore the debugger. 121 | * @return itself for chaining. 122 | */ 123 | fun setIgnoreDebugger(ignoreDebugger: Boolean): AnrMonitor { 124 | mIgnoreDebugger = ignoreDebugger 125 | return this 126 | } 127 | 128 | override fun run() { 129 | var interval = timeoutInterval 130 | while (!isInterrupted) { 131 | if (mStopped) { 132 | try { 133 | Thread.sleep(timeoutInterval) 134 | } catch (e: InterruptedException) { 135 | mInterruptedListener?.onInterrupted(e) 136 | return 137 | } 138 | continue 139 | } 140 | val needPost = mTick == 0L 141 | mTick += interval 142 | if (needPost) { 143 | mainHandler.post(mTicker) 144 | } 145 | try { 146 | Thread.sleep(interval) 147 | } catch (e: InterruptedException) { 148 | mInterruptedListener?.onInterrupted(e) 149 | return 150 | } 151 | // If the main thread has not handled _ticker, it is blocked. ANR. 152 | if (mTick != 0L && !mReported) { 153 | //noinspection ConstantConditions 154 | if (!mIgnoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) { 155 | AnrLog.logd("An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))") 156 | mReported = true 157 | continue 158 | } 159 | interval = mAnrInterceptor?.intercept(mTick) ?: 0 160 | if (interval > 0) { 161 | continue 162 | } 163 | val anrError = if (mPrefix == null) { 164 | AnrError.newMainInstance(mTick) 165 | } else { 166 | AnrError.newInstance(mTick, mPrefix!!, mLogThreadWithoutStackTrace) 167 | } 168 | mAnrListener?.onAppNotResponding(anrError) 169 | interval = timeoutInterval 170 | mReported = true 171 | } 172 | } 173 | } 174 | 175 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 176 | if (event == Lifecycle.Event.ON_CREATE) {//app is created 177 | onAppCreate() 178 | } else if (event == Lifecycle.Event.ON_STOP) {//no activities in stack 179 | onAppStop() 180 | } else if (event == Lifecycle.Event.ON_START) { 181 | onAppStart() 182 | } 183 | } 184 | 185 | private fun onAppStart() { 186 | mStopped = false 187 | } 188 | 189 | private fun onAppCreate() { 190 | this.start() 191 | } 192 | 193 | private fun onAppStop() { 194 | mStopped = true 195 | } 196 | 197 | /** 198 | * When {@see Application#onTerminate()} is called, invoke this method manually. 199 | */ 200 | fun onAppTerminate() { 201 | interrupt() 202 | } 203 | 204 | @Synchronized 205 | override fun start() { 206 | if (isStarted()) { 207 | return 208 | } 209 | super.start() 210 | } 211 | 212 | private fun isStarted(): Boolean { 213 | return state.ordinal > State.NEW.ordinal && state.ordinal < State.TERMINATED.ordinal 214 | } 215 | 216 | companion object { 217 | const val DEFAULT_ANR_TIMEOUT = 5000L 218 | 219 | private val DEFAULT_ANR_LISTENER = object : AnrListener { 220 | override fun onAppNotResponding(error: AnrError) { 221 | throw error 222 | } 223 | } 224 | 225 | private val DEFAULT_INTERRUPTION_LISTENER = object : OnInterruptedListener { 226 | override fun onInterrupted(e: InterruptedException) { 227 | AnrLog.logd(e.message) 228 | } 229 | } 230 | 231 | private val DEFAULT_ANR_INTERCEPTOR = object : AnrInterceptor { 232 | override fun intercept(duration: Long): Long { 233 | return 0 234 | } 235 | } 236 | } 237 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/AnrMonitor2.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | import android.os.Debug 4 | import android.os.Handler 5 | import android.os.HandlerThread 6 | import android.os.Looper 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleEventObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | 11 | /** 12 | * Created by bytebeats on 2021/6/30 : 12:26 13 | * E-mail: happychinapc@gmail.com 14 | * Quote: Peasant. Educated. Worker 15 | */ 16 | 17 | /** 18 | * Constructs a watchdog that checks the ui thread every given interval 19 | * 20 | * @param timeoutInterval The interval, in milliseconds, between to checks of the UI thread. 21 | * It is therefore the maximum time the UI may freeze before being reported as ANR. 22 | */ 23 | class AnrMonitor2(private val timeoutInterval: Long = DEFAULT_ANR_TIMEOUT) : 24 | LifecycleEventObserver { 25 | private val mainHandler: Handler = Handler(Looper.getMainLooper()) 26 | private var handlerThread: HandlerThread? = null 27 | private var backgroundHandler: Handler? = null 28 | 29 | init { 30 | handlerThread = object : HandlerThread(TAG, Thread.NORM_PRIORITY) { 31 | override fun onLooperPrepared() { 32 | super.onLooperPrepared() 33 | backgroundHandler = Handler(handlerThread!!.looper) 34 | } 35 | } 36 | } 37 | 38 | private var mAnrInterceptor: AnrInterceptor? = DEFAULT_ANR_INTERCEPTOR 39 | private var mAnrListener: AnrListener? = DEFAULT_ANR_LISTENER 40 | 41 | private var mPrefix: String? = "" 42 | private var mLogThreadWithoutStackTrace = false 43 | private var mIgnoreDebugger = false 44 | 45 | @Volatile 46 | private var mTick = 0L 47 | 48 | @Volatile 49 | private var mReported = false 50 | 51 | @Volatile 52 | private var mInterval: Long = timeoutInterval 53 | 54 | private val mTicker = Runnable { 55 | mTick = 0 56 | mReported = false 57 | } 58 | 59 | private val mAnrCollector = Runnable { 60 | // If the main thread has not handled mTicker, it is blocked. ANR. 61 | if (mTick != 0L && !mReported) { 62 | //noinspection ConstantConditions 63 | if (!mIgnoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) { 64 | AnrLog.logd("An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))") 65 | mReported = true 66 | delayToTryCollectingAnr() 67 | return@Runnable 68 | } 69 | mInterval = mAnrInterceptor?.intercept(mTick) ?: 0 70 | if (mInterval > 0) { 71 | delayToTryCollectingAnr() 72 | return@Runnable 73 | } 74 | val anrError = if (mPrefix == null) { 75 | AnrError.newMainInstance(mTick) 76 | } else { 77 | AnrError.newInstance(mTick, mPrefix!!, mLogThreadWithoutStackTrace) 78 | } 79 | mAnrListener?.onAppNotResponding(anrError) 80 | mInterval = timeoutInterval 81 | mReported = true 82 | } 83 | delayToTryCollectingAnr() 84 | } 85 | 86 | fun setAnrListener(anrListener: AnrListener?): AnrMonitor2 { 87 | mAnrListener = anrListener ?: DEFAULT_ANR_LISTENER 88 | return this 89 | } 90 | 91 | fun setAnrInterceptor(anrInterceptor: AnrInterceptor?): AnrMonitor2 { 92 | mAnrInterceptor = anrInterceptor ?: DEFAULT_ANR_INTERCEPTOR 93 | return this 94 | } 95 | 96 | /** 97 | * Set the prefix that a thread's name must have for the thread to be reported. 98 | * Note that the main thread is always reported. 99 | * Default "". 100 | * 101 | * @param prefix The thread name's prefix for a thread to be reported. 102 | * @return itself for chaining. 103 | */ 104 | fun setReportThreadNamePrefix(prefix: String): AnrMonitor2 { 105 | mPrefix = prefix 106 | return this 107 | } 108 | 109 | /** 110 | * Set that only the main thread will be reported. 111 | * 112 | * @return itself for chaining. 113 | */ 114 | fun setReportMainThreadOnly(): AnrMonitor2 { 115 | mPrefix = null 116 | return this 117 | } 118 | 119 | /** 120 | * Set that all threads will be reported (default behaviour). 121 | * 122 | * @return itself for chaining. 123 | */ 124 | fun setReportAllThreads(): AnrMonitor2 { 125 | mPrefix = "" 126 | return this 127 | } 128 | 129 | /** 130 | * Set that all running threads will be reported, 131 | * even those from which no stack trace could be extracted. 132 | * Default false. 133 | * 134 | * @param logThreadsWithoutStackTrace Whether or not all running threads should be reported 135 | * @return itself for chaining. 136 | */ 137 | fun setLogThreadWithoutStackTrace(logThreadsWithoutStackTrace: Boolean): AnrMonitor2 { 138 | mLogThreadWithoutStackTrace = logThreadsWithoutStackTrace 139 | return this 140 | } 141 | 142 | /** 143 | * Set whether to ignore the debugger when detecting ANRs. 144 | * When ignoring the debugger, ANRWatchdog will detect ANRs even if the debugger is connected. 145 | * By default, it does not, to avoid interpreting debugging pauses as ANRs. 146 | * Default false. 147 | * 148 | * @param ignoreDebugger Whether to ignore the debugger. 149 | * @return itself for chaining. 150 | */ 151 | fun setIgnoreDebugger(ignoreDebugger: Boolean): AnrMonitor2 { 152 | mIgnoreDebugger = ignoreDebugger 153 | return this 154 | } 155 | 156 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 157 | if (event == Lifecycle.Event.ON_CREATE) {//app is created 158 | onAppCreate() 159 | } else if (event == Lifecycle.Event.ON_STOP) {//no activities in stack 160 | onAppStop() 161 | } else if (event == Lifecycle.Event.ON_START) {//when first activity is started 162 | onAppStart() 163 | } 164 | } 165 | 166 | private fun onAppStart() { 167 | delayToTryCollectingAnr() 168 | } 169 | 170 | @Synchronized 171 | private fun delayToTryCollectingAnr() { 172 | val needPost = mTick == 0L 173 | mTick += mInterval 174 | if (needPost) { 175 | mainHandler.post(mTicker) 176 | } 177 | backgroundHandler?.postDelayed(mAnrCollector, ANR_COLLECTING_INTERVAL) 178 | } 179 | 180 | private fun onAppCreate() { 181 | handlerThread?.start() 182 | } 183 | 184 | private fun onAppStop() { 185 | backgroundHandler?.removeCallbacksAndMessages(null) 186 | mainHandler.removeCallbacksAndMessages(null) 187 | } 188 | 189 | /** 190 | * When {@see Application#onTerminate()} is called, invoke this method manually. 191 | * It will not be invoke. Because {@see Application#onTerminate()} is for use in emulated process environments. 192 | * It will never be called on a production Android device, where processes are 193 | * removed by simply killing them; no user code (including this callback) 194 | * is executed when doing so. 195 | */ 196 | fun onAppTerminate() { 197 | handlerThread?.quitSafely() 198 | handlerThread = null 199 | } 200 | 201 | companion object { 202 | const val DEFAULT_ANR_TIMEOUT = 5000L 203 | const val ANR_COLLECTING_INTERVAL = 2000L 204 | private const val TAG = "||ANR-Monitor||" 205 | 206 | private val DEFAULT_ANR_LISTENER = object : AnrListener { 207 | override fun onAppNotResponding(error: AnrError) { 208 | throw error 209 | } 210 | } 211 | 212 | private val DEFAULT_ANR_INTERCEPTOR = object : AnrInterceptor { 213 | override fun intercept(duration: Long): Long { 214 | return 0 215 | } 216 | } 217 | } 218 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/OnInterruptedListener.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | /** 4 | * Created by bytebeats on 2021/6/30 : 11:36 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | /** 10 | * Anr-monitor run on a Thread which may be interrupted. 11 | */ 12 | interface OnInterruptedListener { 13 | /** 14 | * Called when Anr-monitor Thread is interrupted. 15 | */ 16 | fun onInterrupted(e: InterruptedException) 17 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/StackTraceCollector.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * Created by bytebeats on 2021/6/30 : 11:47 7 | * E-mail: happychinapc@gmail.com 8 | * Quote: Peasant. Educated. Worker 9 | */ 10 | 11 | class StackTraceCollector( 12 | private val name: String, 13 | private val stackTraces: Array 14 | ) : Serializable { 15 | inner class StackTraceThrowable(private val other: StackTraceThrowable?) : 16 | Throwable(name, other) { 17 | override fun fillInStackTrace(): Throwable { 18 | stackTrace = stackTraces 19 | return this 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /anr-library/src/main/java/me/bytebeats/anr/ThreadComparator.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anr 2 | 3 | import android.os.Looper 4 | import java.util.* 5 | 6 | /** 7 | * Created by bytebeats on 2021/6/30 : 12:08 8 | * E-mail: happychinapc@gmail.com 9 | * Quote: Peasant. Educated. Worker 10 | */ 11 | 12 | /** 13 | * Thread Comparator: Main Thread is first, others ordered by Thread names. 14 | */ 15 | class ThreadComparator : Comparator { 16 | override fun compare(o1: Thread, o2: Thread): Int { 17 | if (o1 == o2) return 0 18 | if (o1 == Looper.getMainLooper().thread) return 1 19 | if (o2 == Looper.getMainLooper().thread) return -1 20 | return o2.name.compareTo(o1.name) 21 | } 22 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | applicationId "me.bytebeats.anrmonitor" 12 | minSdkVersion 19 13 | targetSdkVersion 30 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 38 | implementation 'androidx.core:core-ktx:1.5.0' 39 | implementation 'androidx.appcompat:appcompat:1.3.0' 40 | implementation 'com.google.android.material:material:1.3.0' 41 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 42 | 43 | implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" 44 | 45 | // implementation(project(path: ':anr-library')) 46 | // implementation(project(path: ':lags-library')) 47 | // implementation project(path: ':deadlock-library') 48 | 49 | //load from maven central 50 | implementation('io.github.bytebeats:anr:1.0.0') 51 | implementation('io.github.bytebeats:lags:1.0.1') 52 | implementation('io.github.bytebeats:deadlock:1.0.0') 53 | 54 | testImplementation 'junit:junit:4.13.2' 55 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 56 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 57 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/me/bytebeats/anrmonitor/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anrmonitor 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("me.bytebeats.anrmonitor", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/me/bytebeats/anrmonitor/APMApplication.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anrmonitor 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import androidx.lifecycle.ProcessLifecycleOwner 6 | import me.bytebeats.anr.AnrError 7 | import me.bytebeats.anr.AnrInterceptor 8 | import me.bytebeats.anr.AnrListener 9 | import me.bytebeats.anr.AnrLog 10 | import me.bytebeats.anr.AnrMonitor 11 | import me.bytebeats.anr.AnrMonitor2 12 | import me.bytebeats.anr.OnInterruptedListener 13 | import me.bytebeats.lag.LagMonitor 14 | import me.bytebeats.lag.OnFrameJankListener 15 | import me.bytebeats.lag.OnProcessNotRespondingListener 16 | import me.bytebeats.lag.OnUIThreadBlockListener 17 | import java.io.ByteArrayOutputStream 18 | import java.io.IOException 19 | import java.io.ObjectOutputStream 20 | 21 | /** 22 | * Created by bytebeats on 2021/6/30 : 15:23 23 | * E-mail: happychinapc@gmail.com 24 | * Quote: Peasant. Educated. Worker 25 | */ 26 | 27 | class APMApplication : Application() { 28 | // val anrMonitor = AnrMonitor(3000) 29 | val anrMonitor = AnrMonitor2(3000) 30 | 31 | val silentAnrListener = object : AnrListener { 32 | override fun onAppNotResponding(error: AnrError) { 33 | Log.d("anr-log", "onAppNotResponding", error) 34 | } 35 | } 36 | 37 | var duration = 4L 38 | 39 | override fun onCreate() { 40 | super.onCreate() 41 | AnrLog.logStackTrace = false 42 | anrMonitor.setIgnoreDebugger(true) 43 | .setReportAllThreads() 44 | .setAnrListener(object : AnrListener { 45 | override fun onAppNotResponding(error: AnrError) { 46 | AnrLog.logd("onAppNotResponding") 47 | AnrLog.logd(error) 48 | try { 49 | ObjectOutputStream(ByteArrayOutputStream()).writeObject(error) 50 | } catch (e: IOException) { 51 | throw RuntimeException(e) 52 | } 53 | AnrLog.logd("Anr Error was successfully serialized") 54 | // throw error 55 | } 56 | }).setAnrInterceptor(object : AnrInterceptor { 57 | override fun intercept(duration: Long): Long { 58 | val ret = this@APMApplication.duration - duration 59 | if (ret > 0) { 60 | AnrLog.logd( 61 | "Intercepted ANR that is too short ($duration ms), postponing for $ret ms." 62 | ) 63 | } 64 | return ret 65 | } 66 | }) 67 | // .setOnInterruptedListener(object : OnInterruptedListener { 68 | // override fun onInterrupted(e: InterruptedException) { 69 | // throw e 70 | // } 71 | // }) 72 | ProcessLifecycleOwner.get().lifecycle.addObserver(anrMonitor) 73 | 74 | val lagMonitor = LagMonitor.Builder(this.applicationContext) 75 | .setThresholdTimeMillis(3000L) 76 | .setLagLogEnabled(true) 77 | .setMonitorMode(LagMonitor.MonitorMode.UI) 78 | .setOnFrameJankListener(object : OnFrameJankListener { 79 | override fun onJank(janks: Int) { 80 | Log.d("lag-log", "janks: $janks") 81 | } 82 | }) 83 | .setOnUIThreadRunListener(object : OnUIThreadBlockListener { 84 | override fun onBlock(lagTime: Long, uiRunTime: Long) { 85 | Log.d("lag-log", "lagTime: $lagTime, uiRunTime: $uiRunTime") 86 | } 87 | }) 88 | .setOnProcessNotRespondingListener(object : OnProcessNotRespondingListener { 89 | override fun onNotResponding(processInfo: String?) { 90 | Log.d("lag-log", "processInfo: $processInfo") 91 | } 92 | }) 93 | .build() 94 | // ProcessLifecycleOwner.get().lifecycle.addObserver(lagMonitor) 95 | } 96 | 97 | override fun onTerminate() { 98 | super.onTerminate() 99 | anrMonitor.onAppTerminate() 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/me/bytebeats/anrmonitor/DeadLockUtils.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anrmonitor 2 | 3 | import me.bytebeats.anr.AnrLog 4 | import kotlin.concurrent.thread 5 | 6 | /** 7 | * Created by bytebeats on 2021/6/30 : 18:30 8 | * E-mail: happychinapc@gmail.com 9 | * Quote: Peasant. Educated. Worker 10 | */ 11 | 12 | /** 13 | * utility class to create dead lock and anr 14 | */ 15 | object DeadLockUtils { 16 | private const val TAG = "DeadLockUtils" 17 | 18 | fun createDeadLockAnr() { 19 | val lock1 = Any() 20 | val lock2 = Any() 21 | thread() { 22 | synchronized(lock1) { 23 | Thread.sleep(100) 24 | synchronized(lock2) { 25 | AnrLog.logd("ANR: getLock2") 26 | } 27 | } 28 | } 29 | synchronized(lock2) { 30 | Thread.sleep(100) 31 | synchronized(lock1) { 32 | AnrLog.logd("ANR: getLock1") 33 | } 34 | } 35 | } 36 | 37 | fun createDeadLock() { 38 | val lock1 = Any() 39 | val lock2 = Any() 40 | thread(start = true) { 41 | synchronized(lock1) { 42 | Thread.sleep(100) 43 | synchronized(lock2) { 44 | AnrLog.logd("ANR: thead1") 45 | } 46 | } 47 | } 48 | thread(start = true) { 49 | synchronized(lock2) { 50 | Thread.sleep(100) 51 | synchronized(lock1) { 52 | AnrLog.logd("ANR: thead2") 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/me/bytebeats/anrmonitor/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anrmonitor 2 | 3 | import android.os.Bundle 4 | import android.os.Handler 5 | import android.util.Log 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import me.bytebeats.anr.AnrLog 9 | import me.bytebeats.deadlock.DeadLockMonitor 10 | 11 | /** 12 | * Created by bytebeats on 2021/6/28 : 19:55 13 | * E-mail: happychinapc@gmail.com 14 | * Quote: Peasant. Educated. Worker 15 | */ 16 | 17 | class MainActivity : AppCompatActivity(R.layout.activity_main) { 18 | private val deadLockAnr by lazy { findViewById(R.id.dead_lock_anr) } 19 | private val minAnrDuration by lazy { findViewById(R.id.min_anr_duration) } 20 | private val reportMode by lazy { findViewById(R.id.report_mode) } 21 | private val behavior by lazy { findViewById(R.id.behavior) } 22 | private val threadSleep by lazy { findViewById(R.id.thread_sleep) } 23 | private val infiniteLoop by lazy { findViewById(R.id.infinite_loop) } 24 | private val deadLock by lazy { findViewById(R.id.dead_lock) } 25 | private val deadLockMonitorTV by lazy { findViewById(R.id.dead_lock_monitor) } 26 | private val killme by lazy { findViewById(R.id.kill_my_process) } 27 | 28 | private val deadLockMonitor by lazy { 29 | DeadLockMonitor().setDeadLockListener { 30 | Log.i( 31 | "deadlock", 32 | it.toString() 33 | ) 34 | } 35 | } 36 | 37 | private val mMutex = Any() 38 | 39 | private fun sleepFor8s() { 40 | try { 41 | Thread.sleep(8L * 1000L) 42 | } catch (e: InterruptedException) { 43 | e.printStackTrace() 44 | } 45 | } 46 | 47 | private fun infiniteLoop() { 48 | var i = 0 49 | while (true) { 50 | i++ 51 | } 52 | } 53 | 54 | private inner class LockerThread() : Thread("Activity: Locker") { 55 | override fun run() { 56 | synchronized(mMutex) { 57 | while (true) sleepFor8s() 58 | } 59 | } 60 | } 61 | 62 | private fun deadLock() { 63 | LockerThread().start() 64 | Handler().post { synchronized(mMutex) { AnrLog.logd("There should be a dead lock before this message") } } 65 | } 66 | 67 | private var mode = 0 68 | private var crash = true 69 | 70 | override fun onCreate(savedInstanceState: Bundle?) { 71 | super.onCreate(savedInstanceState) 72 | val anrApp = application as APMApplication 73 | deadLockAnr.setOnClickListener { 74 | AnrLog.logd("createDeadLockAnr") 75 | DeadLockUtils.createDeadLockAnr() 76 | } 77 | deadLock.setOnClickListener { deadLock() } 78 | infiniteLoop.setOnClickListener { infiniteLoop() } 79 | threadSleep.setOnClickListener { sleepFor8s() } 80 | behavior.text = "Crash" 81 | behavior.setOnClickListener { 82 | crash = !crash 83 | if (crash) { 84 | behavior.text = "Crash" 85 | anrApp.anrMonitor.setAnrListener(null) 86 | } else { 87 | behavior.text = "Silent" 88 | anrApp.anrMonitor.setAnrListener(anrApp.silentAnrListener) 89 | } 90 | } 91 | reportMode.text = "All Threads" 92 | reportMode.setOnClickListener { 93 | mode = (mode + 1) % 3 94 | when (mode) { 95 | 0 -> { 96 | reportMode.text = "All Threads" 97 | anrApp.anrMonitor.setReportAllThreads() 98 | } 99 | 1 -> { 100 | reportMode.text = "Main Thread only" 101 | anrApp.anrMonitor.setReportMainThreadOnly() 102 | } 103 | 2 -> { 104 | reportMode.text = "Filtered" 105 | anrApp.anrMonitor.setReportThreadNamePrefix("Activity:") 106 | } 107 | } 108 | } 109 | minAnrDuration.text = "${anrApp.duration} seconds" 110 | minAnrDuration.setOnClickListener { 111 | anrApp.duration = anrApp.duration % 6 + 2 112 | minAnrDuration.text = "${anrApp.duration} seconds" 113 | } 114 | deadLockMonitorTV.setOnClickListener { 115 | deadLockMonitorTV.postDelayed({ 116 | deadLockMonitor.start() 117 | }, 200) 118 | DeadLockUtils.createDeadLock() 119 | } 120 | 121 | killme.setOnClickListener { 122 | finish() 123 | // android.os.Process.killProcess(android.os.Process.myPid()); 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 30 | 31 | 42 | 43 | 54 | 55 | 56 | 67 | 68 | 79 | 80 | 91 | 92 | 103 | 104 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Anr-Monitor 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/me/bytebeats/anrmonitor/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.anrmonitor 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.5.20" 4 | ext.lifecycle_version = "2.3.1" 5 | repositories { 6 | maven { url('https://repo1.maven.org/maven2/') } 7 | google() 8 | mavenCentral() 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:4.2.2' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | maven { url('https://repo1.maven.org/maven2/') } 23 | google() 24 | mavenCentral() 25 | jcenter() 26 | } 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | 33 | ext { 34 | GROUP_ID = 'io.github.bytebeats' 35 | DEVELOPER_ID = 'bytebeats' 36 | DEVELOPER_NAME = 'Chen Pan' 37 | DEVELOPER_EMAIL = 'happychinapc@gmail.com' 38 | SCM_CONNECTION = 'scm:git:github.com/bytebeats/Anr-Monitor.git' 39 | SCM_DEVELOPER_CONNECTION = 'scm:git:ssh:github.com/bytebeats/Anr-Monitor.git' 40 | SCM_URL = 'https://github.com/bytebeats/Anr-Monitor/tree/master' 41 | LICENSE_NAME = 'The Apache License, Version 2.0' 42 | LICENSE_URL = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 43 | POM_URL = 'https://github.com/bytebeats/Anr-Monitor' 44 | RELEASES_REPO_URL = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/' 45 | SNAPSHOTS_REPO_URL = 'https://s01.oss.sonatype.org/content/repositories/snapshots/' 46 | } -------------------------------------------------------------------------------- /deadlock-library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /deadlock-library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | 5 | ext { 6 | LAGS_ARTIFACT_ID = 'deadlock' 7 | LAGS_VERSION = '1.0.0' 8 | LAGS_POM_DESCRIPTION = 'Android deadlock checking' 9 | } 10 | 11 | apply from: 'publish_deadlock.gradle' 12 | 13 | android { 14 | compileSdkVersion 30 15 | buildToolsVersion "30.0.3" 16 | 17 | defaultConfig { 18 | minSdkVersion 19 19 | targetSdkVersion 30 20 | versionCode 1 21 | versionName "1.0" 22 | 23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 24 | consumerProguardFiles "consumer-rules.pro" 25 | 26 | externalNativeBuild { 27 | cmake { 28 | cppFlags "" 29 | } 30 | } 31 | 32 | } 33 | 34 | externalNativeBuild { 35 | cmake { 36 | path 'src/main/cpp/CMakeLists.txt' 37 | version('3.10.2') 38 | } 39 | } 40 | 41 | buildTypes { 42 | release { 43 | minifyEnabled false 44 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 45 | } 46 | } 47 | compileOptions { 48 | sourceCompatibility JavaVersion.VERSION_1_7 49 | targetCompatibility JavaVersion.VERSION_1_7 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" 55 | } -------------------------------------------------------------------------------- /deadlock-library/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/deadlock-library/consumer-rules.pro -------------------------------------------------------------------------------- /deadlock-library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /deadlock-library/publish_deadlock.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | //only build and setting gradle can use `plugins` 5 | //plugins { 6 | // id('signing') 7 | // id('maven-publish') 8 | //} 9 | 10 | task androidSourcesJar(type: Jar) { 11 | from android.sourceSets.main.java.source 12 | classifier('sources') 13 | } 14 | 15 | apply from: '../local_setting_reader.gradle' 16 | 17 | project.publishing { 18 | publications { 19 | release(MavenPublication) { 20 | // The coordinates of the library, being set from variables that 21 | // we'll set up in a moment 22 | groupId GROUP_ID 23 | artifactId LAGS_ARTIFACT_ID 24 | version LAGS_VERSION 25 | 26 | // Two artifacts, the `aar` and the sources 27 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") 28 | artifact androidSourcesJar 29 | 30 | // Self-explanatory metadata for the most part 31 | pom { 32 | name = LAGS_ARTIFACT_ID 33 | description = LAGS_POM_DESCRIPTION 34 | // If your project has a dedicated site, use its URL here 35 | url = POM_URL 36 | licenses { 37 | license { 38 | //协议类型,一般默认Apache License2.0的话不用改: 39 | name = LICENSE_NAME 40 | url = LICENSE_URL 41 | } 42 | } 43 | developers { 44 | developer { 45 | id = DEVELOPER_ID 46 | name = DEVELOPER_NAME 47 | email = DEVELOPER_EMAIL 48 | } 49 | } 50 | // Version control info, if you're using GitHub, follow the format as seen here 51 | scm { 52 | //修改成你的Git地址: 53 | connection = SCM_CONNECTION 54 | developerConnection = SCM_DEVELOPER_CONNECTION 55 | //分支地址: 56 | url = SCM_URL 57 | } 58 | // A slightly hacky fix so that your POM will include any transitive dependencies 59 | // that your library builds upon 60 | withXml { 61 | def dependenciesNode = asNode().appendNode('dependencies') 62 | 63 | project.configurations.implementation.allDependencies.each { 64 | def dependencyNode = dependenciesNode.appendNode('dependency') 65 | dependencyNode.appendNode('groupId', it.group) 66 | dependencyNode.appendNode('artifactId', it.name) 67 | dependencyNode.appendNode('version', it.version) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | repositories { 74 | // The repository to publish to, Sonatype/MavenCentral 75 | maven { 76 | // This is an arbitrary name, you may also use "mavencentral" or 77 | // any other name that's descriptive for you 78 | name = "deadlock" 79 | 80 | // You only need this if you want to publish snapshots, otherwise just set the URL 81 | // to the release repo directly 82 | url = version.endsWith('SNAPSHOT') ? SNAPSHOTS_REPO_URL : RELEASES_REPO_URL 83 | 84 | // The username and password we've fetched earlier 85 | credentials { 86 | username ossrhUsername 87 | password ossrhPassword 88 | } 89 | } 90 | } 91 | } 92 | signing { 93 | sign publishing.publications 94 | } -------------------------------------------------------------------------------- /deadlock-library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /deadlock-library/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.10.2) 7 | 8 | # Declares and names the project. 9 | 10 | project("deadlock-lib") 11 | 12 | # Creates and names a library, sets it as either STATIC 13 | # or SHARED, and provides the relative paths to its source code. 14 | # You can define multiple libraries, and CMake builds them for you. 15 | # Gradle automatically packages shared libraries with your APK. 16 | 17 | add_library( 18 | # Sets the name of the library. 19 | deadlock-lib 20 | # Sets the library as a shared library. 21 | SHARED 22 | # Provides a relative path to your source file(s). 23 | deadlock_lib.cpp dl_open.c 24 | ) 25 | 26 | # Searches for a specified prebuilt library and stores the path as a 27 | # variable. Because CMake includes system libraries in the search path by 28 | # default, you only need to specify the name of the public NDK library 29 | # you want to add. CMake verifies that the library exists before 30 | # completing its build. 31 | 32 | find_library( 33 | # Sets the name of the path variable. 34 | log-lib 35 | # Specifies the name of the NDK library that you want CMake to locate. 36 | log 37 | ) 38 | 39 | # Specifies libraries CMake should link to your target library. You 40 | # can link multiple libraries, such as libraries you define in this 41 | # build script, prebuilt third-party libraries, or system libraries. 42 | 43 | target_link_libraries( 44 | # Specifies the target library. 45 | deadlock-lib 46 | # Links the target library to the log library included in the NDK. 47 | ${log-lib} 48 | ) 49 | 50 | include_directories(dl_open.h) -------------------------------------------------------------------------------- /deadlock-library/src/main/cpp/deadlock_lib.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Created by bytebeats on 2021/7/3 : 15:22 4 | * E-mail: happychinapc@gmail.com 5 | * Quote: Peasant. Educated. Worker 6 | * 7 | */ 8 | 9 | #include 10 | #include 11 | #include "dl_open.h" 12 | 13 | #include 14 | 15 | #define TAG "deadlock-lib" 16 | #define Log_i(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) 17 | 18 | void *get_contended_monitor; 19 | void *get_lock_owner_thread_id; 20 | 21 | jint android_sdk_int; 22 | 23 | const char *get_lock_owner_symbol_name(jint sdk_int); 24 | 25 | extern "C" 26 | JNIEXPORT jint 27 | 28 | JNICALL 29 | Java_me_bytebeats_deadlock_DeadLockMonitor_nativeInit(JNIEnv *env, jobject thiz, jint sdk_int) { 30 | android_sdk_int = sdk_int; 31 | //dlopen libart.so 32 | //init 33 | ndk_init(env); 34 | 35 | //load libart.so 36 | void *so_addr = ndk_dl_open("libart.so", RTLD_NOLOAD); 37 | if (so_addr == nullptr) return 1; 38 | // Monitor::GetContendedMonitor 39 | //c++方法地址跟c不一样,c++可以重载,方法描述符会变 40 | //http://androidxref.com/8.0.0_r4/xref/system/core/libbacktrace/testdata/arm/libart.so 41 | 42 | // nm xxx.so 43 | 44 | //获取Monitor::GetContendedMonitor函数符号地址 45 | get_contended_monitor = ndk_dl_symbol(so_addr, 46 | "_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE"); 47 | if (get_contended_monitor == nullptr) return 2; 48 | // Monitor::GetLockOwnerThreadId 49 | //这个函数是用来获取 Monitor的持有者,拥有monitor的是线程 50 | get_lock_owner_thread_id = ndk_dl_symbol(so_addr, get_lock_owner_symbol_name(android_sdk_int)); 51 | if (get_lock_owner_thread_id == nullptr) return 3; 52 | return 0; 53 | } 54 | 55 | const char *get_lock_owner_symbol_name(jint 56 | sdk_int) { 57 | if (sdk_int <= 29) { 58 | //pre android 9.0 59 | //http://androidxref.com/9.0.0_r3/xref/system/core/libbacktrace/testdata/arm/libart.so 搜索 GetLockOwnerThreadId 60 | return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE"; 61 | } else { 62 | //android 10.0+ 63 | //Sorry, androidxref/.../libart.so is no available, and in Google Git, I haven't found this so library. 64 | return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE"; 65 | } 66 | } 67 | 68 | extern "C" 69 | JNIEXPORT jint 70 | 71 | JNICALL 72 | Java_me_bytebeats_deadlock_DeadLockMonitor_getContendedThreadIdArt(JNIEnv *env, jobject thiz, 73 | jlong thd_native_peer) { 74 | Log_i("getContendedThreadIdArt"); 75 | int monitor_thread_id = 0; 76 | if (get_contended_monitor != nullptr && get_lock_owner_thread_id != nullptr) { 77 | Log_i("get_contended_monitor != null"); 78 | //调用一下获取monitor的函数 79 | int monitorObj = ((int (*)(long)) get_contended_monitor)(thd_native_peer); 80 | if (monitorObj != 0) { 81 | Log_i("monitorObj != 0"); 82 | // 获取这个monitor的持有者,返回一个线程id 83 | monitor_thread_id = ((int (*)(long)) get_lock_owner_thread_id)(monitorObj); 84 | } else { 85 | monitor_thread_id = 0; 86 | } 87 | } else { 88 | Log_i("get_contended_monitor == null || get_lock_owner_thread_id == null"); 89 | } 90 | return monitor_thread_id; 91 | } 92 | 93 | extern "C" 94 | JNIEXPORT jint 95 | 96 | JNICALL 97 | Java_me_bytebeats_deadlock_DeadLockMonitor_getThreadIdFromNativePeer(JNIEnv *env, jobject thiz, 98 | jlong thd_native_peer) { 99 | Log_i("getThreadIdFromNativePeer"); 100 | if (thd_native_peer != 0) { 101 | Log_i("thread id != 0"); 102 | if (android_sdk_int > 20) {//Android 5.0 103 | //reinterpret_cast 强制类型转换 104 | int *pInt = reinterpret_cast(thd_native_peer); 105 | //地址 +3,就是ThreadId,这个怎么来的呢? 106 | pInt = pInt + 3; 107 | return *pInt;//返回 monitor 所使用的Thread id 108 | } 109 | } else { 110 | Log_i("suspendThreadArt failed"); 111 | } 112 | return 0; 113 | } 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /deadlock-library/src/main/cpp/dl_open.c: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Created by bytebeats on 2021/7/2 : 11:36 4 | * E-mail: happychinapc@gmail.com 5 | * Quote: Peasant. Educated. Worker 6 | * 7 | */ 8 | 9 | #include "dl_open.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define TAG "deadlock-dl-open" 17 | #define UNSUPPORTED "Not Supported" 18 | #define PAGE_SIZE 4096 19 | #define Log_i(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) 20 | 21 | static volatile int SDK_INT = 0; 22 | static void *quick_on_stack_back; 23 | 24 | static union { 25 | void *stub; 26 | 27 | void * 28 | (*quick_on_stack_replace)(const void *param1, const void *param2, const void *fake_trampoline, 29 | const void *called); 30 | } STUBS; 31 | 32 | void JNIEXPORT 33 | ndk_init(JNIEnv 34 | *env){ 35 | if(SDK_INT <= 0) { 36 | char sdk[PROP_VALUE_MAX]; 37 | __system_property_get("ro.build.version.sdk", sdk); 38 | SDK_INT = atoi(sdk); 39 | if(SDK_INT >= 24) { 40 | static __attribute__((__aligned__(PAGE_SIZE))) uint8_t __insns[PAGE_SIZE]; 41 | STUBS. 42 | stub = __insns; 43 | mprotect(__insns, 44 | sizeof(__insns), PROT_READ | PROT_WRITE | PROT_EXEC); 45 | // we are currently hijacking "FatalError" as a fake system-call trampoline 46 | uintptr_t pv = (uintptr_t)(*env)->FatalError; 47 | uintptr_t pu = (pv | (PAGE_SIZE - 1)) + 1u; 48 | uintptr_t pd = pv & ~(PAGE_SIZE - 1); 49 | mprotect((void *)pd, pv + 8u >= pu ? PAGE_SIZE * 2U : PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC); 50 | quick_on_stack_back = (void *) pv; 51 | #if defined(__i386__) 52 | /* 53 | DEFINE_FUNCTION art_quick_on_stack_replace 54 | movl 12(REG_VAR(esp)), REG_VAR(eax) 55 | movl (REG_VAR(esp)), REG_VAR(edx) 56 | movl REG_VAR(eax), (REG_VAR(esp)) 57 | movl REG_VAR(edx), 12(REG_VAR(esp)) 58 | pushl 16(REG_VAR(esp)) 59 | ret 60 | END_FUNCTION art_quick_on_stack_replace 61 | */ 62 | memcpy(__insns, "\x8B\x44\x24\x0C\x8B\x14\x24\x89\x04\x24\x89\x54\x24\x0C\xFF\x74\x24\x10\xC3", 19); 63 | /* 64 | DEFINE_FUNCTION art_quick_on_stack_back 65 | push 8(REG_VAR(esp)) 66 | ret 67 | END_FUNCTION art_quick_on_stack_back 68 | */ 69 | memcpy(quick_on_stack_back, "\xC3\xFF\x74\x24\x08\xC3", 6); 70 | quick_on_stack_back = (void *)(pv + 1);// inserts `ret` at first 71 | #elif defined(__x86_64__) 72 | // rdi, rsi, rdx, rcx, r8, r9 73 | /* 74 | 0x0000000000000000: 52 push rdx 75 | 0x0000000000000001: 52 push rdx 76 | 0x0000000000000002: FF E1 jmp rcx 77 | */ 78 | memcpy(__insns, "\x52\x52\xFF\xE1", 4); 79 | /* 80 | 0x0000000000000000: 5A pop rdx 81 | 0x0000000000000000: C3 ret 82 | */ 83 | memcpy(quick_on_stack_back, "\x5A\xC3", 2); 84 | #elif defined(__aarch64__) 85 | // x0~x7 86 | /* 87 | 0x0000000000000000: FD 7B BF A9 stp x29, x30, [sp, #-0x10]! 88 | 0x0000000000000004: FD 03 00 91 mov x29, sp 89 | 0x0000000000000008: FE 03 02 AA mov x30, x2 90 | 0x000000000000000C: 60 00 1F D6 br x3 91 | */ 92 | memcpy(__insns, "\xFD\x7B\xBF\xA9\xFD\x03\x00\x91\xFE\x03\x02\xAA\x60\x00\x1F\xD6", 16); 93 | /* 94 | 0x0000000000000000: FD 7B C1 A8 ldp x29, x30, [sp], #0x10 95 | 0x0000000000000004: C0 03 5F D6 ret 96 | */ 97 | memcpy(quick_on_stack_back, "\xFD\x7B\xC1\xA8\xC0\x03\x5F\xD6", 8); 98 | #elif defined(__arm__) 99 | // r0~r3 100 | /* 101 | 0x0000000000000000: 08 E0 2D E5 str lr, [sp, #-8]! 102 | 0x0000000000000004: 02 E0 A0 E1 mov lr, r2 103 | 0x0000000000000008: 13 FF 2F E1 bx r3 104 | */ 105 | memcpy(__insns, "\x08\xE0\x2D\xE5\x02\xE0\xA0\xE1\x13\xFF\x2F\xE1", 12); 106 | if((pv & 1u) != 0u) {//Thumb 107 | /* 108 | 0x0000000000000000: 0C BC pop {r2, r3} 109 | 0x0000000000000002: 10 47 bx r2 110 | */ 111 | memcpy((void *)(pv - 1), "\x0C\xBC\x10\x47", 4); 112 | } else { 113 | /* 114 | 0x0000000000000000: 0C 00 BD E8 pop {r2, r3} 115 | 0x0000000000000004: 12 FF 2F E1 bx r2 116 | */ 117 | memcpy(quick_on_stack_back, "\x0C\x00\xBD\xE8\x12\xFF\x2F\xE1", 8); 118 | } 119 | #else 120 | # error UNSUPPORTED 121 | #endif 122 | Log_i("init done! quick_on_stack_replace = %p, quick_on_stack_back = %p", STUBS.stub, 123 | quick_on_stack_back); 124 | } 125 | } 126 | } 127 | 128 | void *JNIEXPORT 129 | 130 | ndk_dl_open(const char *filename, int flag) { 131 | if (SDK_INT >= 24) { 132 | #if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || defined(__arm__) 133 | return STUBS.quick_on_stack_replace(filename, (void *)flag, quick_on_stack_back, dlopen); 134 | #else 135 | # error UNSUPPORTED 136 | #endif 137 | } 138 | return dlopen(filename, flag); 139 | } 140 | 141 | int JNIEXPORT 142 | 143 | ndk_dl_close(void *handle) { 144 | if (SDK_INT >= 24) { 145 | #if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || defined(__arm__) 146 | return (int)STUBS.quick_on_stack_replace(handle, NULL, quick_on_stack_back, dlclose); 147 | #else 148 | # error UNSUPPORTED 149 | #endif 150 | } 151 | return dlclose(handle); 152 | } 153 | 154 | const char *JNIEXPORT 155 | 156 | ndk_dl_error(void) { 157 | if (SDK_INT >= 24) { 158 | #if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || defined(__arm__) 159 | return STUBS.quick_on_stack_replace(NULL, NULL, quick_on_stack_back, dlerror); 160 | #else 161 | # error UNSUPPORTED 162 | #endif 163 | } 164 | return dlerror(); 165 | } 166 | 167 | void *JNIEXPORT 168 | 169 | ndk_dl_symbol(void *handle, const char *symbol) { 170 | if (SDK_INT >= 24) { 171 | #if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || defined(__arm__) 172 | return STUBS.quick_on_stack_replace(handle, symbol, quick_on_stack_back, dlsym); 173 | #else 174 | # error UNSUPPORTED 175 | #endif 176 | } 177 | return dlsym(handle, symbol); 178 | } 179 | 180 | int JNIEXPORT 181 | 182 | ndk_dl_addr(const void *addr, Dl_info *info) { 183 | if (SDK_INT >= 24) { 184 | #if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || defined(__arm__) 185 | return (int)STUBS.quick_on_stack_replace(addr, info, quick_on_stack_back, dladdr); 186 | #else 187 | # error UNSUPPORTED 188 | #endif 189 | } 190 | return dladdr(addr, info); 191 | } 192 | 193 | 194 | -------------------------------------------------------------------------------- /deadlock-library/src/main/cpp/dl_open.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Created by bytebeats on 2021/7/2 : 11:36 4 | * E-mail: happychinapc@gmail.com 5 | * Quote: Peasant. Educated. Worker 6 | * 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | void ndk_init(JNIEnv *env); 19 | 20 | void *ndk_dl_open(const char *filename, int flag); 21 | 22 | int ndk_dl_close(void *handle); 23 | 24 | const char *ndk_dl_error(void); 25 | 26 | void *ndk_dl_symbol(void *handle, const char *symbol); 27 | 28 | int ndk_dl_addr(const void *addr, Dl_info *info); 29 | 30 | #ifdef __cplusplus 31 | } 32 | #endif 33 | //#ifndef ANR_MONITOR_DL_OPEN_H 34 | //#define ANR_MONITOR_DL_OPEN_H 35 | 36 | //#endif //ANR_MONITOR_DL_OPEN_H 37 | -------------------------------------------------------------------------------- /deadlock-library/src/main/java/me/bytebeats/deadlock/DeadLockError.java: -------------------------------------------------------------------------------- 1 | package me.bytebeats.deadlock; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | /** 6 | * 7 | * Created by bytebeats on 2021/7/3 : 17:51 8 | * E-mail: happychinapc@gmail.com 9 | * Quote: Peasant. Educated. Worker 10 | * 11 | */ 12 | public class DeadLockError extends Error { 13 | public DeadLockError(Thread thread, DeadLockStackTraces.Error error) { 14 | super("Thread(id: " + thread.getId() + ", state = " + thread.getState().toString() + ")", error); 15 | } 16 | 17 | @NonNull 18 | @Override 19 | public synchronized Throwable fillInStackTrace() { 20 | setStackTrace(new StackTraceElement[0]); 21 | return this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /deadlock-library/src/main/java/me/bytebeats/deadlock/DeadLockListener.java: -------------------------------------------------------------------------------- 1 | package me.bytebeats.deadlock; 2 | 3 | /** 4 | * Created by bytebeats on 2021/7/3 : 19:27 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | public interface DeadLockListener { 9 | void onError(DeadLockError error); 10 | } 11 | -------------------------------------------------------------------------------- /deadlock-library/src/main/java/me/bytebeats/deadlock/DeadLockMonitor.java: -------------------------------------------------------------------------------- 1 | package me.bytebeats.deadlock; 2 | 3 | import android.os.Build.VERSION; 4 | import android.os.Build.VERSION_CODES; 5 | import android.util.Log; 6 | 7 | import java.lang.Thread.State; 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Set; 14 | 15 | /** 16 | * Created by bytebeats on 2021/7/2 : 11:24 17 | * E-mail: happychinapc@gmail.com 18 | * Quote: Peasant. Educated. Worker 19 | */ 20 | 21 | public class DeadLockMonitor { 22 | 23 | public static boolean logEnabled = true; 24 | private static final String TAG = "deadlock-java"; 25 | 26 | static { 27 | System.loadLibrary("deadlock-lib"); 28 | } 29 | 30 | private DeadLockListener listener; 31 | 32 | public DeadLockMonitor setDeadLockListener(DeadLockListener listener) { 33 | this.listener = listener; 34 | return this; 35 | } 36 | 37 | 38 | private static String threadName(Thread thread) { 39 | return thread.getName() + " (state = " + thread.getState().toString() + ")"; 40 | } 41 | 42 | Thread[] allThreads() { 43 | ThreadGroup group = Thread.currentThread().getThreadGroup(); 44 | if (group == null) { 45 | return null; 46 | } 47 | while (group.getParent() != null) { 48 | group = group.getParent(); 49 | } 50 | int threadCount = group.activeCount(); 51 | Thread[] allThreads = new Thread[threadCount]; 52 | group.enumerate(allThreads); 53 | return allThreads; 54 | } 55 | 56 | public void start() { 57 | if (VERSION.SDK_INT > VERSION_CODES.Q) { 58 | log("DeadLockMonitor won't work on Android 10, Sooooooorrrrrryyyyyyyy"); 59 | return; 60 | } 61 | int initResult = nativeInit(VERSION.SDK_INT); 62 | log("native init result: " + initResult); 63 | Map blockedThreads = new HashMap<>(); 64 | Thread[] allThreads = allThreads(); 65 | if (allThreads != null) { 66 | for (Thread thread : allThreads) { 67 | if (thread != null && thread.getState() == State.BLOCKED) { 68 | Object nativePeer = ReflectUtil.invokeField(thread, "nativePeer"); 69 | if (nativePeer == null) continue; 70 | long thdAddr = (long) nativePeer; 71 | if (thdAddr <= 0) { 72 | log("thread address not found"); 73 | continue; 74 | } 75 | log("blocked Thread[id = " + thread.getId() + ", address = " + thdAddr + "]"); 76 | int blockThreadId = getContendedThreadIdArt(thdAddr); 77 | int currentThreadId = getThreadIdFromNativePeer(thdAddr); 78 | log("current thread id = " + currentThreadId + ", blocked thread id = " + blockThreadId); 79 | if (currentThreadId != 0 && blockThreadId != 0) { 80 | blockedThreads.put(currentThreadId, new DeadLockThreadWrapper(currentThreadId, blockThreadId, thread)); 81 | } 82 | } 83 | } 84 | } 85 | List> blockedThreadGroups = blockedThreadGroups(blockedThreads); 86 | for (Map group : blockedThreadGroups) { 87 | for (Integer thdId : group.keySet()) { 88 | DeadLockThreadWrapper wrapper = blockedThreads.get(thdId); 89 | if (wrapper == null) { 90 | continue; 91 | } 92 | Thread waitThread = group.get(wrapper.blockedThreadId); 93 | Thread deadThread = group.get(wrapper.currentThreadId); 94 | if (waitThread == null) { 95 | continue; 96 | } 97 | log("waitThread.Name = " + waitThread.getName()); 98 | log("deadThread.Name = " + deadThread.getName()); 99 | DeadLockStackTraces.Error error = new DeadLockStackTraces(threadName(deadThread), deadThread.getStackTrace()).new Error(null); 100 | DeadLockError deadLockError = new DeadLockError(deadThread, error); 101 | if (listener != null) { 102 | listener.onError(deadLockError); 103 | } 104 | } 105 | } 106 | } 107 | 108 | private List> blockedThreadGroups(Map blockedThreads) { 109 | List> blockedThreadGroups = new ArrayList<>(); 110 | Set threadIds = new HashSet<>(); 111 | for (Integer thdId : blockedThreads.keySet()) { 112 | if (threadIds.contains(thdId)) { 113 | continue; 114 | } 115 | threadIds.add(thdId); 116 | Map blockedThreadGroup = findBlockedThreadGroup(thdId, blockedThreads, new HashMap()); 117 | blockedThreadGroups.add(thdId, blockedThreadGroup); 118 | } 119 | return blockedThreadGroups; 120 | } 121 | 122 | private Map findBlockedThreadGroup(Integer currentThreadId, Map deadLockThreads, Map threadMap) { 123 | DeadLockThreadWrapper wrapper = deadLockThreads.get(currentThreadId); 124 | if (wrapper == null) return new HashMap<>(); 125 | if (threadMap.containsKey(currentThreadId)) { 126 | return threadMap; 127 | } 128 | threadMap.put(currentThreadId, wrapper.thread); 129 | return findBlockedThreadGroup(wrapper.blockedThreadId, deadLockThreads, threadMap); 130 | } 131 | 132 | 133 | public native int nativeInit(int sdkInt); 134 | 135 | public native int getContendedThreadIdArt(long thdNativePeer); 136 | 137 | public native int getThreadIdFromNativePeer(long thdNativePeer); 138 | 139 | static class DeadLockThreadWrapper { 140 | int currentThreadId; 141 | int blockedThreadId; 142 | Thread thread; 143 | 144 | public DeadLockThreadWrapper(int currentThreadId, int blockedThreadId, Thread thread) { 145 | this.currentThreadId = currentThreadId; 146 | this.blockedThreadId = blockedThreadId; 147 | this.thread = thread; 148 | } 149 | } 150 | 151 | private void log(String message) { 152 | if (logEnabled) { 153 | Log.i(TAG, message); 154 | } 155 | } 156 | 157 | private void log(String message, Throwable throwable) { 158 | if (logEnabled) { 159 | Log.i(TAG, message, throwable); 160 | } 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /deadlock-library/src/main/java/me/bytebeats/deadlock/DeadLockStackTraces.java: -------------------------------------------------------------------------------- 1 | package me.bytebeats.deadlock; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Created by bytebeats on 2021/7/3 : 17:39 10 | * E-mail: happychinapc@gmail.com 11 | * Quote: Peasant. Educated. Worker 12 | */ 13 | 14 | class DeadLockStackTraces implements Serializable { 15 | private final String name; 16 | private final StackTraceElement[] stackTraceElements; 17 | 18 | DeadLockStackTraces(String name, StackTraceElement[] stackTraceElements) { 19 | this.name = name; 20 | this.stackTraceElements = stackTraceElements; 21 | } 22 | 23 | class Error extends Throwable { 24 | public Error(@Nullable Error cause) { 25 | super(name, cause); 26 | } 27 | 28 | @NonNull 29 | @Override 30 | public synchronized Throwable fillInStackTrace() { 31 | setStackTrace(stackTraceElements); 32 | return this; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /deadlock-library/src/main/java/me/bytebeats/deadlock/ReflectUtil.java: -------------------------------------------------------------------------------- 1 | package me.bytebeats.deadlock; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.InvocationTargetException; 5 | 6 | /** 7 | * Created by bytebeats on 2021/7/3 : 15:56 8 | * E-mail: happychinapc@gmail.com 9 | * Quote: Peasant. Educated. Worker 10 | */ 11 | 12 | public final class ReflectUtil { 13 | private ReflectUtil() { 14 | } 15 | 16 | public static T newInstance(Class klazz, Class argTypes, Object[] args) { 17 | try { 18 | return klazz.getConstructor(argTypes).newInstance(args); 19 | } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { 20 | throwBuildException(e); 21 | return null; 22 | } 23 | } 24 | 25 | public static Object invokeMethod(Object obj, String methodName) { 26 | try { 27 | return obj.getClass().getMethod(methodName, (Class) null).invoke(obj, (Object[]) null); 28 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 29 | throwBuildException(e); 30 | return null; 31 | } 32 | } 33 | 34 | public static Object invokeStaticMethod(Object obj, String methodName) { 35 | try { 36 | return ((Class) obj).getMethod(methodName, (Class) null).invoke(obj, (Object[]) null); 37 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 38 | throwBuildException(e); 39 | return null; 40 | } 41 | } 42 | 43 | public static Object invokeMethod(Object obj, String methodName, Class argTypes, Object arg) { 44 | try { 45 | return obj.getClass().getMethod(methodName, argTypes).invoke(obj, arg); 46 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 47 | throwBuildException(e); 48 | return null; 49 | } 50 | } 51 | 52 | public static Object invokeMethod(Object obj, String methodName, Class argTypes1, Object arg1, Class argTypes2, Object arg2) { 53 | try { 54 | return obj.getClass().getMethod(methodName, argTypes1, argTypes2).invoke(obj, arg1, arg2); 55 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 56 | throwBuildException(e); 57 | return null; 58 | } 59 | } 60 | 61 | public static Object invokeField(Object obj, String fieldName) { 62 | try { 63 | Field field = obj.getClass().getDeclaredField(fieldName); 64 | field.setAccessible(true); 65 | return field.get(obj); 66 | } catch (NoSuchFieldException | IllegalAccessException e) { 67 | throwBuildException(e); 68 | return null; 69 | } 70 | } 71 | 72 | private static void throwBuildException(Exception exception) { 73 | exception.printStackTrace(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 30 10:59:58 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lags-library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lags-library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | ext { 7 | LAGS_ARTIFACT_ID = 'lags' 8 | LAGS_VERSION = '1.0.2' 9 | LAGS_POM_DESCRIPTION = 'Android lags checking' 10 | } 11 | 12 | apply from: 'publish_lags.gradle' 13 | 14 | android { 15 | compileSdkVersion 30 16 | buildToolsVersion "30.0.3" 17 | 18 | defaultConfig { 19 | minSdkVersion 19 20 | targetSdkVersion 30 21 | versionCode 1 22 | versionName "1.0" 23 | 24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 25 | consumerProguardFiles "consumer-rules.pro" 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 45 | implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" 46 | } -------------------------------------------------------------------------------- /lags-library/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebeats/Anr-Monitor/77d211b8e389134161e8697bce1b6569715d3287/lags-library/consumer-rules.pro -------------------------------------------------------------------------------- /lags-library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lags-library/publish_lags.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | //only build and setting gradle can use `plugins` 5 | //plugins { 6 | // id('signing') 7 | // id('maven-publish') 8 | //} 9 | 10 | task androidSourcesJar(type: Jar) { 11 | from android.sourceSets.main.java.source 12 | classifier('sources') 13 | } 14 | 15 | apply from: '../local_setting_reader.gradle' 16 | 17 | project.publishing { 18 | publications { 19 | release(MavenPublication) { 20 | // The coordinates of the library, being set from variables that 21 | // we'll set up in a moment 22 | groupId GROUP_ID 23 | artifactId LAGS_ARTIFACT_ID 24 | version LAGS_VERSION 25 | 26 | // Two artifacts, the `aar` and the sources 27 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") 28 | artifact androidSourcesJar 29 | 30 | // Self-explanatory metadata for the most part 31 | pom { 32 | name = LAGS_ARTIFACT_ID 33 | description = LAGS_POM_DESCRIPTION 34 | // If your project has a dedicated site, use its URL here 35 | url = POM_URL 36 | licenses { 37 | license { 38 | //协议类型,一般默认Apache License2.0的话不用改: 39 | name = LICENSE_NAME 40 | url = LICENSE_URL 41 | } 42 | } 43 | developers { 44 | developer { 45 | id = DEVELOPER_ID 46 | name = DEVELOPER_NAME 47 | email = DEVELOPER_EMAIL 48 | } 49 | } 50 | // Version control info, if you're using GitHub, follow the format as seen here 51 | scm { 52 | //修改成你的Git地址: 53 | connection = SCM_CONNECTION 54 | developerConnection = SCM_DEVELOPER_CONNECTION 55 | //分支地址: 56 | url = SCM_URL 57 | } 58 | // A slightly hacky fix so that your POM will include any transitive dependencies 59 | // that your library builds upon 60 | withXml { 61 | def dependenciesNode = asNode().appendNode('dependencies') 62 | 63 | project.configurations.implementation.allDependencies.each { 64 | def dependencyNode = dependenciesNode.appendNode('dependency') 65 | dependencyNode.appendNode('groupId', it.group) 66 | dependencyNode.appendNode('artifactId', it.name) 67 | dependencyNode.appendNode('version', it.version) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | repositories { 74 | // The repository to publish to, Sonatype/MavenCentral 75 | maven { 76 | // This is an arbitrary name, you may also use "mavencentral" or 77 | // any other name that's descriptive for you 78 | name = "lags" 79 | 80 | // You only need this if you want to publish snapshots, otherwise just set the URL 81 | // to the release repo directly 82 | url = version.endsWith('SNAPSHOT') ? SNAPSHOTS_REPO_URL : RELEASES_REPO_URL 83 | 84 | // The username and password we've fetched earlier 85 | credentials { 86 | username ossrhUsername 87 | password ossrhPassword 88 | } 89 | } 90 | } 91 | } 92 | signing { 93 | sign publishing.publications 94 | } -------------------------------------------------------------------------------- /lags-library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/AnrProsecutor.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | /** 4 | * Created by bytebeats on 2021/7/1 : 20:39 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | interface AnrProsecutor { 10 | fun onAnr(suspecting: Boolean) 11 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/FrameJankDetector.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | import android.view.Choreographer 4 | import me.bytebeats.lag.LagMonitor.Companion.ANR_TIME_LIMIT 5 | import kotlin.math.pow 6 | 7 | /** 8 | * Created by bytebeats on 2021/7/1 : 17:31 9 | * E-mail: happychinapc@gmail.com 10 | * Quote: Peasant. Educated. Worker 11 | */ 12 | 13 | /** 14 | * Post {@see Choreographer.FrameCallback} to {@see Choreographer#getInstance()} to records the time every frame took 15 | */ 16 | class FrameJankDetector( 17 | private val frameJankListener: OnFrameJankListener?, 18 | private val thresholdTimeMillis: Long, 19 | private val refreshRate: Float, 20 | private val anrProsecutor: AnrProsecutor? 21 | ) : Choreographer.FrameCallback, LagMonitorLifecycle { 22 | private var mLastFrameTimeNanos: Long = 0 23 | private var mFrameIntervalInNanos: Long = 0 24 | private var skippedFrameLimit = 0L 25 | private var skippedFrameInvokeAnr = 0L 26 | 27 | private var isStarted = false 28 | 29 | init { 30 | // 1 s = 10^9 ns 31 | mFrameIntervalInNanos = (10.0.pow(9.0) / refreshRate).toLong() 32 | skippedFrameLimit = thresholdTimeMillis * 1001L * 1001L / mFrameIntervalInNanos 33 | skippedFrameInvokeAnr = ANR_TIME_NANOS_LIMIT / mFrameIntervalInNanos 34 | LagLog.logd("mFrameIntervalInNanos: $mFrameIntervalInNanos") 35 | LagLog.logd("skippedFrameLimit: $skippedFrameLimit") 36 | LagLog.logd("skippedFrameInvokeAnr: $skippedFrameInvokeAnr") 37 | } 38 | 39 | override fun onStart() { 40 | if (!isStarted) { 41 | postFrameCallback() 42 | isStarted = true 43 | } 44 | } 45 | 46 | override fun onStop() { 47 | isStarted = false 48 | } 49 | 50 | override fun doFrame(frameTimeNanos: Long) { 51 | if (mLastFrameTimeNanos > 0L) { 52 | val jitterNanos = frameTimeNanos - mLastFrameTimeNanos 53 | if (jitterNanos >= mFrameIntervalInNanos) { 54 | val skippedFrames = jitterNanos / mFrameIntervalInNanos 55 | LagLog.logd("skippedFrames: $skippedFrames") 56 | if (skippedFrames >= skippedFrameLimit) { 57 | LagLog.logd("skippedFrames: $skippedFrames is exceeding skipped frame limit: $skippedFrameLimit") 58 | if (skippedFrames >= skippedFrameInvokeAnr) { 59 | LagLog.logd("skippedFrames: $skippedFrames is too much and trying invoking ANR: $skippedFrameInvokeAnr") 60 | frameJankListener?.onJank(skippedFrames.toInt()) 61 | anrProsecutor?.onAnr(skippedFrames >= skippedFrameInvokeAnr) 62 | } 63 | } 64 | } 65 | } 66 | mLastFrameTimeNanos = frameTimeNanos 67 | if (isStarted) { 68 | postFrameCallback() 69 | } 70 | } 71 | 72 | private fun postFrameCallback() { 73 | Choreographer.getInstance().postFrameCallback(this) 74 | } 75 | 76 | companion object { 77 | private const val ANR_TIME_NANOS_LIMIT = ANR_TIME_LIMIT * 1000L * 1000000L 78 | } 79 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/LagLog.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * Created by bytebeats on 2021/7/1 : 14:27 7 | * E-mail: happychinapc@gmail.com 8 | * Quote: Peasant. Educated. Worker 9 | */ 10 | 11 | object LagLog { 12 | var logStackTrace = true 13 | var enabled = true 14 | private const val TAG = "lag-log" 15 | 16 | private enum class LEVEL { 17 | V, D, I, W, E; 18 | } 19 | 20 | fun logv(message: Any?) = logv(TAG, message) 21 | fun logd(message: Any?) = logd(TAG, message) 22 | fun logi(message: Any?) = logi(TAG, message) 23 | fun logw(message: Any?) = logw(TAG, message) 24 | fun loge(message: Any?) = loge(TAG, message) 25 | 26 | fun logv(tag: String, message: Any?) = log(LEVEL.V, tag, message.toString()) 27 | fun logd(tag: String, message: Any?) = log(LEVEL.D, tag, message.toString()) 28 | fun logi(tag: String, message: Any?) = log(LEVEL.I, tag, message.toString()) 29 | fun logw(tag: String, message: Any?) = log(LEVEL.W, tag, message.toString()) 30 | fun loge(tag: String, message: Any?) = log(LEVEL.E, tag, message.toString()) 31 | 32 | private fun log(level: LEVEL, tag: String, message: String) { 33 | if (!enabled) { 34 | return 35 | } 36 | val tagBuilder = StringBuilder() 37 | tagBuilder.append(tag) 38 | if (logStackTrace) { 39 | val stackTrace = Thread.currentThread().stackTrace[5] 40 | tagBuilder.append(" ${stackTrace.methodName}(${stackTrace.fileName}:${stackTrace.lineNumber})") 41 | } 42 | when (level) { 43 | LEVEL.V -> Log.v(tagBuilder.toString(), message) 44 | LEVEL.D -> Log.d(tagBuilder.toString(), message) 45 | LEVEL.I -> Log.i(tagBuilder.toString(), message) 46 | LEVEL.W -> Log.w(tagBuilder.toString(), message) 47 | LEVEL.E -> Log.e(tagBuilder.toString(), message) 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/LagMonitor.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | import android.app.ActivityManager 4 | import android.content.Context 5 | import android.os.Build 6 | import android.view.WindowManager 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleEventObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import kotlin.math.max 11 | import kotlin.math.min 12 | 13 | /** 14 | * Created by bytebeats on 2021/7/1 : 16:22 15 | * E-mail: happychinapc@gmail.com 16 | * Quote: Peasant. Educated. Worker 17 | */ 18 | 19 | class LagMonitor private constructor( 20 | private val context: Context, 21 | private var thresholdTimeMillis: Long, 22 | private val monitorMode: MonitorMode, 23 | private val logWithStackTrace: Boolean, 24 | private val frameJankListener: OnFrameJankListener?, 25 | private val uiThreadBlockListener: OnUIThreadBlockListener?, 26 | private val processNotRespondingListener: OnProcessNotRespondingListener? 27 | ) : AnrProsecutor, LifecycleEventObserver { 28 | 29 | private var lagMonitorLifecycle: LagMonitorLifecycle? = null 30 | 31 | init { 32 | thresholdTimeMillis = min(max(MIN_THREAD_TIME, thresholdTimeMillis), MAX_THREAD_TIME) 33 | LagLog.logStackTrace = logWithStackTrace 34 | lagMonitorLifecycle = create() 35 | } 36 | 37 | 38 | private fun create(): LagMonitorLifecycle { 39 | return if (monitorMode == MonitorMode.FRAME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 40 | FrameJankDetector(frameJankListener, thresholdTimeMillis, getRefreshRate(), this) 41 | } else { 42 | UILooperPrinter(uiThreadBlockListener, thresholdTimeMillis, this) 43 | } 44 | } 45 | 46 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 47 | when (event) { 48 | Lifecycle.Event.ON_START -> start() 49 | Lifecycle.Event.ON_STOP -> stop() 50 | } 51 | } 52 | 53 | fun start() { 54 | lagMonitorLifecycle?.onStart() 55 | LagLog.logd("LagMonitor started") 56 | } 57 | 58 | fun stop() { 59 | lagMonitorLifecycle?.onStop() 60 | LagLog.logd("LagMonitor stopped") 61 | } 62 | 63 | private fun getRefreshRate(): Float { 64 | return (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.refreshRate 65 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {//only if context is Activity 66 | // context.display?.refreshRate ?: 16F 67 | // } else { 68 | // } 69 | } 70 | 71 | override fun onAnr(suspecting: Boolean) { 72 | if (suspecting) { 73 | processNotRespondingListener?.onNotResponding(getNotRespondingProcessInfo()) 74 | } 75 | } 76 | 77 | private fun getNotRespondingProcessInfo(): String? { 78 | val mng = (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) 79 | val errorProcessInforms = mng.processesInErrorState 80 | if (errorProcessInforms != null) { 81 | for (processInfo in errorProcessInforms) { 82 | if (processInfo.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) { 83 | val info = StringBuilder(processInfo.processName) 84 | info.append("\n").append(processInfo.tag) 85 | .append("\n").append(processInfo.shortMsg) 86 | .append("\n").append(processInfo.longMsg) 87 | .append("\n").append(processInfo.stackTrace) 88 | LagLog.logd(info) 89 | return info.toString() 90 | } 91 | } 92 | } 93 | return null 94 | } 95 | 96 | enum class MonitorMode(val value: Int) { 97 | UI(0), FRAME(1); 98 | } 99 | 100 | class Builder(val mContext: Context) { 101 | private var mThresholdTimeMillis: Long = MAX_THREAD_TIME 102 | private var mMonitorMode: MonitorMode = DEFAULT_MONITOR_MODE 103 | private var mFrameJankListener: OnFrameJankListener? = null 104 | private var mProcessNotRespondingListener: OnProcessNotRespondingListener? = null 105 | private var mUiThreadBlockListener: OnUIThreadBlockListener? = null 106 | private var mLogWithStackTrace: Boolean = true 107 | 108 | fun setThresholdTimeMillis(thresholdTimeMillis: Long): Builder { 109 | mThresholdTimeMillis = thresholdTimeMillis 110 | return this 111 | } 112 | 113 | fun setMonitorMode(mode: MonitorMode): Builder { 114 | mMonitorMode = mode 115 | return this 116 | } 117 | 118 | fun setOnFrameJankListener(listener: OnFrameJankListener?): Builder { 119 | mFrameJankListener = listener 120 | return this 121 | } 122 | 123 | fun setOnProcessNotRespondingListener(listener: OnProcessNotRespondingListener?): Builder { 124 | mProcessNotRespondingListener = listener 125 | return this 126 | } 127 | 128 | fun setOnUIThreadRunListener(listener: OnUIThreadBlockListener?): Builder { 129 | mUiThreadBlockListener = listener 130 | return this 131 | } 132 | 133 | fun setLogWithStackTrace(logWithStackTrace: Boolean): Builder { 134 | mLogWithStackTrace = logWithStackTrace 135 | return this 136 | } 137 | 138 | fun setLagLogEnabled(enabled: Boolean): Builder { 139 | LagLog.enabled = enabled 140 | return this 141 | } 142 | 143 | fun build(): LagMonitor { 144 | return LagMonitor( 145 | mContext, 146 | mThresholdTimeMillis, 147 | mMonitorMode, 148 | mLogWithStackTrace, 149 | mFrameJankListener, 150 | mUiThreadBlockListener, 151 | mProcessNotRespondingListener 152 | ) 153 | } 154 | } 155 | 156 | companion object { 157 | private const val MIN_THREAD_TIME = 500L 158 | private const val MAX_THREAD_TIME = 500L 159 | private val DEFAULT_MONITOR_MODE = MonitorMode.UI 160 | const val ANR_TIME_LIMIT = 5L 161 | } 162 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/LagMonitorLifecycle.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | /** 4 | * Created by bytebeats on 2021/7/1 : 21:23 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | interface LagMonitorLifecycle { 10 | fun onStart() 11 | fun onStop() 12 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/OnFrameJankListener.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | /** 4 | * Created by bytebeats on 2021/7/1 : 16:31 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | /** 10 | * To detect whether frame janks happened 11 | */ 12 | interface OnFrameJankListener { 13 | /** 14 | * Called when frame janks happened 15 | */ 16 | fun onJank(janks: Int) 17 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/OnProcessNotRespondingListener.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | /** 4 | * Created by bytebeats on 2021/7/1 : 16:26 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | /** 10 | * To provide information of processes which are not responding when lags happen on UI thread. 11 | */ 12 | interface OnProcessNotRespondingListener { 13 | /** 14 | * provide information for process not responding 15 | */ 16 | fun onNotResponding(processInfo: String?) 17 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/OnUIThreadBlockListener.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | /** 4 | * Created by bytebeats on 2021/7/1 : 17:04 5 | * E-mail: happychinapc@gmail.com 6 | * Quote: Peasant. Educated. Worker 7 | */ 8 | 9 | interface OnUIThreadBlockListener { 10 | fun onBlock(lagTime: Long, uiRunTime: Long) 11 | } -------------------------------------------------------------------------------- /lags-library/src/main/java/me/bytebeats/lag/UILooperPrinter.kt: -------------------------------------------------------------------------------- 1 | package me.bytebeats.lag 2 | 3 | import android.os.Looper 4 | import android.os.SystemClock 5 | import android.util.Printer 6 | import me.bytebeats.lag.LagMonitor.Companion.ANR_TIME_LIMIT 7 | 8 | /** 9 | * Created by bytebeats on 2021/7/1 : 19:55 10 | * E-mail: happychinapc@gmail.com 11 | * Quote: Peasant. Educated. Worker 12 | */ 13 | 14 | /** 15 | * Changed Looper#mLogging to {@see UILooperPrinter}, so we can record the time every {@see Message} took 16 | */ 17 | class UILooperPrinter( 18 | private val uiThreadBlockListener: OnUIThreadBlockListener?, 19 | private val thresholdTimeMillis: Long, 20 | private val anrProsecutor: AnrProsecutor? 21 | ) : Printer, LagMonitorLifecycle { 22 | private var mLastMessageTimeMillis = 0L 23 | private var mLastThreadTimeMillis = 0L 24 | 25 | override fun onStart() { 26 | Looper.getMainLooper().setMessageLogging(this) 27 | } 28 | 29 | override fun onStop() { 30 | Looper.getMainLooper().setMessageLogging(null) 31 | } 32 | 33 | override fun println(x: String?) { 34 | if (x?.startsWith(LOG_PREFIX) == true) { 35 | mLastMessageTimeMillis = SystemClock.elapsedRealtime() 36 | mLastThreadTimeMillis = SystemClock.currentThreadTimeMillis() 37 | } else if (x?.startsWith(LOG_SUFFIX) == true) { 38 | if (mLastMessageTimeMillis > 0) { 39 | val msgElapsedTimeMillis = SystemClock.elapsedRealtime() - mLastMessageTimeMillis 40 | val thrdElapsedTimeMillis = 41 | SystemClock.currentThreadTimeMillis() - mLastThreadTimeMillis 42 | if (msgElapsedTimeMillis >= thresholdTimeMillis && msgElapsedTimeMillis > ANR_TIME_MILLIS_LIMIT) { 43 | uiThreadBlockListener?.onBlock( 44 | msgElapsedTimeMillis, 45 | thrdElapsedTimeMillis 46 | ) 47 | anrProsecutor?.onAnr(msgElapsedTimeMillis > ANR_TIME_MILLIS_LIMIT) 48 | } 49 | } 50 | } 51 | } 52 | 53 | companion object { 54 | private const val ANR_TIME_MILLIS_LIMIT = ANR_TIME_LIMIT * 1000L 55 | private const val LOG_PREFIX = ">>>>> Dispatching to" 56 | private const val LOG_SUFFIX = "<<<<< Finished to" 57 | } 58 | } -------------------------------------------------------------------------------- /local_setting_reader.gradle: -------------------------------------------------------------------------------- 1 | ext["signing.keyId"] = '' 2 | ext["signing.password"] = '' 3 | ext["signing.secretKeyRingFile"] = '' 4 | ext["ossrhUsername"] = '' 5 | ext["ossrhPassword"] = '' 6 | 7 | File secretPropsFile = project.rootProject.file('local.properties') 8 | if (secretPropsFile.exists()) { 9 | println "Found secret props file, loading props" 10 | Properties p = new Properties() 11 | p.load(new FileInputStream(secretPropsFile)) 12 | p.each { name, value -> 13 | ext[name] = value 14 | } 15 | } else { 16 | println "No props file, loading env vars" 17 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "Anr-Monitor" 3 | include ':anr-library' 4 | include ':lags-library' 5 | include ':deadlock-library' 6 | --------------------------------------------------------------------------------