booster分析-線程優化&性能檢測

本文繼續分析booster的實現原理。java

多線程優化 booster-transform-thread

對於這個組件官方是這樣描述的: 對於開發者來講,線程管理一直是個頭疼的問題,特別是第三方 SDK 中的線程,過多的線程可能會致使內存不足,然而幸運的是,這些問題都能經過 Booster 來解決。node

那booster是如何解決的呢? 其實實現思路相似上一篇文章booster分析-修復系統bug。即經過gradle transform在編譯時作字節碼插樁,來實現代碼的動態替換(整個booster框架大部分功能的實現都是這個原理)。android

那麼來看一下booster到底如何優化了多線程管理吧。git

給線程設置名稱,便於代碼追蹤和bug修復

通常一個比較成熟的應用在運行時後臺都會有不少線程在跑,爲了便於線程的管理。 booster在編譯時強制給應用程序運行時的線程(不管是本身開啓的仍是三方庫開啓的)都起了一個名字。這樣不管是bug的定位,仍是線程的調試都方便了很多。github

booster 在編譯時經過修改字節碼文件動態給HandlerThreadTimerAsyncTaskThreadThreadPoolExecutor等開啓的線程都起了名字。bash

好比,全部的Threadstart()以前:多線程

ThreadTransformer.javaapp

private fun MethodInsnNode.transformInvokeVirtual(context: TransformContext, klass: ClassNode, method: MethodNode) {
    if (context.klassPool.get("java/lang/Thread").isAssignableFrom(this.owner)) {
        when ("${this.name}${this.desc}") {
            "start()V" -> {
                method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
                method.instructions.insertBefore(this, MethodInsnNode(Opcodes.INVOKESTATIC, SHADOW_THREAD, "setThreadName", "(Ljava/lang/Thread;Ljava/lang/String;)Ljava/lang/Thread;", false))
        
            }
        }
    }
}
複製代碼

即在Thread.start()以前調用了ShadowThread.setThreadName():框架

public class ShadowThread {
     public static Thread setThreadName(final Thread t, final String prefix) {
        t.setName(makeThreadName(t.getName(), prefix));
        return t;
    }

    public static String makeThreadName(final String name, final String prefix) {
        return name == null ? prefix : (name.startsWith(MARK) ? name : (prefix + "#" + name));
    }

}
複製代碼

舉個例子:ide

class ThreadTest {

    private val TAG = "Booster-Thread"

    fun test(){
       Thread({
            Log.d(TAG, "thread name : ${Thread.currentThread().name}")
        }).start()

    }
}
複製代碼

上面代碼通過booster優化後log輸入的線程名爲:

D/Booster-Thread: thread name : ​com.susion.boostertest.thread.ThreadTest
複製代碼

即線程的名字改成 : 包+類名, 而沒有優化前名稱是:

D/Booster-Thread: thread name : Thread-2
複製代碼

通過booster優化後確實更容易識別線程了。 咱們寫多線程代碼時仍是給線程起個規範點的名字比較好

設置線程池的 allowCoreThreadTimeOut(true)

先來回顧一下線程池的構造函數:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)
複製代碼

keepAliveTime參數是用來設置線程的超時時長,當線程的閒置時長超過這個時間時線程就會被回收,可是這個策略並不會應用在覈心線程上。

但當對線程池設置allowCoreThreadTimeOut(true)時,核心線程也會被回收退出。

booster經過對字節碼的動態替換,對應用中全部的線程池都設置了allowCoreThreadTimeOut(true), 這樣能夠保證線程資源及時回收, 好比:

ThreadTransformer.java

private fun MethodInsnNode.transformInvokeStatic(context: TransformContext, klass: ClassNode, method: MethodNode) {
    when (this.owner) {
        "java/util/concurrent/Executors" -> {
            when (this.name) {
                "newCachedThreadPool",
                "newFixedThreadPool",
                "newSingleThreadExecutor",
                "newSingleThreadScheduledExecutor",
                "newScheduledThreadPool" -> {
                    val r = this.desc.lastIndexOf(')')
                    val name = this.name.replace("new", "newOptimized")
                    val desc = "${this.desc.substring(0, r)}Ljava/lang/String;${this.desc.substring(r)}"

                    this.owner = 「com/didiglobal/booster/instrument/ShadowExecutors」
                    this.name = name
                    this.desc = desc
                    method.instructions.insertBefore(this, LdcInsnNode(makeThreadName(klass.className)))
                }
            }
        }
    }
}
複製代碼

即將Executors.newCachedThreadPool()等方法都替換爲ShadowExecutors.newOptimizedXXX方法:

ShadowExecutors.java

public static ExecutorService newOptimizedCachedThreadPool() {
    final ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory());
    executor.allowCoreThreadTimeOut(true);
    return executor;
}
複製代碼

性能瓶頸檢測 booster-transform-lint

這個組件經過編譯時的lint檢查來找出一些在應用中調用可能阻塞UI線程的API,如:I/O API 等。主要有兩個特色:

  1. 支持自定義檢查API列表。
  2. 對於查找到的有問題的API能夠產生一張調用圖便於查看與定位。

實現原理

