<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>LOG 归档 - 日志</title>
	<atom:link href="https://www.log.show/category/log/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.log.show/category/log/</link>
	<description>LOG.SHOW</description>
	<lastBuildDate>Mon, 20 Apr 2026 06:49:48 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://www.log.show/wp-content/uploads/2025/11/cropped-logo-32x32.png</url>
	<title>LOG 归档 - 日志</title>
	<link>https://www.log.show/category/log/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Android多类型礼物特效播放队列</title>
		<link>https://www.log.show/log/android-multi-type-gift-effects-playback-queue/</link>
					<comments>https://www.log.show/log/android-multi-type-gift-effects-playback-queue/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 06:49:48 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<guid isPermaLink="false">https://www.log.show/?p=576</guid>

					<description><![CDATA[<p>模型类 接口 核心队列管理器设计 调用</p>
<p><a href="https://www.log.show/log/android-multi-type-gift-effects-playback-queue/">Android多类型礼物特效播放队列</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">模型类</h2>



<pre class="wp-block-code"><code>enum class EffectType {
    SVGA,
    VAP
}

data class EffectTask(
    val id: String,          // 特效唯一标识
    val url: String,         // 下载链接
    val type: EffectType,    // 特效类型
    val priority: Int = 0    // 可选：如果后续需要插队（如全服广播），可以预留优先级字段
)
</code></pre>



<h2 class="wp-block-heading">接口</h2>



<pre class="wp-block-code"><code>interface IEffectPlayer {
    /**
     * 准备就绪，开始播放
     * @param task 当前的特效任务
     * @param localFile 已下载好的本地文件
     * @param onComplete 播放结束（或播放失败）时必须回调此方法，通知队列继续
     */
    fun playEffect(task: EffectTask, localFile: File, onComplete: () -&gt; Unit)
}
</code></pre>



<h2 class="wp-block-heading">核心队列管理器设计</h2>



<pre class="wp-block-code"><code>import com.liulishuo.okdownload.DownloadTask
import com.liulishuo.okdownload.core.cause.EndCause
import com.liulishuo.okdownload.core.listener.DownloadListener1
import com.liulishuo.okdownload.core.listener.assist.Listener1Assist
import java.io.File
import java.util.concurrent.ConcurrentLinkedQueue

class EffectQueueManager(
    private val cacheDir: File,
    private val player: IEffectPlayer
) {
    private val queue = ConcurrentLinkedQueue&lt;EffectTask&gt;()
    private var isPlaying = false
    private var currentDownloadTask: DownloadTask? = null

    /**
     * 将特效加入队列
     */
    fun addEffect(task: EffectTask) {
        queue.offer(task)
        checkAndPlayNext()
    }

    /**
     * 检查并播放下一个
     */
    private fun checkAndPlayNext() {
        if (isPlaying || queue.isEmpty()) {
            return
        }
        
        isPlaying = true
        val nextTask = queue.poll()
        
        if (nextTask != null) {
            processTask(nextTask)
        } else {
            isPlaying = false
        }
    }

    /**
     * 处理任务：检查缓存 -&gt; 下载 -&gt; 播放
     */
    private fun processTask(task: EffectTask) {
        // 使用 URL 的 Hash 或 MD5 作为文件名，避免重复下载
        val fileName = task.url.hashCode().toString() + getExtension(task.type)
        val targetFile = File(cacheDir, fileName)

        if (targetFile.exists() &amp;&amp; targetFile.length() &gt; 0) {
            // 命中本地缓存，直接播放
            performPlay(task, targetFile)
        } else {
            // 启动 okdownload 下载
            startDownload(task, targetFile)
        }
    }

    private fun startDownload(task: EffectTask, targetFile: File) {
        currentDownloadTask = DownloadTask.Builder(task.url, cacheDir)
            .setFilename(targetFile.name)
            // 礼物特效通常较小，为了队列速度，可以根据业务需求调整并发数，这里保持默认
            .setMinIntervalMillisCallbackProcess(30)
            .setPassIfAlreadyCompleted(true)
            .build()

        currentDownloadTask?.enqueue(object : DownloadListener1() {
            override fun taskStart(task: DownloadTask, model: Listener1Assist.Listener1Model) {}
            override fun retry(task: DownloadTask, cause: EndCause) {}
            override fun connected(task: DownloadTask, blockCount: Int, currentOffset: Long, totalLength: Long) {}
            override fun progress(task: DownloadTask, currentOffset: Long, totalLength: Long) {}

            override fun taskEnd(downloadTask: DownloadTask, cause: EndCause, realCause: Exception?, model: Listener1Assist.Listener1Model) {
                if (cause == EndCause.COMPLETED) {
                    // 下载成功，去播放
                    performPlay(task, targetFile)
                } else {
                    // 下载失败：记录日志，并跳过当前任务，继续播放下一个
                    // Log.e("EffectQueue", "Download failed: ${realCause?.message}")
                    finishCurrentAndPlayNext()
                }
            }
        })
    }

    private fun performPlay(task: EffectTask, localFile: File) {
        // 交给 UI 层播放器播放
        player.playEffect(task, localFile) {
            // UI 层通知播放完毕，触发下一个
            finishCurrentAndPlayNext()
        }
    }

    private fun finishCurrentAndPlayNext() {
        isPlaying = false
        currentDownloadTask = null
        checkAndPlayNext()
    }

    /**
     * 清理队列并停止当前操作（例如退出直播间/语音房时调用）
     */
    fun release() {
        queue.clear()
        currentDownloadTask?.cancel()
        currentDownloadTask = null
        isPlaying = false
    }

    private fun getExtension(type: EffectType): String {
        return when (type) {
            EffectType.SVGA -&gt; ".svga"
            EffectType.VAP -&gt; ".mp4"
        }
    }
}
</code></pre>



<h2 class="wp-block-heading">调用</h2>



<pre class="wp-block-code"><code>class RoomActivity : AppCompatActivity() {

    private lateinit var queueManager: EffectQueueManager
    private lateinit var svgaPlayer: SVGAImageView // 你的 SVGA 控件
    private lateinit var vapPlayer: AnimView       // 你的 VAP 控件

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ... 初始化视图
        
        val effectCacheDir = File(cacheDir, "effect_cache").apply { mkdirs() }

        queueManager = EffectQueueManager(effectCacheDir, object : IEffectPlayer {
            override fun playEffect(task: EffectTask, localFile: File, onComplete: () -&gt; Unit) {
                when (task.type) {
                    EffectType.SVGA -&gt; playSVGA(localFile, onComplete)
                    EffectType.VAP -&gt; playVAP(localFile, onComplete)
                }
            }
        })
    }

    private fun playSVGA(file: File, onComplete: () -&gt; Unit) {
        svgaPlayer.visibility = View.VISIBLE
        val parser = SVGAParser(this)
        
        // 记得设置回调
        svgaPlayer.callback = object : SVGACallback {
            override fun onFinished() {
                svgaPlayer.visibility = View.GONE
                svgaPlayer.clear()
                onComplete() // 必须调用，触发下一个礼物
            }
            // ... 其他回调实现
        }

        parser.decodeFromInputStream(FileInputStream(file), file.absolutePath, object : SVGAParser.ParseCompletion {
            override fun onComplete(videoItem: SVGAVideoEntity) {
                svgaPlayer.setVideoItem(videoItem)
                svgaPlayer.startAnimation()
            }
            override fun onError() {
                onComplete() // 解析失败也要触发下一个，防止队列卡死
            }
        }, true)
    }

    private fun playVAP(file: File, onComplete: () -&gt; Unit) {
        vapPlayer.visibility = View.VISIBLE
        
        vapPlayer.setAnimListener(object : IAnimListener {
            override fun onVideoComplete() {
                vapPlayer.visibility = View.GONE
                onComplete() // 必须调用
            }
            override fun onFailed(errorType: Int, errorMsg: String?) {
                vapPlayer.visibility = View.GONE
                onComplete() // 失败也要放行
            }
            // ... 其他回调实现
        })

        vapPlayer.startPlay(file)
    }

    // 接收到新礼物消息时调用
    fun onReceiveNewGift(giftUrl: String, isSvga: Boolean) {
        val type = if (isSvga) EffectType.SVGA else EffectType.VAP
        queueManager.addEffect(EffectTask(UUID.randomUUID().toString(), giftUrl, type))
    }

    override fun onDestroy() {
        super.onDestroy()
        queueManager.release()
    }
}
</code></pre>
<p><a href="https://www.log.show/log/android-multi-type-gift-effects-playback-queue/">Android多类型礼物特效播放队列</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/android-multi-type-gift-effects-playback-queue/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用Compose实现一个简单的弹球消除小游戏</title>
		<link>https://www.log.show/log/little-game-with-compose/</link>
					<comments>https://www.log.show/log/little-game-with-compose/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Thu, 05 Feb 2026 06:16:42 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<guid isPermaLink="false">https://log.show/?p=530</guid>

					<description><![CDATA[<p>很简单的逻辑，下面有预览视频，实现了失败，成功和积分。待实现的逻辑： 简单的预览： 实现代码：</p>
<p><a href="https://www.log.show/log/little-game-with-compose/">用Compose实现一个简单的弹球消除小游戏</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">很简单的逻辑，下面有预览视频，实现了失败，成功和积分。待实现的逻辑：</p>



<ol class="wp-block-list">
<li>积分越大，速度越快，横板越小</li>



<li>关卡概念</li>



<li>随机分布砖块概念</li>



<li>添加不可消除的砖块增加可玩性</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">简单的预览：</p>



<figure class="wp-block-video"><video height="1660" style="aspect-ratio: 720 / 1660;" width="720" controls src="https://log.show/wp-content/uploads/2026/02/MuMu-录屏1.mp4"></video></figure>



<p class="wp-block-paragraph">实现代码：</p>



<pre class="wp-block-code"><code>
data class GameState(
    val ballPos: Offset = Offset(0f, 0f),
    val ballVelocity: Offset = Offset(10f, -10f),
    val paddleX: Float = 0f,
    val bricks: List&lt;Brick> = emptyList(),
    val score: Int = 0,
    val isGameOver: Boolean = false,
    val isWon: Boolean = false
)

data class Brick(
    val id: Int,
    val rect: androidx.compose.ui.geometry.Rect,
    val color: Color,
    val active: Boolean = true
)

private const val BALL_RADIUS = 20f
private const val PADDLE_WIDTH = 200f
private const val PADDLE_HEIGHT = 40f
private const val BRICK_ROWS = 6
private const val BRICK_COLS = 8
private const val BRICK_HEIGHT = 60f

@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun BreakoutGame() {
    BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
        val density = LocalDensity.current
        val screenWidth = with(density) { maxWidth.toPx() }
        val screenHeight = with(density) { maxHeight.toPx() }

        val initialBricks = remember(screenWidth) {
            val bricks = mutableListOf&lt;Brick>()
            val brickWidth = screenWidth / BRICK_COLS
            var idCounter = 0
            for (row in 0 until BRICK_ROWS) {
                for (col in 0 until BRICK_COLS) {
                    val color = Color.hsv((row * 360f / BRICK_ROWS), 0.8f, 1f)
                    bricks.add(
                        Brick(
                            id = idCounter++,
                            rect = androidx.compose.ui.geometry.Rect(
                                left = col * brickWidth + 5f,
                                top = row * BRICK_HEIGHT + 100f,
                                right = (col + 1) * brickWidth - 5f,
                                bottom = (row + 1) * BRICK_HEIGHT + 100f - 5f
                            ),
                            color = color
                        )
                    )
                }
            }
            bricks
        }

        var gameState by remember {
            mutableStateOf(
                GameState(
                    ballPos = Offset(screenWidth / 2, screenHeight / 2),
                    paddleX = (screenWidth - PADDLE_WIDTH) / 2,
                    bricks = initialBricks
                )
            )
        }

        LaunchedEffect(Unit) {
            while (isActive) {
                withFrameNanos { _ ->
                    if (!gameState.isGameOver &amp;&amp; !gameState.isWon) {
                        gameState = updateGameLogic(gameState, screenWidth, screenHeight)
                    }
                }
            }
        }

        Canvas(
            modifier = Modifier
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consume()
                        val newPaddleX = (gameState.paddleX + dragAmount.x)
                            .coerceIn(0f, screenWidth - PADDLE_WIDTH)
                        gameState = gameState.copy(paddleX = newPaddleX)
                    }
                }
        ) {
            drawRect(color = Color(0xFF121212))

            gameState.bricks.forEach { brick ->
                if (brick.active) {
                    drawRect(
                        color = brick.color,
                        topLeft = brick.rect.topLeft,
                        size = brick.rect.size
                    )
                }
            }

            drawRect(
                color = Color.Cyan,
                topLeft = Offset(gameState.paddleX, screenHeight - PADDLE_HEIGHT - 50f),
                size = Size(PADDLE_WIDTH, PADDLE_HEIGHT)
            )

            drawCircle(
                color = Color.White,
                radius = BALL_RADIUS,
                center = gameState.ballPos
            )

            drawGameUI(gameState, size.width, size.height)
        }

        if (gameState.isGameOver || gameState.isWon) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .pointerInput(Unit) {
                        detectDragGestures { _, _ -> }
                    }
                    .pointerInput(Unit) {
                        awaitPointerEventScope {
                            while (true) {
                                val event = awaitPointerEvent()
                                if (event.changes.any { it.pressed }) {
                                    gameState = GameState(
                                        ballPos = Offset(screenWidth / 2, screenHeight / 2),
                                        ballVelocity = Offset(
                                            if (Random.nextBoolean()) 10f else -10f,
                                            -10f
                                        ),
                                        paddleX = (screenWidth - PADDLE_WIDTH) / 2,
                                        bricks = initialBricks.map { it.copy(active = true) },
                                        score = 0,
                                        isGameOver = false,
                                        isWon = false
                                    )
                                }
                            }
                        }
                    }
            )
        }
    }
}

