代码优化

This commit is contained in:
HapeLee
2026-05-13 21:22:44 +08:00
parent 4beeb2d4f6
commit 876f1370ec
21 changed files with 126200 additions and 331 deletions
+4 -1
View File
@@ -64,7 +64,10 @@
"PowerShell(npm *)",
"PowerShell(gcc *)",
"Bash(gh repo *)",
"Bash(Test-Path *)"
"Bash(Test-Path *)",
"Bash(git stash *)",
"Bash(Select-String -Pattern \"FAILED|error:\")",
"Bash(Select-Object -First 5)"
]
}
}
@@ -1,4 +1,4 @@
name: Test Build
name: Auto Release
on:
push:
@@ -118,7 +118,7 @@ jobs:
echo "build_number=$NEW_BUILD_NUM" >> $GITHUB_OUTPUT
echo "commit_number=$COMMIT_COUNT" >> $GITHUB_OUTPUT
echo "last_tag=$LAST_TAG_FOR_LOG" >> $GITHUB_OUTPUT
echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT # 新的输出标志
echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT # 新s的输出标志
build:
needs: prepare
-140
View File
@@ -1,140 +0,0 @@
name: Release Build
on:
# push:
# branches:
# - master
# paths:
# - 'CHANGELOG.md'
workflow_dispatch:
jobs:
prepare:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
outputs:
version: ${{ steps.set-ver.outputs.version }}
play: ${{ steps.check.outputs.play }}
sign: ${{ steps.check.outputs.sign }}
steps:
- id: set-ver
run: |
echo "version=$(date -d "8 hour" -u +3.%y.%m%d%H)" >> $GITHUB_OUTPUT
- id: check
run: |
if [ ! -z "${{ secrets.RELEASE_KEY_STORE }}" ]; then
echo "sign=yes" >> $GITHUB_OUTPUT
fi
if [ ! -z "${{ secrets.SERVICE_ACCOUNT_JSON }}" ]; then
echo "play=yes" >> $GITHUB_OUTPUT
fi
build:
needs: prepare
if: ${{ needs.prepare.outputs.sign }}
strategy:
matrix:
product: [ app, google ]
fail-fast: false
runs-on: ubuntu-latest
env:
product: ${{ matrix.product }}
VERSION: ${{ needs.prepare.outputs.version }}
play: ${{ needs.prepare.outputs.play }}
steps:
- uses: actions/checkout@v5
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Release Apk Sign
run: |
# not use this output
# echo "KeyStore=yes" >> $GITHUB_OUTPUT
echo -e "\n" >> gradle.properties
echo RELEASE_KEY_ALIAS='${{ secrets.RELEASE_KEY_ALIAS }}' >> gradle.properties
echo RELEASE_KEY_PASSWORD='${{ secrets.RELEASE_KEY_PASSWORD }}' >> gradle.properties
echo RELEASE_STORE_PASSWORD='${{ secrets.RELEASE_STORE_PASSWORD }}' >> gradle.properties
echo RELEASE_STORE_FILE='./key.jks' >> gradle.properties
echo ${{ secrets.RELEASE_KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks
- name: Unify Version Name
run: |
echo "统一版本号"
sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build With Gradle
run: |
echo "开始进行${{ env.product }}构建"
chmod +x gradlew
./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all
- name: Organize the Files
run: |
mkdir -p ${{ github.workspace }}/apk/
cp -rf ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk ${{ github.workspace }}/apk/
- name: Upload App To Artifact
uses: actions/upload-artifact@v4
with:
name: legado_${{ env.product }}
path: ${{ github.workspace }}/apk/*.apk
- name: Release
if: ${{ env.product == 'app' }}
uses: softprops/action-gh-release@v2
with:
name: legado_app_${{ env.VERSION }}
tag_name: ${{ env.VERSION }}
body_path: ${{ github.workspace }}/CHANGELOG.md
draft: false
prerelease: false
files: ${{ github.workspace }}/apk/legado_app_*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare For GooglePlay
if: env.product == 'google' && env.play == 'yes'
run: |
mkdir -p ReleaseNotes
ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-en-US
ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-zh-CN
- name: Release To GooglePlay
if: env.product == 'google' && env.play == 'yes'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: io.legado.play
releaseFiles: ${{ github.workspace }}/apk/legado_google_*.apk
track: production
whatsNewDirectory: ${{ github.workspace }}/ReleaseNotes
- name: Push Assets To "release" Branch
if: ${{ github.actor == 'gedoor' }}
run: |
cd $GITHUB_WORKSPACE/apk/
git init
git checkout -b release
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add origin "https://${{ github.actor }}:${{ secrets.ACTIONS_TOKEN }}@github.com/${{ github.actor }}/release"
git add *.apk
git commit -m "${{ env.VERSION }}"
git push -f -u origin release
- name: Purge Jsdelivr Cache
if: ${{ github.actor == 'gedoor' }}
run: |
result=$(curl -s https://purge.jsdelivr.net/gh/${{ github.actor }}/release@release/)
if echo $result |grep -q 'success.*true'; then
echo "jsdelivr缓存更新成功"
else
echo $result
fi
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -38,6 +38,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import splitties.init.appCtx
import splitties.systemservices.notificationManager
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import kotlin.math.min
@@ -69,7 +70,7 @@ class CacheBookService : BaseService() {
private val downloadJobLock = Any()
private var downloadJob: Job? = null
private var notificationContent = appCtx.getString(R.string.service_starting)
private var mutex = Mutex()
private val bookLocks = ConcurrentHashMap<String, Mutex>()
private var lastDiagnosticsLogTime = 0L
private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
@@ -297,7 +298,8 @@ class CacheBookService : BaseService() {
if (chapterCount == 0) {
cacheBook.setLoading()
mutex.withLock {
val bookMutex = bookLocks.getOrPut(request.bookUrl) { Mutex() }
bookMutex.withLock {
val name = book.name
if (!admission.isCurrent()) {
CacheBook.removeModelFromService(request.bookUrl, cacheBook)
@@ -371,8 +373,8 @@ class CacheBookService : BaseService() {
}
private fun admittedBookUrls(): Set<String> {
return CacheBook.cacheBookMap.keys.toHashSet().apply {
synchronized(admissionLock) {
return synchronized(admissionLock) {
CacheBook.cacheBookMap.keys.toHashSet().apply {
addAll(admittingBookUrls)
}
}
@@ -431,21 +433,24 @@ class CacheBookService : BaseService() {
}
private fun finishAdmissionJob(bookUrl: String) {
var restart = false
val waiters = synchronized(admissionLock) {
val buffer = admissionBuffers[bookUrl]
if (buffer != null && buffer.isNotEmpty()) {
restart = true
emptyList()
} else {
admissionBuffers.remove(bookUrl)
admittingBookUrls.remove(bookUrl)
admissionIdleWaiters.remove(bookUrl).orEmpty()
// New requests arrived while job was finishing — restart immediately.
startAdmissionJob(bookUrl)
return
}
}
if (restart) {
startAdmissionJob(bookUrl)
return
admissionBuffers.remove(bookUrl)
admittingBookUrls.remove(bookUrl)
// Re-check: a concurrent submitDownloadRequest may have added a request
// between the buffer empty-check and the admittingBookUrls removal.
val recheck = admissionBuffers[bookUrl]
if (recheck != null && recheck.isNotEmpty()) {
admittingBookUrls.add(bookUrl)
startAdmissionJob(bookUrl)
return
}
admissionIdleWaiters.remove(bookUrl).orEmpty()
}
waiters.forEach { it.complete(Unit) }
}
@@ -480,26 +485,23 @@ class CacheBookService : BaseService() {
private suspend fun runDownloadLoop() {
try {
var lastActiveTime = System.currentTimeMillis()
var idleSince = -1L
while (currentCoroutineContext().isActive) {
drainPendingDownloadRequests()
if (CacheBook.isGloballyPaused) {
break
}
val isActiveNow = CacheBook.isRun
|| hasPendingDownloadRequests()
|| hasAdmittingRequests()
if (isActiveNow) {
lastActiveTime = System.currentTimeMillis()
} else {
break
}
if (!CacheBook.isRun) {
if (System.currentTimeMillis() - lastActiveTime > MAX_IDLE_SPIN_MS) break
delay(200)
if (CacheBook.isRun) {
idleSince = -1L
CacheBook.startProcessJob(cachePool)
continue
}
CacheBook.startProcessJob(cachePool)
// !isRun — only keep looping while there is pending/admitting work.
if (!hasPendingDownloadRequests() && !hasAdmittingRequests()) break
val now = System.currentTimeMillis()
if (idleSince < 0) idleSince = now
if (now - idleSince > MAX_IDLE_SPIN_MS) break
delay(200)
}
} finally {
val finishedJob = currentCoroutineContext()[Job]
@@ -136,7 +136,9 @@ fun BookInfoScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3ExpressiveApi::class
)
@Composable
private fun BookInfoScreenContent(
state: BookInfoUiState,
@@ -2,6 +2,7 @@ package io.legado.app.ui.config.themeManage
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -29,6 +30,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.help.config.ThemeExportData
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -74,7 +76,8 @@ fun EditThemeSheet(
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.navigationBarsPadding()
.padding(bottom = 32.dp)
.padding(bottom = 32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Name
AppTextField(
@@ -84,7 +87,6 @@ fun EditThemeSheet(
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(8.dp))
@@ -5,18 +5,21 @@ import android.os.Looper
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Brush
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
@@ -119,14 +122,17 @@ fun ThemeManageScreen(
)
}
) { paddingValues ->
LazyColumn(
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp
)
),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
item(span = { GridItemSpan(maxLineSpan) }) {
SplicedColumnGroup {
ClickableSettingItem(
title = "保存当前设置",
@@ -155,12 +161,12 @@ fun ThemeManageScreen(
}
if (savedThemes.isNotEmpty()) {
item {
item(span = { GridItemSpan(maxLineSpan) }) {
AppText(
text = "已保存的主题",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp)
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
)
}
@@ -288,79 +294,112 @@ private fun SavedThemeItem(
) {
GlassCard(
onClick = onApply,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
cornerRadius = 20.dp
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 8.dp, top = 12.dp, bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
Column(
modifier = Modifier.fillMaxWidth()
) {
val previewColor = if (theme.data.themeColor != 0) {
Color(theme.data.themeColor)
} else if (theme.data.cPrimary != 0) {
Color(theme.data.cPrimary)
} else {
MaterialTheme.colorScheme.primary
val lightPrimary = if (theme.data.themeColor != 0) Color(theme.data.themeColor)
else if (theme.data.cPrimary != 0) Color(theme.data.cPrimary)
else MaterialTheme.colorScheme.primary
val darkPrimary = if (theme.data.cNPrimary != 0) Color(theme.data.cNPrimary)
else lightPrimary
val lightBg = if (theme.data.themeBackgroundColor != 0) Color(theme.data.themeBackgroundColor)
else Color(0xFFF7F2FA)
val darkBg = if (theme.data.isPureBlack) Color.Black else Color(0xFF1C1B1F)
// 预览区域
Column(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1.2f)
) {
// 日间行
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.background(
Brush.horizontalGradient(
0.5f to lightBg,
0.8f to lightPrimary
)
)
) {
AppText(
text = "日间",
style = MaterialTheme.typography.labelMediumEmphasized,
color = if (theme.data.primaryTextColor != 0) Color(theme.data.primaryTextColor).copy(alpha = 0.6f)
else Color.Black.copy(alpha = 0.5f),
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp)
)
}
// 夜间行
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.background(
Brush.horizontalGradient(
0.5f to darkBg,
0.8f to darkPrimary
)
)
) {
AppText(
text = "夜间",
style = MaterialTheme.typography.labelMediumEmphasized,
color = Color.White.copy(alpha = 0.5f),
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp)
)
}
}
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(12.dp))
.background(previewColor)
)
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f)
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
) {
AppText(
text = theme.name,
style = MaterialTheme.typography.bodyLarge
style = MaterialTheme.typography.titleSmall,
maxLines = 1
)
val features = mutableListOf<String>()
features.add(
when (theme.data.appTheme) {
"0" -> "动态取色"
"12" -> "自定义颜色"
else -> "预设主题"
}
)
if (theme.data.enableBlur) features.add("模糊效果")
if (theme.data.useFloatingBottomBar) features.add("浮动底栏")
AppText(
text = features.joinToString(" · "),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
IconButton(onClick = onEdit) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "编辑",
modifier = Modifier.size(20.dp)
)
}
IconButton(onClick = onExport) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = "导出",
modifier = Modifier.size(20.dp)
)
}
IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除",
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.error
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onEdit, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "编辑",
modifier = Modifier.size(18.dp)
)
}
IconButton(onClick = onExport, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = "导出",
modifier = Modifier.size(18.dp)
)
}
IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除",
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
}
@@ -55,6 +55,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -240,6 +241,7 @@ fun MainScreen(
val haptic = LocalHapticFeedback.current
WideNavigationRailItem(
modifier = Modifier.testTag("nav_${destination.route}"),
railExpanded = navState.targetValue == WideNavigationRailValue.Expanded,
selected = selected,
onClick = {
@@ -308,6 +310,7 @@ fun MainScreen(
MainDestination.My -> ThemeConfig.navIconMy
}
AppNavigationBarItem(
modifier = Modifier.testTag("nav_${destination.route}"),
selected = selected,
onClick = {
coroutineScope.launch { pagerState.animateScrollToPage(index) }
@@ -5,6 +5,7 @@ import android.content.res.Configuration
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
@@ -81,6 +82,7 @@ import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -145,6 +147,8 @@ fun BookshelfScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
ReportDrawnWhen { uiState.groups.isNotEmpty() }
val activeOverlay = uiState.activeOverlay
val showGroupMenu = activeOverlay == BookshelfOverlay.GroupMenu
val isEditMode = uiState.isEditMode
@@ -1215,6 +1219,7 @@ fun BookshelfPage(
state = gridState,
modifier = Modifier
.fillMaxSize()
.testTag("bookshelf_list")
.then(
with(sharedTransitionScope) {
if (this != null) Modifier.skipToLookaheadSize() else Modifier
+8 -16
View File
@@ -1,5 +1,3 @@
import com.android.build.api.dsl.ManagedVirtualDevice
plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.baselineprofile)
@@ -20,7 +18,7 @@ android {
defaultConfig {
minSdk = 28
targetSdk = 36
targetSdk = 37
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -31,24 +29,13 @@ android {
productFlavors {
create("app") { dimension = "mode" }
}
// This code creates the gradle managed device used to generate baseline profiles.
// To use GMD please invoke generation through the command line:
// ./gradlew :app:generateBaselineProfile
testOptions.managedDevices.allDevices {
create<ManagedVirtualDevice>("pixel6Api34") {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "google"
}
}
}
// This is the configuration block for the Baseline Profile plugin.
// You can specify to run the generators on a managed devices or connected devices.
baselineProfile {
managedDevices += "pixel6Api34"
useConnectedDevices = false
// managedDevices += "pixel6Api34"
useConnectedDevices = true
}
dependencies {
@@ -59,6 +46,11 @@ dependencies {
}
androidComponents {
beforeVariants { v ->
if (v.buildType == "noR8" || v.buildType == "debug") {
v.enable = false
}
}
onVariants { v ->
val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
v.instrumentationRunnerArguments.put(
@@ -4,32 +4,15 @@ import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* This test class generates a basic startup baseline profile for the target package.
*
* We recommend you start with this but add important user flows to the profile to improve their performance.
* Refer to the [baseline profile documentation](https://d.android.com/topic/performance/baselineprofiles)
* for more information.
*
* You can run the generator with the "Generate Baseline Profile" run configuration in Android Studio or
* the equivalent `generateBaselineProfile` gradle task:
* ```
* ./gradlew :app:generateReleaseBaselineProfile
* ```
* The run configuration runs the Gradle task and applies filtering to run only the generators.
*
* Check [documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args)
* for more information about available instrumentation arguments.
*
* After you run the generator, you can verify the improvements running the [StartupBenchmarks] benchmark.
*
* When using this class to generate a baseline profile, only API 33+ or rooted API 28+ are supported.
*
* The minimum required version of androidx.benchmark to generate a baseline profile is 1.2.0.
**/
@RunWith(AndroidJUnit4::class)
@LargeTest
@@ -40,29 +23,43 @@ class BaselineProfileGenerator {
@Test
fun generate() {
// The application id for the running build variant is read from the instrumentation arguments.
rule.collect(
packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
?: throw Exception("targetAppId not passed as instrumentation runner arg"),
val packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
?: "io.legado.app"
// See: https://d.android.com/topic/performance/baselineprofiles/dex-layout-optimizations
rule.collect(
packageName = packageName,
includeInStartupProfile = true
) {
// This block defines the app's critical user journey. Here we are interested in
// optimizing for app startup. But you can also navigate and scroll through your most important UI.
// Start default activity for your app
// 1. 启动应用
pressHome()
startActivityAndWait()
// TODO Write more interactions to optimize advanced journeys of your app.
// For example:
// 1. Wait until the content is asynchronously loaded
// 2. Scroll the feed content
// 3. Navigate to detail screen
// 2. 等待书架加载 (我们在 BookshelfScreen 中添加了 ReportDrawnWhen)
// 等待书架列表 testTag 出现
device.wait(Until.hasObject(By.res(packageName, "bookshelf_list")), 10000)
// 3. 模拟滑动书架 (优化列表渲染性能)
val bookshelf = device.findObject(By.res(packageName, "bookshelf_list"))
bookshelf?.apply {
setGestureMargin(device.displayWidth / 10)
fling(Direction.DOWN)
device.waitForIdle()
fling(Direction.UP)
device.waitForIdle()
}
// 4. 遍历主要 Tab (优化 Compose 导航和各页面初始化)
val tabs = listOf("nav_explore", "nav_rss", "nav_my", "nav_bookshelf")
for (tabTag in tabs) {
val tab = device.findObject(By.res(packageName, tabTag))
if (tab != null) {
tab.click()
device.waitForIdle()
// 稍微等待页面加载
Thread.sleep(500)
}
}
// Check UiAutomator documentation for more information how to interact with the app.
// https://d.android.com/training/testing/other-components/ui-automator
}
}
}
@@ -4,10 +4,14 @@ import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,27 +53,32 @@ class StartupBenchmarks {
private fun benchmark(compilationMode: CompilationMode) {
// The application id for the running build variant is read from the instrumentation arguments.
val packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
?: "io.legado.app"
rule.measureRepeated(
packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
?: throw Exception("targetAppId not passed as instrumentation runner arg"),
metrics = listOf(StartupTimingMetric()),
packageName = packageName,
metrics = listOf(StartupTimingMetric(), FrameTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = 10,
iterations = 3,
setupBlock = {
pressHome()
},
measureBlock = {
startActivityAndWait()
// TODO Add interactions to wait for when your app is fully drawn.
// The app is fully drawn when Activity.reportFullyDrawn is called.
// For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
// from the AndroidX Activity library.
// 等待书架列表加载完成
// 我们在 BookshelfScreen 中添加了 testTag("bookshelf_list")
device.wait(Until.hasObject(By.res(packageName, "bookshelf_list")), 10000)
// Check the UiAutomator documentation for more information on how to
// interact with the app.
// https://d.android.com/training/testing/other-components/ui-automator
// 模拟交互:在书架上进行滑动,以确保 Profile 捕获到列表渲染和图片加载的路径
val bookshelfList = device.findObject(By.res(packageName, "bookshelf_list"))
bookshelfList?.apply {
setGestureMargin(device.displayWidth / 10)
fling(Direction.DOWN)
device.waitForIdle()
}
}
)
}
+2
View File
@@ -15,6 +15,8 @@ plugins {
alias(libs.plugins.google.services) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.download) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.baselineprofile) apply false
}
tasks.register<Delete>("clean") {
+2 -2
View File
@@ -45,5 +45,5 @@ android.uniquePackageNames=false
android.dependency.useConstraints=true
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
android.r8.strictFullModeForKeepRules=false
org.gradle.configuration-cache=true
android.newDsl=false
+26 -18
View File
@@ -1,27 +1,27 @@
[versions]
accompanistWebview = "0.36.0"
adaptive = "1.3.0-alpha10"
adaptiveLayout = "1.3.0-alpha10"
adaptiveNavigation = "1.3.0-alpha10"
animation = "1.10.6"
adaptive = "1.3.0-beta01"
adaptiveLayout = "1.3.0-beta01"
adaptiveNavigation = "1.3.0-beta01"
animation = "1.11.1"
biometric = "1.4.0-alpha06"
coilCompose = "2.7.0"
composeBom = "2026.03.01"
composeBom = "2026.05.00"
constraintlayoutCompose = "1.1.1"
coreSplashscreen = "1.2.0"
datastorePreferences = "1.2.1"
foundation = "1.10.6"
foundation = "1.11.1"
kotlin = "2.3.10"
kotlinxSerialization = "1.10.0"
kotlinxSerialization = "1.11.0"
kotlinxCoroutinesAndroid = "1.10.2"
kotlinxSerializationJson = "1.10.0"
kotlinxSerializationJson = "1.11.0"
kotlinxCollectionsImmutable = "0.4.0"
ksp = "2.3.6"
agp = "9.1.1"
agp = "9.2.1"
appcompat = "1.7.1"
colorpicker = "1.1.0"
colorpicker-compose = "1.1.3"
colorpicker-compose = "1.1.4"
commonsText = "1.15.0"
constraintlayout = "2.2.1"
core = "1.18.0"
@@ -34,7 +34,7 @@ hutool = "5.8.22"
libarchive = "1.1.6"
lifecycle = "2.10.0"
glide = "5.0.5"
glide = "5.0.7"
gson = "2.13.2"
jsonPath = "2.10.0"
# issue #3811,不要更新版本,新版引入了一个破坏性变更(详见https://github.com/jhy/jsoup/pull/2017
@@ -44,17 +44,17 @@ jsoupxpath = "2.5.3"
coroutines = "1.10.2"
lifecycleViewmodelCompose = "2.10.0"
liveeventbus = "1.8.14"
navigation3 = "1.0.1"
navigation3 = "1.1.1"
markdownCompose = "0.5.0"
markwon = "4.6.2"
material = "1.14.0-beta01"
material3 = "1.5.0-alpha17"
material = "1.14.0-rc01"
material3 = "1.5.0-alpha19"
material3IconsExtended = "1.7.8"
materialKolor = "4.1.1"
media = "1.7.1"
media3 = "1.8.0"
nanoHttpd = "2.3.1"
navigationCompose = "2.9.7"
navigationCompose = "2.9.8"
okhttp = "5.3.2"
palette = "1.0.0"
preference = "1.2.1"
@@ -74,21 +74,25 @@ recyclerview = "1.4.0"
#noinspection GradleDependency
uiViewbinding = "1.9.4"
viewpager2 = "1.1.0"
webkit = "1.15.0"
webkit = "1.16.0"
collection = "1.6.0"
zxingLite = "3.3.0"
koin-bom = "4.2.0"
koin-bom = "4.2.1"
biometricKtx = "1.1.0"
reorderable = "3.0.0"
reorderable = "3.1.0"
haze = "1.7.2"
timber = "5.0.1"
miuix = "0.9.0"
backdrop = "1.0.6"
capsule = "2.1.3"
lyricViewx = "1.3.2"
uiautomator = "2.3.0"
benchmarkMacroJunit4 = "1.4.1"
baselineprofile = "1.4.1"
profileinstaller = "1.4.1"
[libraries]
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
@@ -260,6 +264,9 @@ miuix-ui-android = { module = "top.yukonga.miuix.kmp:miuix-ui-android", version.
capsule = { module = "io.github.kyant0:capsule", version.ref = "capsule" }
backdrop = { module = "io.github.kyant0:backdrop", version.ref = "backdrop" }
lyricViewx = { module = "com.github.Moriafly:LyricViewX", version.ref = "lyricViewx" }
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
[bundles]
coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
@@ -291,3 +298,4 @@ room = { id = "androidx.room", version.ref = "room" }
#protobuf = { id = "com.google.protobuf", version = "0.9.4" }
download = { id = "de.undercouch.download", version = "5.6.0" }
baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" }
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+1
View File
@@ -53,3 +53,4 @@ rootProject.name = 'legado'
include ':app'
include ':modules:book'
include ':modules:rhino'
include ':baselineprofile'