指定檢查方法

首先指定一些受檢查的方法:

LintTransformer.java

class LintTransformer : ClassTransformer {

    override fun onPreTransform(context: TransformContext) {
        ...
        val parser = SAXParserFactory.newInstance().newSAXParser()
        context.artifacts.get(ArtifactManager.MERGED_MANIFESTS).forEach { manifest ->
            val handler = ComponentHandler()
            parser.parse(manifest, handler)
            // Attach component entry points to graph ROOT
            mapOf(
                Pair(handler.applications, APPLICATION_ENTRY_POINTS),
                Pair(handler.activities, ACTIVITY_ENTRY_POINTS),
                Pair(handler.services, SERVICE_ENTRY_POINTS),
                Pair(handler.receivers, RECEIVER_ENTRY_POINTS),
                Pair(handler.providers, PROVIDER_ENTRY_POINTS)
            ).forEach { components, entryPoints ->
                components.forEach { component ->
                    entryPoints.map {
                        CallGraph.Node(component.replace('.', '/'), it.name, it.desc)
                    }.forEach {
                        globalBuilder.addEdge(CallGraph.ROOT, it) // 全部的分析點
                    }
                }
            }
        }
    }
}
複製代碼

它檢查的方法有:

constant.kt

/**
 * Main/UI thread entry point of Application
 */
internal val APPLICATION_ENTRY_POINTS = arrayOf(
        "onConfigurationChanged(Landroid/content/res/Configuration;)V",
        "onCreate()V",
        "onLowMemory()V",
        "onTerminate()V",
        "onTrimMemory(I)V"
).map(EntryPoint.Companion::valueOf).toSet()

/**
 * Main/UI thread entry point of Activity
 */
internal val ACTIVITY_ENTRY_POINTS = arrayOf(
        // <editor-fold desc="public methods of Activity">
        "onActionModeFinished(Landroid/view/ActionMode;)V",
        "onActionModeStarted(Landroid/view/ActionMode;)V",
        "onActivityReenter(ILandroid/content/Intent;)V",
        "onAttachFragment(Landroid/app/Fragment;)V",
        "onAttachedToWindow()V",
....
複製代碼

Aplication/Activity/Service/BroadcastReceiver/ContentProvider等相關API。

根據定義好的lint API列表來檢查有沒有調用敏感的API

override fun onPostTransform(context: TransformContext) {
        val graph = globalBuilder.build()
        ...
        // Analyse global call graph and separate each chain to individual graph
        graph[CallGraph.ROOT].forEach { node ->
            graph.analyse(context, node, listOf(CallGraph.ROOT, node), lints) { chain ->
                val builder = graphBuilders.getOrPut(node.type) {
                    CallGraph.Builder().setTitle(node.type.replace('/', '.'))
                }
                chain.toEdges().forEach { edge ->
                    builder.addEdges(edge)
                }
            }
        }
  }
複製代碼

analyse()用來檢查當前API是否命中了Lint檢查列表。若是命中就會加入到dot文件繪圖節點中。

booster默認定義的敏感API有:

/**
 * Sensitive APIs
 */
internal val LINT_APIS = setOf(
        ...
        "android/graphics/BitmapFactory.decodeFile(Ljava/lang/String;)Landroid/graphics/Bitmap;",
        "android/graphics/BitmapFactory.decodeFileDescriptor(Ljava/io/FileDescriptor;)Landroid/graphics/Bitmap;",
        "android/graphics/BitmapFactory.decodeFileDescriptor(Ljava/io/FileDescriptor;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory\$Options;)Landroid/graphics/Bitmap;",
        "android/graphics/BitmapFactory.decodeResource(Landroid/content/res/Resources;I)Landroid/graphics/Bitmap;",
        "android/graphics/BitmapFactory.decodeResource(Landroid/content/res/Resources;ILandroid/graphics/BitmapFactory\$Options;)Landroid/graphics/Bitmap;",
        "android/graphics/BitmapFactory.decodeResourceStream(Landroid/content/res/Resources;Landroid/util/TypedValue;Ljava/io/InputStream;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory\$Options;)Landroid/graphics/Bitmap;",
        "android/graphics/BitmapFactory.decodeStream(Ljava/io/InputStream;)Landroid/graphics/Bitmap;",
        "android/graphics/BitmapFactory.decodeStream(Ljava/io/InputStream;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory\$Options;)Landroid/graphics/Bitmap;"
        // </editor-fold>
).map(Node.Companion::valueOf).toSet()
複製代碼

即主要是一些可能致使主線程卡頓的API。

檢測Demo

好比有下面這個類:

class LabThreadTestActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_lab_thread_test)

        val test1 = getSharedPreferences("default", Context.MODE_PRIVATE).edit().putBoolean("test",true).commit()
//        val test2 = getSharedPreferences("default", Context.MODE_PRIVATE).edit().putBoolean("test",true).apply()
    }
}
複製代碼

通過booster-transform-lint檢查後會生成com.susion.boostertest.thread.LabThreadTestActivity.dot文件。這個文件轉爲png圖片以下:

更多分享見AdvancedAndroid

相關文章
相關標籤/搜索