fun updateGameLogic(state: GameState, width: Float, height: Float): GameState {
    var newPos = state.ballPos + state.ballVelocity
    var newVel = state.ballVelocity
    var newScore = state.score
    var gameOver = false
    var won = false

    if (newPos.x - BALL_RADIUS &lt; 0) {
        newPos = newPos.copy(x = BALL_RADIUS)
        newVel = newVel.copy(x = -newVel.x)
    } else if (newPos.x + BALL_RADIUS > width) {
        newPos = newPos.copy(x = width - BALL_RADIUS)
        newVel = newVel.copy(x = -newVel.x)
    }

    if (newPos.y - BALL_RADIUS &lt; 0) {
        newPos = newPos.copy(y = BALL_RADIUS)
        newVel = newVel.copy(y = -newVel.y)
    }

    if (newPos.y - BALL_RADIUS > height) {
        gameOver = true
    }

    val paddleRect = androidx.compose.ui.geometry.Rect(
        left = state.paddleX,
        top = height - PADDLE_HEIGHT - 50f,
        right = state.paddleX + PADDLE_WIDTH,
        bottom = height - 50f
    )

    if (newPos.y + BALL_RADIUS >= paddleRect.top &amp;&amp;
        newPos.y - BALL_RADIUS &lt;= paddleRect.bottom &amp;&amp;
        newPos.x >= paddleRect.left &amp;&amp;
        newPos.x &lt;= paddleRect.right &amp;&amp;
        newVel.y > 0
    ) {
        newVel = newVel.copy(y = -newVel.y)
        val hitPercent = (newPos.x - paddleRect.center.x) / (PADDLE_WIDTH / 2)
        newVel = newVel.copy(x = newVel.x + hitPercent * 5f)
    }

    val newBricks = state.bricks.toMutableList()
    var hitBrick = false

    for (i in newBricks.indices) {
        val brick = newBricks&#91;i]
        if (brick.active) {
            val ballRect = androidx.compose.ui.geometry.Rect(
                newPos.x - BALL_RADIUS, newPos.y - BALL_RADIUS,
                newPos.x + BALL_RADIUS, newPos.y + BALL_RADIUS
            )

            if (brick.rect.overlaps(ballRect)) {
                newBricks&#91;i] = brick.copy(active = false)
                newVel = newVel.copy(y = -newVel.y)
                newScore += 10
                hitBrick = true
                break
            }
        }
    }

    if (newBricks.none { it.active }) {
        won = true
    }

    return state.copy(
        ballPos = newPos,
        ballVelocity = newVel,
        bricks = newBricks,
        score = newScore,
        isGameOver = gameOver,
        isWon = won
    )
}

fun DrawScope.drawGameUI(state: GameState, width: Float, height: Float) {
    val paint = android.graphics.Paint().apply {
        textSize = 60f
        color = android.graphics.Color.WHITE
        textAlign = android.graphics.Paint.Align.LEFT
    }

    drawContext.canvas.nativeCanvas.drawText("Score: ${state.score}", 50f, 80f, paint)

    if (state.isGameOver) {
        paint.textAlign = android.graphics.Paint.Align.CENTER
        paint.textSize = 100f
        paint.color = android.graphics.Color.RED
        drawContext.canvas.nativeCanvas.drawText("GAME OVER", width / 2, height / 2, paint)
        paint.textSize = 50f
        paint.color = android.graphics.Color.WHITE
        drawContext.canvas.nativeCanvas.drawText("Tap to Restart", width / 2, height / 2 + 100f, paint)
    } else if (state.isWon) {
        paint.textAlign = android.graphics.Paint.Align.CENTER
        paint.textSize = 100f
        paint.color = android.graphics.Color.GREEN
        drawContext.canvas.nativeCanvas.drawText("YOU WIN!", width / 2, height / 2, paint)
        paint.textSize = 50f
        paint.color = android.graphics.Color.WHITE
        drawContext.canvas.nativeCanvas.drawText("Tap to Restart", width / 2, height / 2 + 100f, paint)
    }
}</code></pre>
<p><a href="https://www.log.show/log/little-game-with-compose/">用Compose实现一个简单的弹球消除小游戏</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/little-game-with-compose/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://log.show/wp-content/uploads/2026/02/MuMu-录屏1.mp4" length="1996831" type="video/mp4" />

			</item>
		<item>
		<title>Android ViewPager2嵌套滑动冲突</title>
		<link>https://www.log.show/log/android-viewpager2-nested-problem/</link>
					<comments>https://www.log.show/log/android-viewpager2-nested-problem/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Sat, 27 Dec 2025 10:54:19 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Android开发]]></category>
		<category><![CDATA[ViewPager2]]></category>
		<category><![CDATA[嵌套滑动]]></category>
		<category><![CDATA[嵌套问题]]></category>
		<category><![CDATA[开发]]></category>
		<category><![CDATA[滑动冲突]]></category>
		<guid isPermaLink="false">https://log.show/?p=484</guid>

					<description><![CDATA[<p>当你使用两个ViewPager2嵌套时，确实是会有恶心的滑动冲突。默认是优先响应父布局的ViewPager2的 [&#8230;]</p>
<p><a href="https://www.log.show/log/android-viewpager2-nested-problem/">Android ViewPager2嵌套滑动冲突</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">当你使用两个ViewPager2嵌套时，确实是会有恶心的滑动冲突。默认是优先响应父布局的ViewPager2的。</p>



<p class="wp-block-paragraph">而如果你希望它优先响应子布局，当子布局滑动到最后一格的时候才响应父布局，下面是我的解决方案：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">以下内容，我们假设父布局的ViewPager2存在A,B,C,D四个页面，A页面存在子ViewPager2</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">首先我是用的广播来触发的：</p>



<ol class="wp-block-list">
<li>默认关闭父布局滑动</li>



<li>子布局滑动到最后一格，发送广播通知父布局打开滑动</li>



<li>父布局切换A，关闭父布局滑动</li>
</ol>



<p class="wp-block-paragraph">但是问题很明显，我从B,C,D重新滑到A页面，父布局是已经被通知关闭滑动的，我没办法再次滑动返回B,C,D页面。所以我采用了下面的Google提供的方案。</p>



<h3 class="wp-block-heading">首先新建一个工具 NestedScrollableHost</h3>



<pre class="wp-block-code"><code>
class NestedScrollableHost @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    private var touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    private var initialX = 0f
    private var initialY = 0f

    // 找到直接子节点中的 ViewPager2
    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while (v != null &amp;&amp; v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }

    private val child: View? get() = if (childCount &gt; 0) getChildAt(0) else null

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            ViewPager2.ORIENTATION_HORIZONTAL -&gt; child?.canScrollHorizontally(direction) ?: false
            ViewPager2.ORIENTATION_VERTICAL -&gt; child?.canScrollVertically(direction) ?: false
            else -&gt; throw IllegalArgumentException()
        }
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        handleInterceptTouchEvent(e)
        return super.onInterceptTouchEvent(e)
    }

    private fun handleInterceptTouchEvent(e: MotionEvent) {
        val orientation = parentViewPager?.orientation ?: return

        // 只有当子 View 能够滑动时，才处理拦截逻辑
        if (!canChildScroll(orientation, -1f) &amp;&amp; !canChildScroll(orientation, 1f)) {
            return
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)
        } else if (e.action == MotionEvent.ACTION_MOVE) {
            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL

            val scaledDx = dx.absoluteValue * if (isVpHorizontal) 1f else .5f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) .5f else 1f

            if (scaledDx &gt; touchSlop || scaledDy &gt; touchSlop) {
                if (isVpHorizontal == (scaledDy &gt; scaledDx)) {
                    // 滑动方向与 ViewPager2 方向不一致，允许父级拦截
                    parent.requestDisallowInterceptTouchEvent(false)
                } else {
                    // 滑动方向一致，判断子 View 是否能继续滑动
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        parent.requestDisallowInterceptTouchEvent(true)
                    } else {
                        parent.requestDisallowInterceptTouchEvent(false)
                    }
                }
            }
        }
    }
}</code></pre>



<h3 class="wp-block-heading">在子布局外嵌套这个View</h3>



<p class="wp-block-paragraph">在默认情况下，你嵌套之后发现父布局的滑动不会被触发，子布局正常了。这个问题的原因很简单，解决方案↓</p>



<h3 class="wp-block-heading">解决滑动监听的问题</h3>



<p class="wp-block-paragraph">ViewPager2实际上是一个RecyclerView，因此它默认打开了越界回弹（OverScroll）。就是那种滑动到边缘的拉伸效果（早期Android版本可能是一个奇怪的发光效果）。当这个效果触发时，系统会认为内层 View 已经处理了这次滑动，从而不会把剩余的位移传递给父布局。</p>



<p class="wp-block-paragraph">所以你需要禁用这个属性：</p>



<pre class="wp-block-code"><code>// 在代码中找到内层的 ViewPager2
val innerViewPager = findViewById&lt;ViewPager2&gt;(R.id.inner_view_pager)

// ViewPager2 内部是一个 RecyclerView，需要找到并设置它
(innerViewPager.getChildAt(0) as? RecyclerView)?.overScrollMode = View.OVER_SCROLL_NEVER</code></pre>



<p class="wp-block-paragraph">当然也有其它的可能，比如你的NestedScrollableHost嵌套层级太深。你需要保证你的NestedScrollableHost下一层就是你的Viewpager2的子视图！</p>



<h3 class="wp-block-heading">其他思路：</h3>



<p class="wp-block-paragraph">这里有一个更粗暴，更清晰的方案，但是可能会有奇怪的问题，不过我认为可以记录下来(与我之前的想法比较接近，可能会有奇怪的问题)：</p>



<pre class="wp-block-code"><code>innerViewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        val isAtEnd = position == (innerViewPager.adapter?.itemCount ?: 0) - 1
        val isAtStart = position == 0
        
        if ((isAtEnd &amp;&amp; positionOffsetPixels == 0) || (isAtStart &amp;&amp; positionOffsetPixels == 0)) {
            // 到达边界，允许父类拦截
            innerViewPager.parent.requestDisallowInterceptTouchEvent(false)
        } else {
            // 未到边界，禁止父类拦截
            innerViewPager.parent.requestDisallowInterceptTouchEvent(true)
        }
    }
})</code></pre>
<p><a href="https://www.log.show/log/android-viewpager2-nested-problem/">Android ViewPager2嵌套滑动冲突</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/android-viewpager2-nested-problem/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Android 的毛玻璃(高斯模糊)方案</title>
		<link>https://www.log.show/log/android-blur/</link>
					<comments>https://www.log.show/log/android-blur/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Thu, 25 Dec 2025 04:29:36 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[Android 12]]></category>
		<category><![CDATA[Android Development]]></category>
		<category><![CDATA[BlurView]]></category>
		<category><![CDATA[CPU Usage]]></category>
		<category><![CDATA[FPS]]></category>
		<category><![CDATA[Gaussian Blur]]></category>
		<category><![CDATA[Performance Monitoring]]></category>
		<category><![CDATA[UI Optimization]]></category>
		<category><![CDATA[WindowBlur]]></category>
		<category><![CDATA[性能优化]]></category>
		<category><![CDATA[毛玻璃效果]]></category>
		<category><![CDATA[高斯模糊]]></category>
		<guid isPermaLink="false">https://log.show/?p=479</guid>

					<description><![CDATA[<p>原生方案 Android在Android12更新时支持了原生的高斯模糊，但是基本只能用于窗口模糊或者ViewG [&#8230;]</p>
<p><a href="https://www.log.show/log/android-blur/">Android 的毛玻璃(高斯模糊)方案</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">原生方案</h2>



<p class="wp-block-paragraph">Android在<a href="https://source.android.com/docs/core/display/window-blurs?hl=zh-cn">Android12更新时支持了原生的高斯模糊</a>，但是基本只能用于窗口模糊或者ViewGroup模糊。大概效果如下：</p>



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="814" height="544" src="https://log.show/wp-content/uploads/2025/12/image-1.png" alt="" class="wp-image-480" srcset="https://www.log.show/wp-content/uploads/2025/12/image-1.png 814w, https://www.log.show/wp-content/uploads/2025/12/image-1-300x200.png 300w, https://www.log.show/wp-content/uploads/2025/12/image-1-768x513.png 768w" sizes="(max-width: 814px) 100vw, 814px" /></figure>



<p class="wp-block-paragraph">分别为：仅背景模糊处理 (a)、仅模糊处理后方屏幕 (b)、背景模糊处理和模糊处理后方屏幕 (c)。我们希望达到的要求为a效果，下面是官方的a效果的使用方案步骤。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">对浮动窗口使用背景模糊处理可实现窗口背景效果，这是底层内容的模糊处理图像。如需为窗口添加模糊处理的背景，请执行以下操作：</p>



<ol class="wp-block-list">
<li>调用&nbsp;<a href="https://developer.android.com/reference/android/view/Window?hl=zh-cn#setBackgroundBlurRadius(int)">Window#setBackgroundBlurRadius(int)</a>&nbsp;设置背景模糊处理半径。或者，在窗口主题中设置&nbsp;<a href="https://developer.android.com/reference/android/R.attr?hl=zh-cn#windowBackgroundBlurRadius">R.attr.windowBackgroundBlurRadius</a>。</li>



<li>将&nbsp;<a href="https://developer.android.com/reference/android/R.attr?hl=zh-cn#windowIsTranslucent">R.attr.windowIsTranslucent</a>&nbsp;设为 true，使窗口变为半透明。模糊处理是在窗口 Surface 下面绘制的，因此窗口必须是半透明的，才能显示出模糊处理效果。</li>



<li>（可选）调用&nbsp;<a href="https://developer.android.com/reference/android/view/Window?hl=zh-cn#setBackgroundDrawableResource(int)">Window#setBackgroundDrawableResource(int)</a>&nbsp;添加具有半透明颜色的矩形窗口背景可绘制对象。或者，在窗口主题中设置&nbsp;<a href="https://developer.android.com/reference/android/R.attr?hl=zh-cn#windowBackground">R.attr.windowBackground</a>。</li>



<li>对于具有圆角的窗口，可通过将具有<a href="https://developer.android.com/guide/topics/resources/drawable-resource?hl=zh-cn#corners-element">圆角</a>的&nbsp;<a href="https://developer.android.com/guide/topics/resources/drawable-resource?hl=zh-cn#Shape">ShapeDrawable</a>&nbsp;设为窗口背景可绘制对象来确定模糊处理区域的圆角。</li>



<li>处理启用和停用模糊处理的状态。如需了解详情，请参阅<a href="https://source.android.com/docs/core/display/window-blurs?hl=zh-cn#guidelines">在应用中使用窗口模糊处理的准则</a>部分。</li>
</ol>
</blockquote>



<p class="wp-block-paragraph">但是这个方案有很多问题：</p>



<ol class="wp-block-list">
<li>我不是在单独窗口使用模糊，或者我希望在当前页面简单显示一个View(Group)</li>



<li>低于Android12的处理逻辑过于复杂</li>
</ol>



<h2 class="wp-block-heading">第三方库方案</h2>



<p class="wp-block-paragraph">根据更新时间，使用成本，以及Star数量，我选择使用：</p>



<p class="wp-block-paragraph"><a href="https://github.com/Dimezis/BlurView">https://github.com/Dimezis/BlurView</a></p>



<p class="wp-block-paragraph"> 它不仅可以在多种View显示需求下使用，并且自动适配了低于Android12的代码。</p>



<h3 class="wp-block-heading">使用步骤</h3>



<h4 class="wp-block-heading">1. 导入第三方库</h4>



<h4 class="wp-block-heading">2. 设置布局</h4>



<ul class="wp-block-list">
<li>你需要用一个 `BlurTarget` 包裹你目前的根布局。假设你的毛玻璃View出现在固定的位置，则你只需要包裹原来View的父布局即可，这样比较节省占用！</li>



<li>然后你需要把需要模糊的内容放在 `BlurView` 布局中</li>



<li>注意注意注意！！！ `BlurView` 与 `BlurTarget` 为同级，而非父子布局！！！</li>
</ul>



<p class="wp-block-paragraph">示例：</p>



<pre class="wp-block-code"><code>    &lt;!--This is the content to be blurred by the BlurView. 
    It will render normally, and BlurView will use its snapshot for blurring--&gt;
    &lt;eightbitlab.com.blurview.BlurTarget
        android:id="@+id/target"
        android:layout_width="match_parent"
        android:layout_height="match_parent"&gt;
        
        &lt;!--Your main content here--&gt;

    &lt;/eightbitlab.com.blurview.BlurTarget&gt;

    &lt;eightbitlab.com.blurview.BlurView
      android:id="@+id/blurView"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:blurOverlayColor="@color/colorOverlay"&gt;
    
       &lt;!--Any child View here, TabLayout for example. This View will NOT be blurred --&gt;
    
    &lt;/eightbitlab.com.blurview.BlurView&gt;</code></pre>



<h4 class="wp-block-heading">3. 模糊配置</h4>



<pre class="wp-block-code"><code>    private fun setupBlurView() {
        val radius = 20f  //模糊半径，越大占用越多！

        val windowBackground = window.decorView.background

        binding.blurView
            .setupWith(binding.target)           // ← 要模糊的根View，通常是最接近的父容器
            .setFrameClearDrawable(windowBackground)  // 防止透明区域变黑/变奇怪
            .setBlurRadius(radius)
    }</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">性能适配</h2>



<p class="wp-block-paragraph">以上代码基本可以实现很多毛玻璃效果。但是我推荐你在低端机型关闭这个效果，使用地透明度的纯色背景。</p>



<p class="wp-block-paragraph">下面是一个直接可以使用的性能监控工具，它会在运行10秒后返回平均结果，你可以根据结果重新设置布局属性（当然我推荐你保存下来这个结果，避免每次打开都重复获取和设置）：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">注意：</p>



<ul class="wp-block-list">
<li>CPU 使用率：Android 从 API 26（Oreo）开始，系统越来越严格限制普通 App 读取其他进程的 /proc/[pid]/stat，但读取 整体系统 的 /proc/stat 仍然在大多数设备上可行（不保证 100% 成功，尤其是一些定制 ROM 或高安全设备可能会返回空或抛异常）。</li>



<li>这个工具类只计算整体系统 CPU（不是只算你 App 的），因为你关心的是“设备当前是否很忙”，而不是只看自己 App。</li>
</ul>
</blockquote>



<h3 class="wp-block-heading">工具代码：</h3>



<pre class="wp-block-code"><code>import android.os.Handler
import android.os.Looper
import android.view.Choreographer
import java.io.RandomAccessFile
import kotlin.math.max
import kotlin.math.min

/**
 * 10秒性能快照工具类
 * - 监控 FPS（UI 渲染帧率）
 * - 监控整体系统 CPU 使用率（通过 /proc/stat）
 * - 10秒后返回结果，并给出是否“高占用”的简单判断
 */
class PerformanceSnapshot private constructor() {

    companion object {
        // 单例懒加载
        val instance by lazy { PerformanceSnapshot() }
    }

    private var frameCount = 0L
    private var lastFrameTimeNs = 0L
    private var fpsSamples = mutableListOf&lt;Double&gt;()   // 每秒一个 FPS 样本

    private var cpuSamples = mutableListOf&lt;Float&gt;()    // 每秒一个 CPU% 样本

    private var prevIdle = -1L
    private var prevTotal = -1L

    private val handler = Handler(Looper.getMainLooper())
    private val choreographer = Choreographer.getInstance()

    private val frameCallback = object : Choreographer.FrameCallback {
        override fun doFrame(frameTimeNanos: Long) {
            if (lastFrameTimeNs == 0L) {
                lastFrameTimeNs = frameTimeNanos
            }

            frameCount++

            val elapsedNs = frameTimeNs - lastFrameTimeNs
            if (elapsedNs &gt;= 1_000_000_000L) {  // 每秒计算一次
                val fps = (frameCount * 1_000_000_000.0) / elapsedNs
                fpsSamples.add(fps.coerceIn(0.0, 120.0))  // 限制合理范围

                frameCount = 0
                lastFrameTimeNs = frameTimeNanos
            }

            choreographer.postFrameCallback(this)
        }
    }

    /**
     * 采集一次 CPU 使用率（整体系统）
     * 返回 -1 表示读取失败
     */
    private fun sampleCpuUsage(): Float {
        try {
            val reader = RandomAccessFile("/proc/stat", "r")
            val line = reader.readLine() ?: return -1f
            reader.close()

            val toks = line.split("\\s+".toRegex()).filter { it.isNotBlank() }
            if (toks.size &lt; 11) return -1f

            // cpu user nice system idle iowait irq softirq steal guest guest_nice
            val user   = toks&#91;1].toLongOrNull() ?: 0L
            val nice   = toks&#91;2].toLongOrNull() ?: 0L
            val system = toks&#91;3].toLongOrNull() ?: 0L
            val idle   = toks&#91;4].toLongOrNull() ?: 0L
            val iowait = toks&#91;5].toLongOrNull() ?: 0L
            val irq    = toks&#91;6].toLongOrNull() ?: 0L
            val softirq= toks&#91;7].toLongOrNull() ?: 0L
            // 忽略 steal/guest 等（通常为0）

            val total = user + nice + system + idle + iowait + irq + softirq
            val idleDiff = idle - prevIdle
            val totalDiff = total - prevTotal

            return if (prevTotal &gt; 0 &amp;&amp; totalDiff &gt; 0) {
                val usage = 100f * (totalDiff - idleDiff) / totalDiff
                usage.coerceIn(0f, 100f)
            } else {
                -1f
            }.also {
                prevIdle = idle
                prevTotal = total
            }
        } catch (e: Exception) {
            return -1f
        }
    }

    /**
     * 开始 10 秒监控，返回结果
     * @param onComplete 回调：(平均FPS, 平均CPU%, 是否认为是高占用)
     */
    fun startMonitoring(onComplete: (avgFps: Double, avgCpu: Float, isHighLoad: Boolean, message: String) -&gt; Unit) {
        // 重置
        fpsSamples.clear()
        cpuSamples.clear()
        frameCount = 0L
        lastFrameTimeNs = 0L
        prevIdle = -1L
        prevTotal = -1L

        // 第一次采样 CPU（建立基线）
        sampleCpuUsage()

        // 开启 FPS 监控
        choreographer.postFrameCallback(frameCallback)

        // 每秒采样一次 CPU（大约 10 次）
        var seconds = 0
        val cpuSampler = object : Runnable {
            override fun run() {
                if (seconds &gt;= 10) {
                    // 停止监控
                    choreographer.removeFrameCallback(frameCallback)
                    handler.removeCallbacks(this)

                    // 计算平均值
                    val avgFps = if (fpsSamples.isNotEmpty()) {
                        fpsSamples.average()
                    } else 0.0

                    val avgCpu = if (cpuSamples.isNotEmpty()) {
                        val valid = cpuSamples.filter { it &gt;= 0 }
                        if (valid.isNotEmpty()) valid.average().toFloat() else -1f
                    } else -1f

                    // 我的简单高占用判断逻辑（可自行调整阈值）
                    val isHighLoad = when {
                        avgCpu &lt; 0 -&gt; false                     // 无法读取 CPU → 不算高
                        avgCpu &gt; 80f -&gt; true                     // CPU 非常高
                        avgCpu &gt; 65f &amp;&amp; avgFps &lt; 48.0 -&gt; true    // CPU 中高 + FPS 偏低
                        avgFps &lt; 42.0 -&gt; true                    // FPS 严重偏低（即使 CPU 不高）
                        else -&gt; false
                    }

                    val msg = buildString {
                        append("10秒监控结果：\n")
                        append("  • 平均 FPS: %.1f\n".format(avgFps))
                        append("  • 平均 CPU: %.1f%%\n".format(avgCpu))
                        append("  • 判断：${if (isHighLoad) "高占用（建议关闭模糊）" else "正常"}")
                        if (avgCpu &lt; 0) append("\n（CPU 数据读取失败，仅参考 FPS）")
                    }

                    onComplete(avgFps, avgCpu, isHighLoad, msg)
                    return
                }

                val cpu = sampleCpuUsage()
                if (cpu &gt;= 0) cpuSamples.add(cpu)

                seconds++
                handler.postDelayed(this, 1000L)
            }
        }

        handler.post(cpuSampler)
    }
}</code></pre>



<h3 class="wp-block-heading">使用代码：</h3>



<pre class="wp-block-code"><code>// 在适当的地方调用（比如 Fragment/Activity 的某个按钮或初始化时）
PerformanceSnapshot.instance.startMonitoring { avgFps, avgCpu, isHighLoad, message -&gt;
    Log.d("Perf", message)
    
    if (isHighLoad) {
        // 关闭模糊
        binding.blurView.setBlurRadius(0f)
        // 或完全 detach：blurView.setupWith(null) 等
    } else {
        // 恢复模糊
        binding.blurView.setBlurRadius(20f)
    }
}</code></pre>



<p class="wp-block-paragraph">我依然推荐你在打开APP（或者某个Activity）3-5秒后再启动这个工具。因为内容加载以及页面频繁跳转CPU计算或许会有影响！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2026年1月4日更新内容</h3>



<p class="wp-block-paragraph">如果你是在Activity中调用了一个次级窗口或者其它Activity，那么你或许需要使用下面的方案，以下以BottomSheetDialogFragment</p>



<pre class="wp-block-code"><code>            //差不多就是，在跟View添加BlurTarget，然后获取根布局的BlurTarget，setupWith它就可以了
            val activity = context as? Activity ?: return
            val targetView = activity.window.decorView.findViewById&lt;BlurTarget>(R.id.target)
            mVB.blurView.setupWith(targetView)
                .setBlurRadius(15f)
                .setFrameClearDrawable(activity.window.decorView.background)
                .setBlurAutoUpdate(true)</code></pre>



<p class="wp-block-paragraph">圆角的方案：</p>



<pre class="wp-block-code"><code>        viewModel.blurView.outlineProvider = object : ViewOutlineProvider() {
                override fun getOutline(view: View, outline: Outline) {
                    // 直接根据 View 的宽高设置圆角矩形
                    // 16f.dpToPx() 是将 16dp 转为像素，如果你没有扩展函数，可以直接写数值（比如 48）
                    val radiusInPx = TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics
                    )

                    // 设置圆角矩形区域 (左, 上, 右, 下, 半径)
                    outline.setRoundRect(0, 0, view.width, view.height, radiusInPx)
                }
            }</code></pre>
<p><a href="https://www.log.show/log/android-blur/">Android 的毛玻璃(高斯模糊)方案</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/android-blur/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Android开发中屏蔽第三方库的权限</title>
		<link>https://www.log.show/log/android-prerelease-permission-check/</link>
					<comments>https://www.log.show/log/android-prerelease-permission-check/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Fri, 19 Dec 2025 05:55:33 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[AndroidManifest]]></category>
		<category><![CDATA[Android开发]]></category>
		<category><![CDATA[APK反编译]]></category>
		<category><![CDATA[BOOT_COMPLETED]]></category>
		<category><![CDATA[tools:node="remove"]]></category>
		<category><![CDATA[TrueTime]]></category>
		<category><![CDATA[上架审核]]></category>
		<category><![CDATA[权限屏蔽]]></category>
		<category><![CDATA[第三方库权限]]></category>
		<category><![CDATA[自启动权限]]></category>
		<guid isPermaLink="false">https://log.show/?p=474</guid>

					<description><![CDATA[<p>下面是实际应用中的例子。 因为需要准确时间，所以导入了TrueTime的第三方库，而这个库会声明一个自启动权限 [&#8230;]</p>
<p><a href="https://www.log.show/log/android-prerelease-permission-check/">Android开发中屏蔽第三方库的权限</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">下面是实际应用中的例子。</p>



<p class="wp-block-paragraph">因为需要准确时间，所以导入了TrueTime的第三方库，而这个库会声明一个自启动权限<code>BOOT_COMPLETED</code>，来自于<code>AndroidManifest.xml: com.instacart.library.truetime.BootCompletedBroadcastReceiver</code></p>



<figure class="wp-block-embed is-type-wp-embed"><div class="wp-block-embed__wrapper">
<blockquote class="wp-embedded-content" data-secret="i3xWcqJdHW"><a href="https://log.show/log/use-ntp-server-time-on-mobile-device/">在移动设备上使用准确的时间</a></blockquote><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  title="《 在移动设备上使用准确的时间 》—日志" src="https://log.show/log/use-ntp-server-time-on-mobile-device/embed/#?secret=lNXmZW7TWu#?secret=i3xWcqJdHW" data-secret="i3xWcqJdHW" width="500" height="282" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
</div></figure>



<p class="wp-block-paragraph">第一次看到我没在意，我删除了自己APP中的这个权限。</p>



<p class="wp-block-paragraph">再次被上架驳回后我发现这是第三方库的权限。在解决这个问题之前，或者说我们上架之前是需要先自查的，下面是自查方案：</p>



<ol class="wp-block-list">
<li>使用apktool反编译后查看打包之后的<code>AndroidManifest.xml</code>文件内是否存在敏感权限</li>
</ol>



<pre class="wp-block-code"><code># 反编译APK到指定目录
apktool d your_app.apk -o apk_decompile

# 查看合并后的AndroidManifest.xml
cat apk_decompile/AndroidManifest.xml | grep -i "BOOT_COMPLETED"</code></pre>



<ol start="2" class="wp-block-list">
<li>在源码中排除第三方库的该声明</li>
</ol>



<pre class="wp-block-code"><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"  &lt;!-- 必须添加这个命名空间 --&gt;
    package="com.your.package.name"&gt;

    &lt;!-- 1. 移除开机自启权限（如果有的话） --&gt;
    &lt;uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" 
                     tools:node="remove"/&gt;

    &lt;application&gt;

    &lt;/application&gt;

&lt;/manifest&gt;</code></pre>



<ol start="3" class="wp-block-list">
<li>Gradle 构建时排查依赖</li>
</ol>



<pre class="wp-block-code"><code># 查看所有依赖（Android项目）
./gradlew app:dependencies --configuration releaseRuntimeClasspath

# 结合grep筛选（Mac/Linux）
./gradlew app:dependencies | grep -i "boot"</code></pre>



<ol start="4" class="wp-block-list">
<li>我使用的方案，也是最简单的方案：使用jadx反编译后查看配置文件中的权限</li>
</ol>



<h2 class="wp-block-heading">下面是解决这个问题的方案：</h2>



<h3 class="wp-block-heading">1. 删除第三方库</h3>



<p class="wp-block-paragraph">简单见效快</p>



<h3 class="wp-block-heading">2. 移出权限</h3>



<pre class="wp-block-code"><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"  &lt;!-- 必须添加这个命名空间 --&gt;
    package="com.miaoyuyin.miao"&gt;

    &lt;!-- 保留你原有所有配置，仅添加以下移除声明 --&gt;

    &lt;!-- 1. 移除开机自启权限（如果第三方库声明了） --&gt;
    &lt;uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" 
                     tools:node="remove"/&gt;

    &lt;application
        &lt;!-- 保留你原有所有application属性和子组件 --&gt;
        &gt;

        &lt;!-- 2. 精准移除TrueTime库的开机广播接收器 --&gt;
        &lt;receiver
            android:name="com.instacart.library.truetime.BootCompletedBroadcastReceiver"
            tools:node="remove" /&gt;

        &lt;!-- 你原有所有Activity/Service/Provider等组件 --&gt;

    &lt;/application&gt;
&lt;/manifest&gt;</code></pre>



<p class="wp-block-paragraph">在修改之后，再次提交之前，你可以使用上面的自查方案再次验证是否已经修改完善！</p>
<p><a href="https://www.log.show/log/android-prerelease-permission-check/">Android开发中屏蔽第三方库的权限</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/android-prerelease-permission-check/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>一个Android的Log类</title>
		<link>https://www.log.show/log/android-log-utils-wrapper/</link>
					<comments>https://www.log.show/log/android-log-utils-wrapper/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Wed, 17 Dec 2025 05:16:22 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[Android开发]]></category>
		<category><![CDATA[Debug日志]]></category>
		<category><![CDATA[Kotlin封装]]></category>
		<category><![CDATA[Logger库]]></category>
		<category><![CDATA[LogUtils]]></category>
		<category><![CDATA[Release禁用]]></category>
		<category><![CDATA[内存泄漏]]></category>
		<category><![CDATA[异常堆栈]]></category>
		<category><![CDATA[日志工具类]]></category>
		<category><![CDATA[格式化日志]]></category>
		<guid isPermaLink="false">https://log.show/?p=468</guid>

					<description><![CDATA[<p>优点： 下面是类的内容： 在使用之前，需要在Application 中调用： 使用示例 最终可能的输出效果：  [&#8230;]</p>
<p><a href="https://www.log.show/log/android-log-utils-wrapper/">一个Android的Log类</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">优点：</p>



<ul class="wp-block-list">
<li>代码级禁用，DEBUG才有日志可以看到</li>



<li>isLoggable运行时拦截</li>



<li>零泄露风险</li>



<li>边框包裹，自动格式化</li>



<li>精准定位，点击跳转</li>



<li>使用简单，动态调用和TAG</li>



<li>零开销潜力，内存友好</li>
</ul>



<p class="wp-block-paragraph">下面是类的内容：</p>



<pre class="wp-block-code"><code>import android.util.Log
import com.orhanobut.logger.Logger
import com.orhanobut.logger.PrettyFormatStrategy
import your.package.app.BuildConfig

/**
 * 日志工具类
 * 核心规则：Release包完全禁用所有日志输出，Debug包正常输出格式化日志
 * 支持日志级别：VERBOSE/DEBUG/INFO/WARN/ERROR
 * 支持场景：无Tag日志、自定义Tag、带异常堆栈、可变参数格式化
 */
object LogUtils {
    // 全局默认Tag（可根据项目修改）
    private const val DEFAULT_TAG = "MY_APP"

    /**
     * 初始化日志配置（在App.kt的onCreate中调用）
     */
    fun init() {
        if (BuildConfig.DEBUG) {
            // Debug模式：初始化格式化日志配置
            val formatStrategy = PrettyFormatStrategy.newBuilder()
                .showThreadInfo(false)      // 显示线程信息
                .methodCount(2)            // 显示调用方法行数（2行足够定位）
                .methodOffset(1)           // 方法偏移量
                .tag(DEFAULT_TAG)          // 默认Tag
                .build()
            Logger.addLogAdapter(object : com.orhanobut.logger.AndroidLogAdapter(formatStrategy) {
                // 仅Debug模式允许日志输出
                override fun isLoggable(priority: Int, tag: String?): Boolean = BuildConfig.DEBUG
            })
        } else {
            // Release模式：移除所有日志适配器，确保无日志输出
            Logger.clearLogAdapters()
        }
    }

    // ====================== 基础日志方法（无Tag，使用默认Tag） ======================
    fun v(message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.v(message, *args)
    }

    fun d(message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.d(message, *args)
    }

    fun i(message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.i(message, *args)
    }

    fun w(message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.w(message, *args)
    }

    fun e(message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.e(message, *args)
    }

    // ====================== 带异常堆栈的日志（无Tag） ======================
    fun e(throwable: Throwable, message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.e(throwable, message, *args)
    }

    // ====================== 自定义Tag的日志方法 ======================
    fun v(tag: String, message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.t(tag).v(message, *args)
    }

    fun d(tag: String, message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.t(tag).d(message, *args)
    }

    fun i(tag: String, message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.t(tag).i(message, *args)
    }

    fun w(tag: String, message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.t(tag).w(message, *args)
    }

    fun e(tag: String, throwable: Throwable, message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.t(tag).e(throwable, message, *args)
    }

    fun e(tag: String, message: String, vararg args: Any) {
        if (BuildConfig.DEBUG) Logger.t(tag).e(message, *args)
    }

    // ====================== 兼容原生Log的极简方法（可选） ======================
    /**
     * 极简Debug日志（自动取调用类名作为Tag）
     */
    fun simpleD(message: String) {
        if (BuildConfig.DEBUG) {
            val tag = getCallerClassName()
            Log.d(tag, message)
        }
    }

    /**
     * 极简Error日志（自动取调用类名作为Tag，带异常）
     */
    fun simpleE(throwable: Throwable, message: String) {
        if (BuildConfig.DEBUG) {
            val tag = getCallerClassName()
            Log.e(tag, message, throwable)
        }
    }

    /**
     * 私有方法：获取调用者的类名（用于simple系列日志的自动Tag）
     */
    private fun getCallerClassName(): String {
        return try {
            // 获取调用栈，定位到LogUtils外部的调用类
            val stackTrace = Thread.currentThread().stackTrace
            val callerStackIndex = 4 // 栈索引：0=VM,1=Thread,2=getCallerClassName,3=simpleD/E,4=外部调用者
            val callerClass = Class.forName(stackTrace&#91;callerStackIndex].className)
            callerClass.simpleName // 只取类名（不含包名），简化Tag
        } catch (e: Exception) {
            DEFAULT_TAG // 异常时使用默认Tag
        }
    }

    /**
     * 清空日志（Release模式下无操作）
     */
    fun clear() {
        if (BuildConfig.DEBUG) Logger.clearLogAdapters()
    }
}</code></pre>



<p class="wp-block-paragraph">在使用之前，需要在<code>Application</code> 中调用：</p>



<pre class="wp-block-code"><code>LogUtils.init()
//记得在Manifest文件中检查是否为自定义的Application</code></pre>



<p class="wp-block-paragraph">使用示例</p>



<pre class="wp-block-code"><code>import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import your.package.app.common.LogUtils // 导入你的日志工具类

class MainActivity : AppCompatActivity() {
    private val USER_ID = 1001

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

        // 使用封装后的日志方法，它会在 Debug 包中格式化输出
        LogUtils.d("MainActivity 已启动") // 无Tag，使用默认Tag
        
        // 使用带参数的格式化日志
        LogUtils.i("用户 %d 正在加载数据...", USER_ID) 
        
        // 使用自定义 Tag
        LogUtils.w("UI", "Warning: 布局文件加载可能耗时") 
        
        // 模拟一个异常日志
        try {
            // 模拟抛出异常
            throw RuntimeException("网络连接失败")
        } catch (e: Exception) {
            // 输出带异常堆栈的日志
            LogUtils.e(e, "数据加载失败，请检查网络！") 
        }
    }
}</code></pre>



<p class="wp-block-paragraph">最终可能的输出效果：</p>



<pre class="wp-block-code"><code>
// LogUtils.d("MainActivity 已启动") 的输出
D/MY_APP: ┌────────────────────────────────────────────────────────
D/MY_APP: │ MainActivity.onCreate (MainActivity.kt:15)
D/MY_APP: ├────────────────────────────────────────────────────────
D/MY_APP: │ MainActivity 已启动
D/MY_APP: └────────────────────────────────────────────────────────

// LogUtils.e(e, "数据加载失败，请检查网络！") 的输出
E/MY_APP: ┌────────────────────────────────────────────────────────
E/MY_APP: │ MainActivity.onCreate (MainActivity.kt:28)
E/MY_APP: ├────────────────────────────────────────────────────────
E/MY_APP: │ RuntimeException: 网络连接失败
E/MY_APP: │   at your.package.app.MainActivity.onCreate(MainActivity.kt:24)
E/MY_APP: │   ... (省略部分堆栈)
E/MY_APP: ├────────────────────────────────────────────────────────
E/MY_APP: │ 数据加载失败，请检查网络！
E/MY_APP: └────────────────────────────────────────────────────────</code></pre>



<p class="wp-block-paragraph">图片来自： <a href="https://www.pexels.com/zh-cn/photo/1260324">https://www.pexels.com/zh-cn/photo/1260324</a></p>
<p><a href="https://www.log.show/log/android-log-utils-wrapper/">一个Android的Log类</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/android-log-utils-wrapper/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Android重构项目的第一步</title>
		<link>https://www.log.show/log/android-refactoring-an-project/</link>
					<comments>https://www.log.show/log/android-refactoring-an-project/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Sat, 13 Dec 2025 02:50:29 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[Android Jetpack]]></category>
		<category><![CDATA[Android应用开发]]></category>
		<category><![CDATA[Glide图片加载]]></category>
		<category><![CDATA[Hilt依赖注入]]></category>
		<category><![CDATA[Kotlin协程]]></category>
		<category><![CDATA[MVVM架构]]></category>
		<category><![CDATA[Navigation组件]]></category>
		<category><![CDATA[Retrofit网络]]></category>
		<category><![CDATA[ViewBinding]]></category>
		<category><![CDATA[架构分层]]></category>
		<guid isPermaLink="false">https://log.show/?p=463</guid>

					<description><![CDATA[<p>下面的内容基于MVVM架构，不适用于纯Jetpack Compose的项目 目录结构 开启Viewbindin [&#8230;]</p>
<p><a href="https://www.log.show/log/android-refactoring-an-project/">Android重构项目的第一步</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">下面的内容基于MVVM架构，不适用于纯Jetpack Compose的项目</p>



<h2 class="wp-block-heading">目录结构</h2>



<pre class="wp-block-code"><code>com.example.myapp
├── App.kt                  // Application类 (用于初始化Hilt, Timber等)
├── di                      // Dependency Injection (依赖注入模块)
│   ├── NetworkModule.kt    // Retrofit/OkHttp 注入配置
│   └── DatabaseModule.kt   // Room 数据库注入配置
├── data                    // 数据层 (Data Layer)
│   ├── local               // 本地数据源 (Room, SharedPreferences)
│   │   ├── AppDatabase.kt
│   │   ├── dao
│   │   └── entity
│   ├── remote              // 网络数据源 (Retrofit)
│   │   ├── ApiService.kt
│   │   └── model           // 服务端返回的 JSON 实体类 (DTO)
│   ├── repository          // 仓库层 (协调 Local 和 Remote 数据)
│   │   └── UserRepository.kt
│   └── Model.kt            // 领域模型 (App内部使用的纯净Model)
├── ui                      // UI 层 (View &amp; ViewModel)
│   ├── base                // 基类
│   │   ├── BaseActivity.kt
│   │   ├── BaseFragment.kt
│   │   └── BaseViewModel.kt
│   ├── main                // 主界面功能模块
│   │   ├── MainActivity.kt
│   │   └── MainViewModel.kt
│   ├── login               // 登录功能模块
│   │   ├── LoginFragment.kt
│   │   └── LoginViewModel.kt
│   └── adapter             // RecyclerView 适配器 (如通用性强可单独放)
└── utils                   // 工具类和扩展函数
    ├── Constants.kt
    ├── Extensions.kt       // Kotlin 扩展 (ViewExt, StringExt)
    └── DateUtils.kt</code></pre>



<h2 class="wp-block-heading">开启Viewbinding</h2>



<pre class="wp-block-code"><code>android {
    ...
    buildFeatures {
        viewBinding = true
    }
}</code></pre>



<h2 class="wp-block-heading">导入需要的库和插件</h2>



<h4 class="wp-block-heading">1. 架构组件 (Android Jetpack)</h4>



<p class="wp-block-paragraph">这是 Google 官方的基础设施，用于生命周期管理和导航。</p>



<ul class="wp-block-list">
<li><strong>Lifecycle &amp; ViewModel:</strong> 管理生命周期，防止内存泄漏。</li>



<li><strong>Navigation Component:</strong> 处理 Fragment 之间的跳转（推荐单 Activity 多 Fragment 架构）。</li>
</ul>



<pre class="wp-block-code"><code>// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
// Lifecycle runtime
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
// Navigation Component
implementation "androidx.navigation:navigation-fragment-ktx:2.7.5"
implementation "androidx.navigation:navigation-ui-ktx:2.7.5"</code></pre>



<h4 class="wp-block-heading">2. 依赖注入 (Dependency Injection)（可选，有的项目用不到）</h4>



<ul class="wp-block-list">
<li><strong>Hilt:</strong> Google 官方推荐，基于 Dagger 但更易用。它能帮你自动管理对象的创建和生命周期（如自动注入 Repository 到 ViewModel）。也可以使用Dagger2。</li>
</ul>



<pre class="wp-block-code"><code>implementation "com.google.dagger:hilt-android:2.48"
kapt "com.google.dagger:hilt-android-compiler:2.48"</code></pre>



<h4 class="wp-block-heading">3. 网络请求 (Networking)</h4>



<ul class="wp-block-list">
<li><strong>Retrofit:</strong> 业界标准的 HTTP 客户端。</li>



<li><strong>OkHttp:</strong> Retrofit 的底层支持，用于处理拦截器（Logging, Header）。</li>



<li><strong>Moshi (或 Gson):</strong> 用于 JSON 解析。Moshi 对 Kotlin 支持更好。</li>
</ul>



<pre class="wp-block-code"><code>// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:2.9.0" // 或 converter-gson
// OkHttp Logging (调试接口用)
implementation "com.squareup.okhttp3:logging-interceptor:4.11.0"</code></pre>



<h4 class="wp-block-heading">4. 异步处理 (Asynchronous)</h4>



<ul class="wp-block-list">
<li><strong>Coroutines (协程):</strong> Kotlin 的杀手级特性，用于替代线程池和 RxJava。</li>
</ul>



<pre class="wp-block-code"><code>implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"</code></pre>



<h4 class="wp-block-heading">5. 图片加载 (Image Loading)</h4>



<ul class="wp-block-list">
<li><strong>Glide:</strong> 传统 View 开发中最稳健的选择，API 简单，缓存机制强大。</li>



<li><strong>备选Fresco:</strong> 实际占用比Glide更小。</li>
</ul>



<pre class="wp-block-code"><code>implementation "com.github.bumptech.glide:glide:4.16.0"
//如果你需要其他支持，可以选择导入。例如gif和其他内容管理</code></pre>



<h4 class="wp-block-heading">6. 其他实用工具</h4>



<ul class="wp-block-list">
<li><strong>Timber:</strong> 更好的 Log 打印工具（自动处理 Tag，Release 版自动静音）。</li>



<li><strong>LeakCanary:</strong> (仅 Debug) 内存泄漏检测神器。</li>
</ul>



<pre class="wp-block-code"><code>implementation "com.jakewharton.timber:timber:5.0.1"
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.12"</code></pre>



<ul class="wp-block-list">
<li><strong>AndroidUtilCode:</strong> 国内Android开发者几乎都在用的工具集合库</li>
</ul>



<pre class="wp-block-code"><code>// if u use AndroidX, use the following
implementation 'com.blankj:utilcodex:1.31.1'

// Not in maintenance
implementation 'com.blankj:utilcode:1.30.7'

//文档页面
https:&#47;&#47;github.com/Blankj/AndroidUtilCode/blob/master/lib/utilcode/README-CN.md</code></pre>



<h2 class="wp-block-heading">总结</h2>



<p class="wp-block-paragraph">XML (Layouts) + ViewBinding + MVVM + Coroutines + Hilt + Retrofit + Jetpack Navigation</p>



<p class="wp-block-paragraph">一个基础框架的要求很简单：解耦，安全，简洁，可用程度高，上手成本低。如果你有兴趣，可以关注这个文章，之后我会上传打包好的内容！</p>
<p><a href="https://www.log.show/log/android-refactoring-an-project/">Android重构项目的第一步</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/android-refactoring-an-project/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>APP上架需要的内容</title>
		<link>https://www.log.show/log/app-publish-stuff/</link>
					<comments>https://www.log.show/log/app-publish-stuff/#respond</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 14:34:22 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[APP]]></category>
		<category><![CDATA[app store]]></category>
		<category><![CDATA[ICP备案]]></category>
		<category><![CDATA[上架]]></category>
		<category><![CDATA[合规]]></category>
		<category><![CDATA[备案]]></category>
		<category><![CDATA[安全评估报告]]></category>
		<category><![CDATA[应用商店]]></category>
		<category><![CDATA[文网文]]></category>
		<category><![CDATA[电信增值业务许可]]></category>
		<category><![CDATA[电子软著]]></category>
		<category><![CDATA[网络文化经营许可证]]></category>
		<category><![CDATA[软著]]></category>
		<guid isPermaLink="false">https://log.show/?p=426</guid>

					<description><![CDATA[<p>本文只给自己做记录学习用。本网站不提供任何第三方代办服务和服务推荐。 以下内容几乎不区分个人账户和公司账户。这 [&#8230;]</p>
<p><a href="https://www.log.show/log/app-publish-stuff/">APP上架需要的内容</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">本文只给自己做记录学习用。本网站不提供任何第三方代办服务和服务推荐。</p>
</blockquote>



<p class="wp-block-paragraph">以下内容几乎不区分个人账户和公司账户。这两个的区别在：</p>



<ol class="wp-block-list">
<li>一个是需要个人认证，一般是身份证，手持身份证，支付宝扫脸这种方式。另一个则是上传扫描的纸质营业执照。</li>



<li>企业账户个别平台会要求填写联系人信息，或者法人实名信息。</li>
</ol>



<h2 class="wp-block-heading">一，一个内容符合规定的APP</h2>



<p class="wp-block-paragraph">首先你要确定你的APP不包括以下内容，假设包括，那么需要很多东西，所以你的APP（至少是上架版本）尽量不可以包含下列信息：</p>



<ol class="wp-block-list">
<li>通讯录功能</li>



<li>直播</li>



<li>聊天</li>



<li>游戏</li>



<li>充值/提现</li>



<li>购物</li>



<li>虚拟资产</li>



<li>与其他软件高低相似</li>
</ol>



<h2 class="wp-block-heading">二，一个你需要上架的平台的开发者账号</h2>



<h2 class="wp-block-heading">三，APP软著证书（Android平台需要，iOS暂时不需要）</h2>



<p class="wp-block-paragraph">如果你有时间，你可以申请软著。周期一般在30-40个工作日。</p>



<p class="wp-block-paragraph">如果你没时间，你可以申请电子软著。下面是代办的周期和价格：</p>



<ul class="wp-block-list">
<li>周期为0-1日，2000-4000元</li>



<li>周期3日，800-1500元</li>



<li>周期10日，500-600元</li>
</ul>



<h2 class="wp-block-heading">四，ICP备案</h2>



<p class="wp-block-paragraph">域名ICP备案周期：10个工作日左右</p>



<p class="wp-block-paragraph">APPICP备案周期：5个工作日左右</p>



<p class="wp-block-paragraph">两种备案均：</p>



<ol class="wp-block-list">
<li>个人备案需要个人信息</li>



<li>公司备案需要公司以及法人信息</li>



<li>其中域名备案之后需要进行公安备案，公安备案需要填写紧急联系人信息</li>



<li>APP备案需要填写实际使用的接口地址，接口地址需要为备案后的域名</li>
</ol>



<h2 class="wp-block-heading">五，电信增值业务许可证</h2>



<p class="wp-block-paragraph">可以访问这里进行文档下载： https://tsm.miit.gov.cn/#/home 写的很清楚。其中需要注意，公司需要有3名员工最近一个月的社保证明（其中至少1人需为计算机专业本科毕业）；申请周期大概为45-60天左右。</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="651" src="https://log.show/wp-content/uploads/2025/12/image-1024x651.png" alt="" class="wp-image-450" srcset="https://www.log.show/wp-content/uploads/2025/12/image-1024x651.png 1024w, https://www.log.show/wp-content/uploads/2025/12/image-300x191.png 300w, https://www.log.show/wp-content/uploads/2025/12/image-768x488.png 768w, https://www.log.show/wp-content/uploads/2025/12/image-1536x977.png 1536w, https://www.log.show/wp-content/uploads/2025/12/image.png 1579w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">六，安全评估报告</h2>



<p class="wp-block-paragraph">所有包含UGC内容的软件都需要。你可以买第三方服务后自己写，大概需要的信息如下：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>项目</td><td>详情</td></tr><tr><td>APP 名称</td><td>[填写 APP 正式名称]</td></tr><tr><td>版本号</td><td>[填写当前上架版本，如 V3.2.1]</td></tr><tr><td>开发者名称</td><td>[公司全称 / 个人开发者姓名]</td></tr><tr><td>开发者资质</td><td>[营业执照编号 / 个人身份证号]</td></tr><tr><td>评估日期</td><td>[YYYY 年 MM 月 DD 日 &#8211; YYYY 年 MM 月 DD 日]</td></tr><tr><td>评估机构</td><td>[自有安全团队 / 第三方权威机构名称，附资质编号]</td></tr><tr><td>评估范围</td><td>客户端（iOS/Android）、服务端、数据库、API 接口、用户数据处理全流程</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">七，网络文化经营许可证</h2>



<p class="wp-block-paragraph">直播的基本下不来，别想了。可以申请游戏，音乐或者其他类别。初审30日内进行回复，会告知是否符合条件，符合后才会进行审核。</p>



<p class="wp-block-paragraph">似乎需要公司有多个相关专业的技术人员才可以申请。</p>
<p><a href="https://www.log.show/log/app-publish-stuff/">APP上架需要的内容</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/app-publish-stuff/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>在移动设备上使用准确的时间</title>
		<link>https://www.log.show/log/use-ntp-server-time-on-mobile-device/</link>
					<comments>https://www.log.show/log/use-ntp-server-time-on-mobile-device/#comments</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Fri, 28 Nov 2025 02:22:44 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[Android时间同步]]></category>
		<category><![CDATA[iOS时间同步]]></category>
		<category><![CDATA[NTP服务器列表]]></category>
		<category><![CDATA[TrueTime-Android]]></category>
		<category><![CDATA[TrueTime.swift]]></category>
		<category><![CDATA[时间戳获取]]></category>
		<category><![CDATA[移动端NTP]]></category>
		<category><![CDATA[第三方库]]></category>
		<category><![CDATA[腾讯云NTP]]></category>
		<guid isPermaLink="false">https://log.show/?p=361</guid>

					<description><![CDATA[<p>上一篇文章写了在服务器同步NTP服务器时间的方案。如果你有兴趣可以点击下面的链接查看。 下面介绍在移动端同步N [&#8230;]</p>
<p><a href="https://www.log.show/log/use-ntp-server-time-on-mobile-device/">在移动设备上使用准确的时间</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">上一篇文章写了在服务器同步NTP服务器时间的方案。如果你有兴趣可以点击下面的链接查看。</p>



<figure class="wp-block-embed is-type-wp-embed"><div class="wp-block-embed__wrapper">
<blockquote class="wp-embedded-content" data-secret="7iiidhbmqx"><a href="https://log.show/log/sync-time-for-server/">在服务器和PC上校准时间[Chrony]</a></blockquote><iframe loading="lazy" class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  title="《 在服务器和PC上校准时间[Chrony] 》—日志" src="https://log.show/log/sync-time-for-server/embed/#?secret=7qLhs5N2ri#?secret=7iiidhbmqx" data-secret="7iiidhbmqx" width="500" height="282" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
</div></figure>



<p class="wp-block-paragraph">下面介绍在移动端同步NTP时间的方案（非修改系统时间，是直接使用NTP服务器时间）。</p>



<h2 class="wp-block-heading">Android端</h2>



<p class="wp-block-paragraph">本教程使用第三方库 [<a href="https://github.com/instacart/truetime-android">TrueTime-Android</a>]</p>



<h3 class="wp-block-heading">引入最新版本</h3>



<pre class="wp-block-code"><code>implementation 'com.github.instacart:truetime-android:3.5'</code></pre>



<h3 class="wp-block-heading">在一个稳定页面初始化</h3>



<p class="wp-block-paragraph">注意，这里初始化经常会有奇怪的问题，需要你写一个足够的冗余条件！！！下面是我的方案</p>



<pre class="wp-block-code"><code>        try {
            val thread = Thread {
                try {
                    TrueTimeRx.build()
                        .withNtpHost("ntp.aliyun.com")
                        .withRootDelayMax(1000f)
                        .withRootDispersionMax(1000f)
                        .initialize()
                } catch (e: Exception) {
                }
            }
            thread.start()
        } catch (e: Exception) {
        }</code></pre>



<h3 class="wp-block-heading">使用时</h3>



<p class="wp-block-paragraph">正常使用语句</p>



<pre class="wp-block-preformatted">//获取当前时间：<br><br>val time = TrueTimeRx.now()<br><br>//获取当前时间戳<br><br>val timestamp = TrueTimeRx.now().time</pre>



<p class="wp-block-paragraph">我的使用方案</p>



<pre class="wp-block-code"><code>val timestamp = try {
   TrueTimeRx.now().time
}catch (e: Exception){
   System.currentTimeMillis()
}</code></pre>



<h2 class="wp-block-heading">iOS端</h2>



<p class="wp-block-paragraph">本教程使用第三方库 [<a href="https://github.com/instacart/TrueTime.swift">TrueTime.swift</a>]</p>



<p class="wp-block-paragraph">引入插件</p>



<pre class="wp-block-code"><code>github "instacart/TrueTime.swift"
# 或者
pod 'TrueTime'</code></pre>



<h3 class="wp-block-heading">swift</h3>



<pre class="wp-block-code"><code>import TrueTime

// At an opportune time (e.g. app start):
let client = TrueTimeClient.sharedInstance
client.start()

// You can now use this instead of NSDate():
let now = client.referenceTime?.now()

// To block waiting for fetch, use the following:
client.fetchIfNeeded { result in
    switch result {
        case let .success(referenceTime):
            let now = referenceTime.now()
        case let .failure(error):
            print("Error! \(error)")
    }
}</code></pre>



<h3 class="wp-block-heading">Object-C</h3>



<pre class="wp-block-code"><code>@import TrueTime;

// At an opportune time (e.g. app start):
TrueTimeClient *client = &#91;TrueTimeClient sharedInstance];
&#91;client startWithPool:@&#91;@"ntp.aliyun.com"] port:123];

// You can now use this instead of &#91;NSDate date]:
NSDate *now = &#91;&#91;client referenceTime] now];

// To block waiting for fetch, use the following:
&#91;client fetchIfNeededWithSuccess:^(NTPReferenceTime *referenceTime) {
    NSLog(@"True time: %@", &#91;referenceTime now]);
} failure:^(NSError *error) {
    NSLog(@"Error! %@", error);
}];</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">关于NTP服务器，下面是我总结的一些NTP服务器，推荐程度从上到下</p>



<pre class="wp-block-code"><code># 腾讯云
ntp.tencent.com
ntp1.tencent.com
ntp2.tencent.com
ntp3.tencent.com
ntp4.tencent.com
ntp5.tencent.com

# 阿里云
ntp.aliyun.com
ntp1.aliyun.com
ntp2.aliyun.com
ntp3.aliyun.com
ntp4.aliyun.com
ntp5.aliyun.com
ntp6.aliyun.com
ntp7.aliyun.com

# 以下为不推荐国内使用的方案

# 中科院，大部分情况正常，但是偶尔会有延迟，需要自己做冗余方案！！
ntp.ntsc.ac.cn

# 来自 https://www.ntppool.org/ 的服务地址，这是绝大多数LINUX的默认NTP服务器
0.pool.ntp.org
1.pool.ntp.org
2.pool.ntp.org
3.pool.ntp.org

# 苹果和微软
time.apple.com
time.asia.apple.com
time.windows.com
time.nist.gov

# 苹果的其他服务地址
time1.apple.com
time2.apple.com
time3.apple.com
time4.apple.com
time5.apple.com
time6.apple.com
time7.apple.com

# Google
time1.google.com
time2.google.com
time3.google.com
time4.google.com</code></pre>
<p><a href="https://www.log.show/log/use-ntp-server-time-on-mobile-device/">在移动设备上使用准确的时间</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/use-ntp-server-time-on-mobile-device/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>在服务器和PC上校准时间[Chrony]</title>
		<link>https://www.log.show/log/sync-time-for-server/</link>
					<comments>https://www.log.show/log/sync-time-for-server/#comments</comments>
		
		<dc:creator><![CDATA[LOGGER]]></dc:creator>
		<pubDate>Thu, 27 Nov 2025 10:03:34 +0000</pubDate>
				<category><![CDATA[LOG]]></category>
		<category><![CDATA[CentOS]]></category>
		<category><![CDATA[chronyc命令]]></category>
		<category><![CDATA[chrony配置]]></category>
		<category><![CDATA[Linux时间同步]]></category>
		<category><![CDATA[NTP时间同步]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[云服务器配置]]></category>
		<category><![CDATA[服务器时间校准]]></category>
		<category><![CDATA[腾讯云NTP]]></category>
		<category><![CDATA[阿里云NTP]]></category>
		<guid isPermaLink="false">https://log.show/?p=355</guid>

					<description><![CDATA[<p>NTP地址 阿里云内网 阿里云外网 腾讯云内网 腾讯云外网 linux使用方案 以下默认是centos和ubu [&#8230;]</p>
<p><a href="https://www.log.show/log/sync-time-for-server/">在服务器和PC上校准时间[Chrony]</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading">NTP地址</h1>



<h2 class="wp-block-heading">阿里云内网</h2>



<pre class="wp-block-code"><code>ntp.cloud.aliyuncs.com
ntp7.cloud.aliyuncs.com
ntp8.cloud.aliyuncs.com
ntp9.cloud.aliyuncs.com
ntp10.cloud.aliyuncs.com
ntp11.cloud.aliyuncs.com
ntp12.cloud.aliyuncs.com</code></pre>



<h2 class="wp-block-heading">阿里云外网</h2>



<pre class="wp-block-code"><code>ntp.aliyun.com
ntp1.aliyun.com
ntp2.aliyun.com
ntp3.aliyun.com
ntp4.aliyun.com
ntp5.aliyun.com
ntp6.aliyun.com
ntp7.aliyun.com</code></pre>



<h2 class="wp-block-heading">腾讯云内网</h2>



<pre class="wp-block-code"><code>time1.tencentyun.com
time2.tencentyun.com
time3.tencentyun.com
time4.tencentyun.com
time5.tencentyun.com</code></pre>



<h2 class="wp-block-heading">腾讯云外网</h2>



<pre class="wp-block-code"><code>ntp.tencent.com
ntp1.tencent.com
ntp2.tencent.com
ntp3.tencent.com
ntp4.tencent.com
ntp5.tencent.com</code></pre>



<h1 class="wp-block-heading">linux使用方案</h1>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">以下默认是centos和ubuntu，默认是先centOS，后ubuntu</p>
</blockquote>



<h3 class="wp-block-heading">安装chronyc</h3>



<pre class="wp-block-code"><code>sudo yum install chrony
##
sudo apt install chrony</code></pre>



<h3 class="wp-block-heading">修改配置文件</h3>



<p class="wp-block-paragraph">文件位于 <code>/etc/chrony.conf</code> 或者 <code>/etc/chrony/chrony.conf</code>，下面是配置文件示例（腾讯云内网方案）：</p>



<pre class="wp-block-code"><code># drift 文件用于记录之前时钟频率调整（单位是ppm）
driftfile /var/lib/chrony/drift

# chronyd 开启后的前三次时间调整，且时间差超过1.0秒才跳变调整时间
makestep 1.0 3

# 允许内核定期将系统时间同步到 RTC 时间
rtcsync

# TAI-UTC offset 和闰秒的信息
leapsectz right/UTC

# 日志输出到/var/log/chrony
logdir /var/log/chrony
# 打印每个 ntp 包的 rawmeasurements，一般在调试或问题排查时使用
log rawmeasurements
# 打印 tracking 日志，用来记录系统时钟的整体状态
log tracking

# （可选）监听 NTP 端口（默认是 udp 123），作为 NTP server 向其他机器提供 NTP 服务
# allow all

# 配置 NTP 服务器
server time1.tencentyun.com iburst
server time2.tencentyun.com iburst
server time3.tencentyun.com iburst
server time4.tencentyun.com iburst
server time5.tencentyun.com iburst</code></pre>



<p class="wp-block-paragraph">或者</p>



<pre class="wp-block-code"><code># 配置信息，包含了 NTP 服务器地址，最小轮询间隔，最大轮询间隔等信息。
server ntp.cloud.aliyuncs.com minpoll 4 maxpoll 10 iburst
server ntp10.cloud.aliyuncs.com minpoll 4 maxpoll 10 iburst
server ntp11.cloud.aliyuncs.com minpoll 4 maxpoll 10 iburst
server ntp12.cloud.aliyuncs.com minpoll 4 maxpoll 10 iburst
server ntp7.cloud.aliyuncs.com minpoll 4 maxpoll 10 iburst
server ntp8.cloud.aliyuncs.com minpoll 4 maxpoll 10 iburst
server ntp9.cloud.aliyuncs.com minpoll 4 maxpoll 10 iburst</code></pre>



<h3 class="wp-block-heading">启动 chrony</h3>



<pre class="wp-block-code"><code>sudo systemctl restart chronyd  # 重启 chronyd 服务
sudo systemctl enable chronyd   # 开机自动启动 chronyd

###

sudo systemctl restart chrony  # 重启 chrony 服务
sudo systemctl enable chrony   # 开机自动启动 chrony</code></pre>



<h3 class="wp-block-heading">检查 chrony 状态</h3>



<pre class="wp-block-code"><code>systemctl status chronyd     # 查看 chronyd 服务状态，Active: active (running)表示正在运行
systemctl is-enabled chronyd # 查看 chronyd 是否开机自动启动，enabled 表示开机自动启动

### 

systemctl status chrony     # 查看 chrony 服务状态，Active: active (running)表示正在运行
systemctl is-enabled chrony # 查看 chrony 是否开机自动启动，enabled 表示开机自动启动</code></pre>



<h2 class="wp-block-heading">其他命令</h2>



<pre class="wp-block-code"><code>chronyd -Q # 向 NTP 服务器查询当前时间差，不修改系统时间

chronyc sources -v # 查看时钟源的状态，有时钟源被星号*标记表示已同步
chronyc tracking   # 查看系统时钟状态</code></pre>



<h2 class="wp-block-heading">关于chronyc的其他内容</h2>



<p class="wp-block-paragraph"><strong>chronyc sources -v 指标简要说明</strong></p>



<p class="wp-block-paragraph"><code>M</code>：对端类型，<code>^</code>表示 server。</p>



<p class="wp-block-paragraph"><code>S</code>：<code>*</code>表示最优时钟源，<code>+</code>表示按统计方式（加权平均）合入系统时钟的调整，<code>-</code>表示未合入。</p>



<p class="wp-block-paragraph"><code>Name/IP Address</code>：时钟源的域名/IP。</p>



<p class="wp-block-paragraph"><code>Stratum</code>：表示时钟源的层级，正常取值1到15，16表示有异常。</p>



<p class="wp-block-paragraph"><code>Poll</code>：log2轮询间隔，poll=4表示轮询间隔是24秒，即16秒。</p>



<p class="wp-block-paragraph"><code>Reach</code>：表示近8个数据包的到达情况，八进制377表示近8个 ntp 包都没有丢。</p>



<p class="wp-block-paragraph"><code>LastRx</code>：距离上一次收到通过校验的 ntp 包过了多久。单位一般为秒，具体以界面显示为准。</p>



<p class="wp-block-paragraph"><code>Last sample</code>：用来衡量当前机器与时钟源的时间差，这里几个时间差是统计计算的中间结果。</p>



<p class="wp-block-paragraph"><strong>chronyc tracking 指标简要说明</strong></p>



<p class="wp-block-paragraph"><code>Reference ID</code>：最优时钟源域名/IP。</p>



<p class="wp-block-paragraph"><code>Stratum</code>：当前机器的层级。</p>



<p class="wp-block-paragraph"><code>Ref time</code>：上次从时钟源计算指标的时间。</p>



<p class="wp-block-paragraph"><code>System time</code>：非跳变调整的时间差。</p>



<p class="wp-block-paragraph"><code>Last offset</code>：上一次时间差，正数表示本地时钟比服务器时钟快。</p>



<p class="wp-block-paragraph"><code>RMS offset</code>：时间差的长期统计均值。</p>



<p class="wp-block-paragraph"><code>Frequency</code>：表示如果 chrony 不调整时钟，系统时钟频率会差多少。</p>



<p class="wp-block-paragraph"><code>Residual freq</code>：当前时钟频率与最优时钟源频率的差距。</p>



<p class="wp-block-paragraph"><code>Skew</code>：频率误差界。</p>



<p class="wp-block-paragraph"><code>Root delay</code>：到 stratum-1时钟源的 RTT（roud-time trip）。</p>



<p class="wp-block-paragraph"><code>Root dispersion</code>：到 stratum-1时钟源的固有误差。</p>



<p class="wp-block-paragraph"><code>Update interval</code>：两次时钟修正的间隔。</p>



<p class="wp-block-paragraph"><code>Leap status</code>：闰秒状态。</p>



<h1 class="wp-block-heading">至于Windows</h1>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="795" src="https://log.show/wp-content/uploads/2025/11/image-1024x795.png" alt="" class="wp-image-356" srcset="https://www.log.show/wp-content/uploads/2025/11/image-1024x795.png 1024w, https://www.log.show/wp-content/uploads/2025/11/image-300x233.png 300w, https://www.log.show/wp-content/uploads/2025/11/image-768x596.png 768w, https://www.log.show/wp-content/uploads/2025/11/image-1130x878.png 1130w, https://www.log.show/wp-content/uploads/2025/11/image-760x590.png 760w, https://www.log.show/wp-content/uploads/2025/11/image.png 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
<p><a href="https://www.log.show/log/sync-time-for-server/">在服务器和PC上校准时间[Chrony]</a>最先出现在<a href="https://www.log.show">日志</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.log.show/log/sync-time-for-server/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
	</channel>
</rss>
