Przeglądaj źródła

feat: 日志统计工具、多余日志清理 (#46)

* feat: 日志tag统计分析工具

* feat: 日志tag统计分析工具

* feat: 日志清理
WilliumP 5 miesięcy temu
rodzic
commit
bd3afa7be2
17 zmienionych plików z 1355 dodań i 95 usunięć
  1. 27 30
      app/src/main/java/com/adealink/weparty/apm/HookTest.kt
  2. 13 13
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/ApplicationModeWindowManager.kt
  3. 2 2
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/fix/Android14ModeWindowManager.kt
  4. 2 3
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/view/FloatLinearLayout.kt
  5. 5 5
      app/src/main/java/com/adealink/weparty/module/level/label/base/BaseImageLabelView.kt
  6. 2 2
      app/src/main/java/com/adealink/weparty/module/message/dot/MessageDot.kt
  7. 1 1
      module/account/src/main/java/com/adealink/weparty/account/login/data/LoginData.kt
  8. 2 13
      module/game/src/main/java/com/adealink/weparty/game/rocket/manager/RocketManager.kt
  9. 2 3
      module/message/src/main/java/com/adealink/weparty/message/conversationlist/ConversationListFragment.kt
  10. 1 1
      module/message/src/main/java/com/adealink/weparty/message/conversationlist/viewmodel/ConversationListViewModel.kt
  11. 7 11
      module/operation/src/main/java/com/adealink/weparty/operation/signinreward/viewmodel/SignInViewModel.kt
  12. 4 8
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPJoinController.kt
  13. 2 3
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPMemberController.kt
  14. 282 0
      tool/QUICK_START.md
  15. 40 0
      tool/README.md
  16. 408 0
      tool/analyze_log_tags.py
  17. 555 0
      tool/generate_log_report_html.py

+ 27 - 30
app/src/main/java/com/adealink/weparty/apm/HookTest.kt

@@ -1,8 +1,5 @@
 package com.adealink.weparty.apm
 
-import android.view.View
-import androidx.viewpager2.widget.ViewPager2
-import com.adealink.frame.log.Log
 import com.flyjingfish.android_aop_annotation.ProceedJoinPoint
 import com.flyjingfish.android_aop_annotation.anno.AndroidAopMatchClassMethod
 import com.flyjingfish.android_aop_annotation.base.MatchClassMethod
@@ -42,30 +39,30 @@ class ScrollEventAdapterMethod : MatchClassMethod {
 }
 
 
-@AndroidAopMatchClassMethod(
-    targetClassName = "androidx.viewpager2.widget.ViewPager2",
-    methodName = ["setCurrentItemInternal"],
-    type = MatchType.SELF
-)
-class SetCurrentItemInternalMethod : MatchClassMethod {
-    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
-        try {
-            val target = joinPoint.target
-            if (target != null) {
-                val mViewPager = target as ViewPager2
-                val viewPagerIdString = mViewPager.resources?.getResourceEntryName(mViewPager.id)
-                val args = joinPoint.args
-                val item = args?.getOrNull(0) as? Int
-                val smoothScroll = args?.getOrNull(1) as? Boolean
-                Log.i(
-                    "SetCurrentItemInternal",
-                    "=====setCurrentItemInternal=====${mViewPager}, viewPager:$viewPagerIdString, currentItem:${mViewPager.currentItem}, " +
-                            "${mViewPager.adapter?.javaClass?.name}, setItem:$item, smoothScroll:$smoothScroll"
-                )
-            }
-        } catch (e: Exception) {
-
-        }
-        return joinPoint.proceed()
-    }
-}
+//@AndroidAopMatchClassMethod(
+//    targetClassName = "androidx.viewpager2.widget.ViewPager2",
+//    methodName = ["setCurrentItemInternal"],
+//    type = MatchType.SELF
+//)
+//class SetCurrentItemInternalMethod : MatchClassMethod {
+//    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+//        try {
+//            val target = joinPoint.target
+//            if (target != null) {
+//                val mViewPager = target as ViewPager2
+//                val viewPagerIdString = mViewPager.resources?.getResourceEntryName(mViewPager.id)
+//                val args = joinPoint.args
+//                val item = args?.getOrNull(0) as? Int
+//                val smoothScroll = args?.getOrNull(1) as? Boolean
+//                Log.d(
+//                    "SetCurrentItemInternal",
+//                    "=====setCurrentItemInternal=====${mViewPager}, viewPager:$viewPagerIdString, currentItem:${mViewPager.currentItem}, " +
+//                            "${mViewPager.adapter?.javaClass?.name}, setItem:$item, smoothScroll:$smoothScroll"
+//                )
+//            }
+//        } catch (e: Exception) {
+//
+//        }
+//        return joinPoint.proceed()
+//    }
+//}

+ 13 - 13
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/ApplicationModeWindowManager.kt

@@ -105,7 +105,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
                 return false
             }
 
-            Log.i(TAG_FLOAT_VIEW, "${SUB_TAG}, addViewImmediate, viewAddMap.add($view)")
+            Log.d(TAG_FLOAT_VIEW, "${SUB_TAG}, addViewImmediate, viewAddMap.add($view)")
             return addViewInner(wm, view).also { addView ->
                 if (addView) {
                     viewAddMap.add(view)
@@ -124,19 +124,19 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
             if (view is BaseLayoutFloatView) {
                 when (val gravity = view.baseFloatData.gravity()) {
                     GRAVITY_TOP -> {
-                        Log.i(
+                        Log.d(
                             TAG_FLOAT_VIEW,
                             "${SUB_TAG}, addViewInner, topViewContainer add view: $view, currentWindowManager: ${getCurrentWindowManager()}"
                         )
 
                         if (topViewContainer.windowLayoutParams.token == null) {
-                            Log.i(
+                            Log.d(
                                 TAG_FLOAT_VIEW,
                                 "${SUB_TAG}, inflateViewContainer, currentWindowManager: $windowManager"
                             )
                             windowManager.addView(topViewContainer.view(), topViewContainer.windowLayoutParams)
                         } else {
-                            Log.i(
+                            Log.d(
                                 TAG_FLOAT_VIEW,
                                 "${SUB_TAG}, already inflateViewContainer, view: $topViewContainer"
                             )
@@ -156,7 +156,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
 
             } else if (view is BaseWindowFloatView) {
                 if (view.windowParams.token == null) {
-                    Log.i(
+                    Log.d(
                         TAG_FLOAT_VIEW,
                         "${SUB_TAG}, addViewInner, windowManager add view: $view, currentWindowManager: ${getCurrentWindowManager()}"
                     )
@@ -165,7 +165,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
                     windowManager.addView(view, view.windowParams)
                     return true
                 } else {
-                    Log.i(
+                    Log.d(
                         TAG_FLOAT_VIEW,
                         "${SUB_TAG}, addViewInner, windowManager already add view: $view"
                     )
@@ -198,7 +198,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
         if (view is BaseLayoutFloatView) {
             when (val gravity = view.baseFloatData.gravity()) {
                 GRAVITY_TOP -> {
-                    Log.i(
+                    Log.d(
                         TAG_FLOAT_VIEW,
                         "${SUB_TAG}, removeViewInner, topContainer removeView $view"
                     )
@@ -217,7 +217,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
             if (view.windowParams.token != null) {
                 try {
                     windowManager.removeViewImmediate(view)
-                    Log.i(
+                    Log.d(
                         TAG_FLOAT_VIEW,
                         "$SUB_TAG, removeViewInner, windowManager remove $view"
                     )
@@ -233,12 +233,12 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
         }
 
         if (toAddViewQueue.remove(view)) {
-            Log.i(
+            Log.d(
                 TAG_FLOAT_VIEW,
                 "$SUB_TAG, removeViewInner from toAddViewQueue, reason: $reason, view: $view"
             )
         } else {
-            Log.i(
+            Log.d(
                 TAG_FLOAT_VIEW,
                 "$SUB_TAG, removeViewInner, already removeView, reason: $reason, view: $view"
             )
@@ -301,7 +301,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
                 Log.e(TAG_FLOAT_VIEW, "onPause, windowManager removeView $topViewContainer fail", e)
             }
             topViewContainer.windowLayoutParams.token = null
-            Log.i(
+            Log.d(
                 TAG_FLOAT_VIEW,
                 "$SUB_TAG, onPause, windowManager removeView: $topViewContainer, activity.windowManager: ${activity.windowManager}"
             )
@@ -353,7 +353,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
     }
 
     private fun showNextView(lastView: BaseFloatView<out IFloatData>?) {
-        Log.i(TAG_FLOAT_VIEW, "${SUB_TAG}, showNextView, lastView:$lastView")
+        Log.d(TAG_FLOAT_VIEW, "${SUB_TAG}, showNextView, lastView:$lastView")
         runOnUiThread {
             val newAddViewQueue = LinkedList(toAddViewQueue)
             val toAddViewList = mutableListOf<BaseFloatView<out IFloatData>>()
@@ -363,7 +363,7 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
                 }
             }
             toAddViewQueue.clear()
-            toAddViewQueue.addAll( toAddViewList)
+            toAddViewQueue.addAll(toAddViewList)
         }
     }
 

+ 2 - 2
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/fix/Android14ModeWindowManager.kt

@@ -28,7 +28,7 @@ class Android14ModeWindowManager : ApplicationModeWindowManager() {
 
     override fun onResume(activity: Activity) {
         activity.contentView()?.safeAction {
-            Log.i(
+            Log.d(
                 TAG_FLOAT_VIEW,
                 "${SUB_TAG}, onResume, addView: currentWindowManager: ${activity.windowManager}"
             )
@@ -38,7 +38,7 @@ class Android14ModeWindowManager : ApplicationModeWindowManager() {
 
     override fun onPause(activity: Activity) {
         activity.contentView()?.safeAction {
-            Log.i(
+            Log.d(
                 TAG_FLOAT_VIEW,
                 "${SUB_TAG}, onPause, removeView: currentWindowManager: ${activity.windowManager}"
             )

+ 2 - 3
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/view/FloatLinearLayout.kt

@@ -9,7 +9,6 @@ import android.util.SparseIntArray
 import android.view.View
 import androidx.annotation.IdRes
 import androidx.appcompat.widget.LinearLayoutCompat
-import androidx.camera.core.ViewPort
 import androidx.core.util.isEmpty
 import androidx.core.view.children
 import androidx.core.view.contains
@@ -148,7 +147,7 @@ class FloatLinearLayout @JvmOverloads constructor(
     }
 
     override fun removeFloatView(view: BaseLayoutFloatView) {
-        Log.i(TAG_FLOAT_LAYOUT_VIEW, "removeFloatView, $view")
+        Log.d(TAG_FLOAT_LAYOUT_VIEW, "removeFloatView, $view")
         removeView(view)
     }
 
@@ -158,7 +157,7 @@ class FloatLinearLayout @JvmOverloads constructor(
 
     override fun addFloatView(view: BaseLayoutFloatView) {
         val index = getViewIndex(view.id)
-        Log.i(TAG_FLOAT_LAYOUT_VIEW, "addFloatView(index:$index) $view")
+        Log.d(TAG_FLOAT_LAYOUT_VIEW, "addFloatView(index:$index) $view")
         //兼容切语言后依赖appContext的layoutDirection不更新问题
         view.layoutDirection = LAYOUT_DIRECTION_LOCALE
         addView(view, index, view.layoutParams)

+ 5 - 5
app/src/main/java/com/adealink/weparty/module/level/label/base/BaseImageLabelView.kt

@@ -3,7 +3,10 @@ package com.adealink.weparty.module.level.label.base
 import android.content.Context
 import android.graphics.BitmapFactory
 import android.util.AttributeSet
+import androidx.core.net.toUri
+import androidx.core.view.updateLayoutParams
 import com.adealink.frame.image.view.NetworkImageView
+import com.adealink.frame.log.Log
 import com.facebook.common.executors.CallerThreadExecutor
 import com.facebook.common.memory.PooledByteBuffer
 import com.facebook.common.references.CloseableReference
@@ -11,11 +14,8 @@ import com.facebook.datasource.BaseDataSubscriber
 import com.facebook.datasource.DataSource
 import com.facebook.drawee.backends.pipeline.Fresco
 import com.facebook.drawee.drawable.ScalingUtils
-import com.facebook.imagepipeline.request.ImageRequestBuilder
-import androidx.core.net.toUri
-import androidx.core.view.updateLayoutParams
-import com.adealink.frame.log.Log
 import com.facebook.imagepipeline.image.EncodedImage
+import com.facebook.imagepipeline.request.ImageRequestBuilder
 
 /**
  * Created by LfJ on 2025/8/6.
@@ -118,7 +118,7 @@ open class BaseImageLabelView @JvmOverloads constructor(
 
     private fun updateViewWidthIfNeed() {
         post {
-            Log.i(
+            Log.d(
                 TAG,
                 "${this.javaClass.simpleName} updateViewWidth viewHeight:$height mWHRatio:$mWHRatio"
             )

+ 2 - 2
app/src/main/java/com/adealink/weparty/module/message/dot/MessageDot.kt

@@ -82,12 +82,12 @@ class FamilyMessageDot(
     }
 
     override fun onCountChanged(count: Int) {
-        Log.i(TAG, "onCountChanged count:$count withMuteNotification:$withMuteNotification")
+        Log.d(TAG, "onCountChanged count:$count withMuteNotification:$withMuteNotification")
         if (withMuteNotification) {
             launch(Dispatcher.WENEXT_THREAD_POOL) {
                 MessageModule.getUserFamilySettingConfig(true).onSuccess {
                     val nonMute = it?.mute != MuteFamilyConversationNotification.MUTE.value
-                    Log.i(TAG, "onCountChanged nonMute:$nonMute")
+                    Log.d(TAG, "onCountChanged nonMute:$nonMute")
                     if (nonMute) {
                         show(NumDot(count))
                     }

+ 1 - 1
module/account/src/main/java/com/adealink/weparty/account/login/data/LoginData.kt

@@ -59,7 +59,7 @@ data class LoginResult(
     @SerializedName("seqid") val seqId: Long = 0,
     @SerializedName("token") val token: String,
     @SerializedName("refreshToken") val refreshToken: String,
-    @SerializedName("userInfo") val userInfo: com.adealink.weparty.module.profile.data.UserInfo,
+    @SerializedName("userInfo") val userInfo: UserInfo,
     @SerializedName("reason") val reason: String,
     @SerializedName("expire") val expire: Long,
 ) : Parcelable

+ 2 - 13
module/game/src/main/java/com/adealink/weparty/game/rocket/manager/RocketManager.kt

@@ -43,7 +43,6 @@ import com.adealink.weparty.module.gift.GiftModule
 import com.adealink.weparty.module.room.RoomModule
 import com.adealink.weparty.module.room.data.RoomLimitTimeGiftData
 import com.adealink.weparty.module.room.listener.IRoomListener
-import com.adealink.weparty.storage.file.FilePath
 import kotlinx.coroutines.launch
 
 /**
@@ -68,12 +67,10 @@ class RocketManager : BaseFrame<IRocketListener>(), IRocketManager, IRoomListene
 
         override fun needHandle(data: AddRoomRocketCoinsNotify?): Boolean {
             val needHandle = data != null
-            Log.i(TAG_ROCKET, "URI_ROOM_ROCKET_ADD_COINS needHandle: $needHandle")
             return needHandle
         }
 
         override fun onNotify(data: AddRoomRocketCoinsNotify) {
-            Log.i(TAG_ROCKET, "URI_ROOM_ROCKET_ADD_COINS onNotify: $data")
             handleAddRoomCoinsNotify(data)
         }
 
@@ -88,15 +85,10 @@ class RocketManager : BaseFrame<IRocketListener>(), IRocketManager, IRoomListene
 
         override fun needHandle(data: RocketUpgradeNotify?): Boolean {
             val needHandle = data != null
-            Log.i(TAG_ROCKET, "URI_ROOM_ROCKET_UPGRADE needHandle: $needHandle")
             return needHandle
         }
 
         override fun onNotify(data: RocketUpgradeNotify) {
-            Log.i(
-                TAG_ROCKET,
-                "URI_ROOM_ROCKET_UPGRADE onNotify roomId: ${data.roomId}, level: ${data.level}"
-            )
             handleRocketUpgradeNotify(data)
         }
     }
@@ -110,12 +102,10 @@ class RocketManager : BaseFrame<IRocketListener>(), IRocketManager, IRoomListene
 
         override fun needHandle(data: RocketRewardIssueNotify?): Boolean {
             val needHandle = data != null && RoomModule.getJoinedRoomId() == data.roomId
-            Log.i(TAG_ROCKET, "URI_ROOM_ROCKET_REWARD needHandle: $needHandle")
             return needHandle
         }
 
         override fun onNotify(data: RocketRewardIssueNotify) {
-            Log.i(TAG_ROCKET, "URI_ROOM_ROCKET_REWARD onNotify: $data")
             handleRocketRewardNotify(data)
         }
 
@@ -234,7 +224,7 @@ class RocketManager : BaseFrame<IRocketListener>(), IRocketManager, IRoomListene
                         RocketLevel.mapOrNull(it)
                     }?.let { maxLevel ->
                         RocketLevel.maxRocketLevel = maxLevel
-                        Log.i(TAG_ROCKET, "getRocketInfo set rocket max level: $maxLevel")
+                        Log.d(TAG_ROCKET, "getRocketInfo set rocket max level: $maxLevel")
                     }
                     notifyResetLevel(rocketInfo.currRoundNum, RocketLevel.map(rocketInfo.currLevel))
                     Rlt.Success(rocketInfo)
@@ -400,13 +390,12 @@ class RocketManager : BaseFrame<IRocketListener>(), IRocketManager, IRoomListene
                 )
             )?.apply {
                 onSelfRemovedCallback = {
-                    Log.i(TAG_ROCKET, "RocketHeadlineFloatView, onSelfRemovedCallback")
+                    Log.d(TAG_ROCKET, "RocketHeadlineFloatView, onSelfRemovedCallback")
                     isRocketHeadlineShowing = false
                     showNextRocketHeadline()
                 }
             }
             if (floatView != null) {
-                Log.i(TAG_ROCKET, "add RocketHeadlineFloatView")
                 WindowManagerProxy.getWindowManager().addView(floatView)
             }
         }

+ 2 - 3
module/message/src/main/java/com/adealink/weparty/message/conversationlist/ConversationListFragment.kt

@@ -163,7 +163,7 @@ class ConversationListFragment : BaseFragment(R.layout.fragment_conversationlist
         
         // 只观察当前tab对应的LiveData
         getCurrentLiveData().observe(viewLifecycleOwner) { uiConversations ->
-            Log.i(
+            Log.d(
                 TAG_CONV_LIST_FRG,
                 "conversation list onChanged, tabType: $currentTabType, size: ${uiConversations.size}, recyclerviewStatus: $newState"
             )
@@ -246,7 +246,7 @@ class ConversationListFragment : BaseFragment(R.layout.fragment_conversationlist
     private fun setDataCollection(uiConversations: List<BaseUiConversation>) {
         if (!isViewBindingValid()) return
         
-        Log.i(TAG_CONV_LIST_FRG, "setDataCollection dataList.size:${uiConversations.size}")
+        Log.d(TAG_CONV_LIST_FRG, "setDataCollection dataList.size:${uiConversations.size}")
         adapter.setDataCollection(uiConversations)
         if (uiConversations.isEmpty()) {
             if (!emptyViewAdded) {
@@ -255,7 +255,6 @@ class ConversationListFragment : BaseFragment(R.layout.fragment_conversationlist
                 adapter.notifyDataSetChanged()
             }
         }
-        Log.i(TAG_CONV_LIST_FRG, "setDataCollection notifyDataSetChanged complete")
     }
 
 

+ 1 - 1
module/message/src/main/java/com/adealink/weparty/message/conversationlist/viewmodel/ConversationListViewModel.kt

@@ -1197,7 +1197,7 @@ open class ConversationListViewModel : BaseViewModel(), IIMUserInfoListener, Cor
                 return@launch
             }
 
-            Log.i(
+            Log.d(
                 TAG_IM_CONV_LIST_VM,
                 "updateConversationList. tabType: $tabType, size: ${conversations.size}"
             )

+ 7 - 11
module/operation/src/main/java/com/adealink/weparty/operation/signinreward/viewmodel/SignInViewModel.kt

@@ -33,17 +33,13 @@ class SignInViewModel : ISignInViewModel, BaseViewModel() {
     override fun getDailySignInRewards(httpCache: Boolean) {
         viewModelScope.launch {
             dailySignReward.send(
-                when (val result =
-                    signInRewardHttpService.getDailySignInRewards(
-                        SignInActivityCode.DAILY_SIGN_IN.code,
-                    )
-                        .apply {
-                            Log.logRltI(
-                                TAG_OPERATION_GET_DAILY_SIGN_IN_REWARD,
-                                "getDailySignInRewards",
-                                this
-                            )
-                        }) {
+                when (val result = signInRewardHttpService.getDailySignInRewards(
+                    SignInActivityCode.DAILY_SIGN_IN.code,
+                ).apply {
+                        Log.i(
+                            TAG_OPERATION_GET_DAILY_SIGN_IN_REWARD, "getDailySignInRewards"
+                        )
+                    }) {
                     is Rlt.Success -> {
                         val data = result.data.data
                         if (data == null) {

+ 4 - 8
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPJoinController.kt

@@ -183,11 +183,10 @@ open class WPJoinController(override val ctx: IRoomContext, serialHandler: Handl
             val socketJoinRoomAsync = async { socketJoinRoom(req) }
             val getRoomInfoAsync = async { ctx.roomService.attrController.getRoomInfo(req.roomId) }
             val joinRoomRes = socketJoinRoomAsync.await()
-            Log.i(TAG_ROOM_FLOW,"joinRoomRes isSuccess:${joinRoomRes.isSuccess}")
             logRoomTime("joinRoom res")
             val getRoomInfoRes = getRoomInfoAsync.await()
-            Log.i(TAG_ROOM_FLOW,"getRoomInfoRes isSuccess:${getRoomInfoRes.isSuccess}")
             logRoomTime("getRoomInfo res")
+            Log.i(TAG_ROOM_FLOW,"joinRoomRes isSuccess:${joinRoomRes.isSuccess},getRoomInfoRes isSuccess:${getRoomInfoRes.isSuccess}")
             if (getRoomInfoRes is Rlt.Failed) {
                 val roomId = getRoomInfoRes.error.data as? Long
                 if (joiningRoomId == roomId) {
@@ -221,7 +220,7 @@ open class WPJoinController(override val ctx: IRoomContext, serialHandler: Handl
             joiningRoomId = null
             joinedRoomInfo = (getRoomInfoRes as Rlt.Success).data
             joinRoomPassword = req.password
-            Log.i(TAG_ROOM_FLOW, "joinRoom: $joinedRoomInfo")
+            Log.i(TAG_ROOM_FLOW, "joinRoom: req:${req},joinedRoomInfo:${joinedRoomInfo}")
             val joinRoomData = (joinRoomRes as Rlt.Success).data
             ctx.mediaService.currRtcType = RtcType.map(joinRoomData.rtcType)
             invisibleJoin = joinRoomData.invisibleJoin
@@ -254,8 +253,6 @@ open class WPJoinController(override val ctx: IRoomContext, serialHandler: Handl
             ctx.roomService.attrController.getNecessaryRoomConfig(req.roomId)
             logRoomTime("joinRoom end")
             return@withContext joinRoomRes
-        }.apply {
-            Log.logRltI(TAG_ROOM_FLOW, "joinRoom, req:${req}", this)
         }
     }
 
@@ -328,7 +325,7 @@ open class WPJoinController(override val ctx: IRoomContext, serialHandler: Handl
             val channel = roomId.toString()
             Log.i(
                 TAG_ROOM_FLOW,
-                "joinChannel, reason:$reason, channelToken:${channelToken}, channel:${channel}, joiningChannel:$joiningChannel"
+                "joinChannel, roomId:${roomId}, reason:$reason, channelToken:${channelToken}, channel:${channel}, joiningChannel:$joiningChannel"
             )
             if (joinedChannel != null && joinedChannel == channel) {
                 callback?.onSuccess(channel)
@@ -387,7 +384,6 @@ open class WPJoinController(override val ctx: IRoomContext, serialHandler: Handl
                 token = (tokenRlt as Rlt.Success).data
             }
 
-            Log.i(TAG_ROOM_FLOW, "joinChannel start, roomId:${roomId}")
             ctx.mediaService.setChannelProfile(ChannelProfile.LIVE_BROADCASTING)
             val joinChannelRlt = ctx.mediaService.joinChannel(
                 channel,
@@ -516,7 +512,7 @@ open class WPJoinController(override val ctx: IRoomContext, serialHandler: Handl
             lastRoomRejoiningTime = 0L
             result
         }.apply {
-            Log.logRltI(TAG_ROOM_FLOW, "rejoinRoom, roomId:${getJoinedRoomId()}", this)
+            Log.i(TAG_ROOM_FLOW, "rejoinRoom, roomId:${getJoinedRoomId()}")
         }
     }
 

+ 2 - 3
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPMemberController.kt

@@ -69,7 +69,6 @@ class WPMemberController(override val ctx: IRoomContext, serialHandler: Handler)
         }
 
         override fun onNotify(data: RoomMemberOnlineInfoNotify) {
-            Log.i(TAG_ROOM_MEMBER, "ROOM_ONLINE_NOTIFY", data.memberOnlineInfo)
             handleMemberOnlineInfo(data.memberOnlineInfo, data.ownerInfo)
         }
 
@@ -108,7 +107,7 @@ class WPMemberController(override val ctx: IRoomContext, serialHandler: Handler)
 
         launch {
             if (memberOnlineInfo.version <= onlineInfoVersion) {
-                Log.i(
+                Log.d(
                     TAG_ROOM_MEMBER,
                     "version return, onlineInfoVersion:$onlineInfoVersion,memberOnlineInfo.version:${memberOnlineInfo.version},"
                 )
@@ -122,7 +121,7 @@ class WPMemberController(override val ctx: IRoomContext, serialHandler: Handler)
             }
             onlineInfoVersion = memberOnlineInfo.version
             onlineMemberCount = memberOnlineInfo.onlineMemberCount
-            Log.i(
+            Log.d(
                 TAG_ROOM_MEMBER,
                 "version return, onlineMemberCount:$onlineMemberCount,isMediaUsing:${ctx.mediaService.isMediaUsing()},"
             )

+ 282 - 0
tool/QUICK_START.md

@@ -0,0 +1,282 @@
+# 日志TAG分析工具 - 快速开始
+
+## ⚡ 5分钟上手
+
+### 1. 准备日志文件
+
+您的日志格式:
+```
+[I][2025-09-22 +3.0 23:57:49.377][12735, 1*][tag_float_view][:0, ][remove, reason: clearWhenExitRoom
+```
+
+### 2. 运行分析(3种方式)
+
+#### 方式A:HTML可视化报告(推荐)⭐
+
+```bash
+cd tool
+python3 generate_log_report_html.py your_log.txt
+```
+
+然后在浏览器中打开生成的 `log_report.html`
+
+#### 方式B:命令行分析
+
+```bash
+python3 analyze_log_tags.py your_log.txt
+```
+
+#### 方式C:导出CSV/JSON
+
+```bash
+python3 analyze_log_tags.py your_log.txt --export report.csv
+python3 analyze_log_tags.py your_log.txt --export-json report.json
+```
+
+### 3. 查看结果
+
+命令行输出示例:
+```
+分析完成!
+总行数: 1000
+匹配的日志行数: 995
+自定义格式日志数: 995  👈 识别您的自定义格式
+不同TAG数量: 45
+
+排名   TAG名称              数量    占比
+1     tag_float_view      250     25.1%  👈 最高频
+2     tag_gift_send       180     18.1%
+3     tag_network         150     15.1%
+...
+```
+
+## 🎯 常用命令
+
+```bash
+# 只看TOP 20
+python3 analyze_log_tags.py log.txt --top 20
+
+# 过滤低频TAG(只看出现≥100次的)
+python3 analyze_log_tags.py log.txt --min-count 100
+
+# 获取优化建议
+python3 analyze_log_tags.py log.txt --suggest-cleanup
+
+# 一键生成完整报告
+python3 analyze_log_tags.py log.txt --export full.csv
+python3 generate_log_report_html.py log.txt
+```
+
+## 📋 支持的日志格式
+
+### ✅ 您的自定义格式
+```
+[级别][日期时间][进程][TAG][其他][消息]
+```
+
+### ✅ Android标准格式
+```
+09-22 23:57:49.377 12735 12735 I TAG: message
+```
+
+### ✅ 简化格式
+```
+I/TAG: message
+```
+
+工具会**自动识别**格式,无需手动指定!
+
+## 📊 输出说明
+
+### 关键指标
+
+| 指标 | 说明 | 建议 |
+|------|------|------|
+| 占比 > 10% | 高频TAG | 检查是否日志过多 |
+| 占比 < 0.01% | 低频TAG | 考虑删除 |
+| TOP 20占比 > 80% | 日志集中 | 重点优化TOP TAG |
+| DEBUG占比 > 70% | 调试日志过多 | Release版本应禁用 |
+
+### 级别说明
+
+- **V** (VERBOSE) - 最详细,开发调试用
+- **D** (DEBUG) - 调试信息,Release应禁用
+- **I** (INFO) - 重要信息,可保留
+- **W** (WARN) - 警告信息,需关注
+- **E** (ERROR) - 错误信息,必须保留
+
+## 🛠️ 实用场景
+
+### 场景1:首次分析
+
+```bash
+# 生成HTML报告查看全貌
+python3 generate_log_report_html.py app_log.txt
+
+# 在浏览器中查看,识别问题TAG
+```
+
+### 场景2:优化高频TAG
+
+```bash
+# 找出占比最高的TAG
+python3 analyze_log_tags.py app_log.txt --top 10
+
+# 根据结果修改代码,减少日志输出
+```
+
+### 场景3:清理低频TAG
+
+```bash
+# 找出可以删除的TAG
+python3 analyze_log_tags.py app_log.txt --suggest-cleanup
+
+# 在代码中搜索并删除这些TAG
+```
+
+### 场景4:对比优化效果
+
+```bash
+# 优化前
+python3 analyze_log_tags.py before.txt --export before.csv
+
+# 优化后
+python3 analyze_log_tags.py after.txt --export after.csv
+
+# 对比两个CSV文件
+```
+
+## 💡 优化建议
+
+### 立即可做的
+
+1. **禁用VERBOSE和DEBUG日志**(Release版本)
+   ```kotlin
+   if (BuildConfig.DEBUG) {
+       Log.d(TAG, "调试信息")
+   }
+   ```
+
+2. **删除低频TAG**(占比 < 0.01%)
+   - 运行 `--suggest-cleanup` 获取清理列表
+   - 在代码中搜索并删除
+
+3. **减少循环中的日志**
+   ```kotlin
+   // ❌ 不好
+   for (item in items) {
+       Log.d(TAG, "处理: $item")  // 每次循环都打印
+   }
+   
+   // ✅ 好
+   Log.d(TAG, "开始处理 ${items.size} 个项目")
+   ```
+
+### 长期优化
+
+1. **统一TAG命名规范**
+   ```kotlin
+   private const val TAG = "TAG_MODULE_FEATURE"
+   ```
+
+2. **使用日志框架**(如Timber)
+   ```kotlin
+   Timber.tag(TAG).d("消息")  // 自动处理Release版本
+   ```
+
+3. **定期分析**(建议每月一次)
+   ```bash
+   # 加入CI/CD流程
+   python3 analyze_log_tags.py latest_log.txt --export monthly_report.csv
+   ```
+
+## 📁 文件说明
+
+```
+tool/
+├── analyze_log_tags.py          ⭐ 主分析脚本
+├── generate_log_report_html.py  ⭐ HTML报告生成器
+├── LOG_ANALYSIS_README.md       📖 详细文档
+├── CUSTOM_FORMAT_GUIDE.md       📖 自定义格式说明
+├── QUICK_START.md               📖 本文件
+├── example_usage.sh             💡 使用示例
+└── test_custom_format.sh        🧪 测试脚本
+```
+
+## ❓ 常见问题
+
+### Q: 我的日志格式不一样怎么办?
+
+A: 工具支持3种格式。如果都不匹配,可以修改 `analyze_log_tags.py` 第36-38行的正则表达式。
+
+### Q: 分析很慢怎么办?
+
+A: 
+- 使用 `--top N` 限制输出
+- 对大文件先截取部分分析:`head -10000 log.txt > sample.txt`
+
+### Q: 想看特定模块的日志怎么办?
+
+A:
+```bash
+grep "gift" app_log.txt > gift_log.txt
+python3 analyze_log_tags.py gift_log.txt
+```
+
+### Q: 如何持续监控日志?
+
+A: 创建定时任务:
+```bash
+#!/bin/bash
+DATE=$(date +%Y%m%d)
+python3 analyze_log_tags.py "log_${DATE}.txt" --export "report_${DATE}.csv"
+```
+
+## 🎓 进阶技巧
+
+### 1. 按时间段分析
+
+```bash
+# 提取特定时间段的日志
+grep "2025-09-22 23:" app_log.txt > night_log.txt
+python3 analyze_log_tags.py night_log.txt
+```
+
+### 2. 按级别过滤
+
+```bash
+# 只分析ERROR日志
+grep "^\[E\]" app_log.txt > error_log.txt
+python3 analyze_log_tags.py error_log.txt
+```
+
+### 3. 多文件批量分析
+
+```bash
+for log in logs/*.txt; do
+    echo "分析: $log"
+    python3 analyze_log_tags.py "$log" --export "reports/$(basename $log .txt).csv"
+done
+```
+
+## 📞 获取帮助
+
+```bash
+# 查看所有选项
+python3 analyze_log_tags.py --help
+
+# 运行测试
+./test_custom_format.sh
+```
+
+## 🎉 开始使用
+
+现在就开始吧!
+
+```bash
+cd tool
+python3 generate_log_report_html.py your_log_file.txt
+```
+
+祝您优化顺利!🚀
+

+ 40 - 0
tool/README.md

@@ -55,3 +55,43 @@ adb logcat --pid=12298 | grep 'tag_http'
 java -Dfile.encoding=utf-8 -jar decode-ui-all.jar
 ```
 
+## 日志分析工具
+
+### generate_log_report_html.py - HTML可视化报告生成
+#### 使用方法
+
+```shell
+# 基本使用(推荐)- 自动生成报告文件名
+python3 generate_log_report_html.py com.partyjoy.yoki_20250922.xlog.log
+# 输出: com.partyjoy.yoki_20250922_report.html
+
+# 指定输出文件名
+python3 generate_log_report_html.py logcat.txt --output my_report.html
+
+# 批量处理多个日志文件
+for log in *.xlog.log; do
+    python3 generate_log_report_html.py "$log"
+done
+```
+
+#### 详细文档
+- [快速开始](QUICK_START.md) - 日志分析快速入门指南
+
+### analyze_log_tags.py - 命令行分析工具
+
+命令行版本的日志TAG分析工具,适合快速查看统计数据。
+
+```shell
+# 基本分析
+python3 analyze_log_tags.py logcat.txt
+
+# 显示TOP 30 TAG
+python3 analyze_log_tags.py logcat.txt --top 30
+
+# 显示超长日志示例
+python3 analyze_log_tags.py logcat.txt --show-long-logs 10
+
+# 导出CSV报告
+python3 analyze_log_tags.py logcat.txt --export report.csv
+```
+

+ 408 - 0
tool/analyze_log_tags.py

@@ -0,0 +1,408 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+日志标签分析工具
+分析Android日志文件中不同tag的使用比例,帮助识别和减少不必要的日志
+
+使用方法:
+    python analyze_log_tags.py logfile.txt
+    python analyze_log_tags.py logfile.txt --top 20
+    python analyze_log_tags.py logfile.txt --min-count 100
+    python analyze_log_tags.py logfile.txt --export report.csv
+"""
+
+import re
+import sys
+import argparse
+from collections import Counter
+from pathlib import Path
+from typing import Dict, List, Tuple
+import json
+
+
+class LogTagAnalyzer:
+    """日志标签分析器"""
+    
+    # Android日志格式匹配:日期 时间 进程号 线程号 级别 TAG: 消息
+    LOG_PATTERN = re.compile(
+        r'^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+[VDIWEF]\s+(\S+?)\s*:\s*(.*)$'
+    )
+    
+    # 简化格式匹配:级别/TAG: 消息
+    SIMPLE_PATTERN = re.compile(r'^[VDIWEF]/(\S+?)\s*:\s*(.*)$')
+    
+    # 自定义格式匹配:[级别][日期时间][进程信息][TAG][其他]
+    # 示例: [I][2025-09-22 +3.0 23:57:49.377][12735, 1*][tag_float_view][:0, ][remove, reason: clearWhenExitRoom
+    CUSTOM_PATTERN = re.compile(
+        r'^\[([VDIWEF])\]\[[\d\-+:\s.]+\]\[[^\]]+\]\[([^\]]+)\]'
+    )
+    
+    # TAG_XXX 格式匹配
+    TAG_CONSTANT_PATTERN = re.compile(r'TAG_[A-Z_]+')
+    
+    def __init__(self):
+        self.tag_counter = Counter()
+        self.tag_level_counter = {}  # {tag: {level: count}}
+        self.total_lines = 0
+        self.matched_lines = 0
+        self.custom_format_count = 0  # 统计自定义格式的数量
+        self.long_log_counter = Counter()  # 统计超长日志的TAG
+        self.long_log_samples = {}  # 存储超长日志的示例 {tag: [samples]}
+        # 超长日志阈值:字符数(约等于5行,假设每行80字符)
+        self.LONG_LOG_THRESHOLD = 400
+        
+    def analyze_file(self, file_path: str) -> None:
+        """分析日志文件"""
+        print(f"正在分析日志文件: {file_path}")
+        
+        try:
+            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
+                for line in f:
+                    self.total_lines += 1
+                    self._analyze_line(line.strip())
+                    
+                    # 进度提示
+                    if self.total_lines % 10000 == 0:
+                        print(f"已处理 {self.total_lines} 行...")
+                        
+        except FileNotFoundError:
+            print(f"错误: 找不到文件 {file_path}")
+            sys.exit(1)
+        except Exception as e:
+            print(f"错误: 读取文件时发生异常: {e}")
+            sys.exit(1)
+            
+        print(f"\n分析完成!")
+        print(f"总行数: {self.total_lines}")
+        print(f"匹配的日志行数: {self.matched_lines}")
+        if self.custom_format_count > 0:
+            print(f"自定义格式日志数: {self.custom_format_count}")
+        print(f"不同TAG数量: {len(self.tag_counter)}")
+        
+        # 统计超长日志
+        total_long_logs = sum(self.long_log_counter.values())
+        if total_long_logs > 0:
+            print(f"⚠️  超长日志数量: {total_long_logs} ({total_long_logs/self.matched_lines*100:.2f}%)")
+            print(f"   包含超长日志的TAG数: {len(self.long_log_counter)}")
+        
+    def _analyze_line(self, line: str) -> None:
+        """分析单行日志"""
+        if not line:
+            return
+        
+        tag = None
+        level = None
+        message = None
+        
+        # 优先尝试匹配自定义格式:[级别][日期][进程][TAG][其他][消息]
+        custom_match = self.CUSTOM_PATTERN.match(line)
+        if custom_match:
+            level = custom_match.group(1)
+            tag = custom_match.group(2)
+            self.custom_format_count += 1
+            # 提取消息内容(第5个方括号之后的部分)
+            msg_match = re.search(r'(?:\[[^\]]*\]){5}(.*)$', line)
+            if msg_match:
+                message = msg_match.group(1)
+        else:
+            # 尝试匹配Android标准格式
+            match = self.LOG_PATTERN.match(line)
+            if not match:
+                # 尝试匹配简化格式
+                match = self.SIMPLE_PATTERN.match(line)
+            
+            if match:
+                tag = match.group(1)
+                if len(match.groups()) > 1:
+                    message = match.group(2)
+                # 从原始行中提取级别
+                level_match = re.search(r'\s([VDIWEF])\s', line)
+                if level_match:
+                    level = level_match.group(1)
+        
+        # 如果成功提取到TAG,进行统计
+        if tag:
+            self.tag_counter[tag] += 1
+            self.matched_lines += 1
+            
+            # 统计日志级别
+            if level:
+                if tag not in self.tag_level_counter:
+                    self.tag_level_counter[tag] = Counter()
+                self.tag_level_counter[tag][level] += 1
+            
+            # 检查是否为超长日志
+            if message and len(message) > self.LONG_LOG_THRESHOLD:
+                self.long_log_counter[tag] += 1
+                # 保存示例(每个TAG最多保存3个示例)
+                if tag not in self.long_log_samples:
+                    self.long_log_samples[tag] = []
+                if len(self.long_log_samples[tag]) < 3:
+                    # 截取前200个字符作为示例
+                    sample = message[:200] + "..." if len(message) > 200 else message
+                    self.long_log_samples[tag].append(sample)
+    
+    def get_statistics(self) -> List[Tuple[str, int, float]]:
+        """获取统计结果: [(tag, count, percentage), ...]"""
+        if self.matched_lines == 0:
+            return []
+            
+        stats = []
+        for tag, count in self.tag_counter.most_common():
+            percentage = (count / self.matched_lines) * 100
+            stats.append((tag, count, percentage))
+        return stats
+    
+    def print_report(self, top_n: int = None, min_count: int = 0) -> None:
+        """打印分析报告"""
+        stats = self.get_statistics()
+        
+        if not stats:
+            print("没有找到日志数据")
+            return
+            
+        print("\n" + "=" * 100)
+        print("日志TAG使用统计报告")
+        print("=" * 100)
+        print(f"{'排名':<6} {'TAG名称':<40} {'数量':<12} {'占比':<10} {'级别分布'}")
+        print("-" * 100)
+        
+        rank = 1
+        for tag, count, percentage in stats:
+            if min_count > 0 and count < min_count:
+                continue
+                
+            if top_n and rank > top_n:
+                break
+                
+            # 获取级别分布
+            level_dist = ""
+            if tag in self.tag_level_counter:
+                levels = []
+                for level in ['V', 'D', 'I', 'W', 'E', 'F']:
+                    if level in self.tag_level_counter[tag]:
+                        level_count = self.tag_level_counter[tag][level]
+                        levels.append(f"{level}:{level_count}")
+                level_dist = " ".join(levels)
+            
+            print(f"{rank:<6} {tag:<40} {count:<12} {percentage:>6.2f}%    {level_dist}")
+            rank += 1
+            
+        print("=" * 100)
+        
+        # 额外统计
+        self._print_summary_statistics(stats, min_count)
+    
+    def _print_summary_statistics(self, stats: List[Tuple[str, int, float]], min_count: int) -> None:
+        """打印汇总统计"""
+        print("\n" + "=" * 100)
+        print("汇总统计")
+        print("=" * 100)
+        
+        # TOP 10占比
+        if len(stats) >= 10:
+            top10_count = sum(count for _, count, _ in stats[:10])
+            top10_percentage = (top10_count / self.matched_lines) * 100
+            print(f"TOP 10 TAG占比: {top10_percentage:.2f}%")
+        
+        # TOP 20占比
+        if len(stats) >= 20:
+            top20_count = sum(count for _, count, _ in stats[:20])
+            top20_percentage = (top20_count / self.matched_lines) * 100
+            print(f"TOP 20 TAG占比: {top20_percentage:.2f}%")
+        
+        # 低频TAG统计
+        low_freq_tags = [tag for tag, count, _ in stats if count < 10]
+        if low_freq_tags:
+            print(f"\n出现次数 < 10 的TAG: {len(low_freq_tags)} 个")
+            print(f"这些低频TAG总占比: {sum(count for _, count, _ in stats if count < 10) / self.matched_lines * 100:.2f}%")
+        
+        # 按级别统计
+        level_totals = Counter()
+        for tag_levels in self.tag_level_counter.values():
+            level_totals.update(tag_levels)
+        
+        if level_totals:
+            print("\n按日志级别统计:")
+            for level in ['V', 'D', 'I', 'W', 'E', 'F']:
+                if level in level_totals:
+                    count = level_totals[level]
+                    percentage = (count / self.matched_lines) * 100
+                    level_name = {
+                        'V': 'VERBOSE',
+                        'D': 'DEBUG',
+                        'I': 'INFO',
+                        'W': 'WARN',
+                        'E': 'ERROR',
+                        'F': 'FATAL'
+                    }.get(level, level)
+                    print(f"  {level_name:<10}: {count:>10} ({percentage:>6.2f}%)")
+        
+        # 超长日志统计
+        if self.long_log_counter:
+            total_long = sum(self.long_log_counter.values())
+            print(f"\n⚠️  超长日志统计(消息 > {self.LONG_LOG_THRESHOLD} 字符 ≈ 5行):")
+            print(f"  总超长日志数: {total_long} ({total_long/self.matched_lines*100:.2f}%)")
+            print(f"  包含超长日志的TAG TOP 10:")
+            for tag, count in self.long_log_counter.most_common(10):
+                tag_total = self.tag_counter[tag]
+                percentage_in_tag = (count / tag_total) * 100
+                print(f"    • {tag:<35} {count:>6} 条 ({percentage_in_tag:>5.1f}% 的该TAG)")
+        
+        print("=" * 100)
+    
+    def show_long_log_samples(self, top_n: int = 5) -> None:
+        """显示超长日志的示例"""
+        if not self.long_log_counter:
+            print("没有发现超长日志")
+            return
+        
+        print("\n" + "=" * 100)
+        print("超长日志示例")
+        print("=" * 100)
+        
+        for tag, count in self.long_log_counter.most_common(top_n):
+            print(f"\n【{tag}】 共 {count} 条超长日志")
+            if tag in self.long_log_samples:
+                for i, sample in enumerate(self.long_log_samples[tag], 1):
+                    print(f"  示例 {i}: {sample}")
+        
+        print("=" * 100)
+    
+    def export_csv(self, output_path: str, min_count: int = 0) -> None:
+        """导出为CSV文件"""
+        stats = self.get_statistics()
+        
+        try:
+            with open(output_path, 'w', encoding='utf-8') as f:
+                f.write("排名,TAG名称,数量,占比(%),超长日志数,超长占比(%)\n")
+                rank = 1
+                for tag, count, percentage in stats:
+                    if min_count > 0 and count < min_count:
+                        continue
+                    long_count = self.long_log_counter.get(tag, 0)
+                    long_percentage = (long_count / count * 100) if count > 0 else 0
+                    f.write(f"{rank},{tag},{count},{percentage:.2f},{long_count},{long_percentage:.2f}\n")
+                    rank += 1
+            print(f"\n已导出CSV报告到: {output_path}")
+        except Exception as e:
+            print(f"导出CSV失败: {e}")
+    
+    def export_json(self, output_path: str) -> None:
+        """导出为JSON文件"""
+        stats = self.get_statistics()
+        
+        result = {
+            "summary": {
+                "total_lines": self.total_lines,
+                "matched_lines": self.matched_lines,
+                "unique_tags": len(self.tag_counter)
+            },
+            "tags": []
+        }
+        
+        rank = 1
+        for tag, count, percentage in stats:
+            tag_data = {
+                "rank": rank,
+                "tag": tag,
+                "count": count,
+                "percentage": round(percentage, 2)
+            }
+            
+            # 添加级别分布
+            if tag in self.tag_level_counter:
+                tag_data["levels"] = dict(self.tag_level_counter[tag])
+            
+            result["tags"].append(tag_data)
+            rank += 1
+        
+        try:
+            with open(output_path, 'w', encoding='utf-8') as f:
+                json.dump(result, f, indent=2, ensure_ascii=False)
+            print(f"\n已导出JSON报告到: {output_path}")
+        except Exception as e:
+            print(f"导出JSON失败: {e}")
+    
+    def suggest_cleanup(self, threshold_percentage: float = 0.01) -> List[str]:
+        """建议清理的TAG(占比低于阈值)"""
+        stats = self.get_statistics()
+        suggestions = []
+        
+        for tag, count, percentage in stats:
+            if percentage < threshold_percentage:
+                suggestions.append(tag)
+        
+        return suggestions
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='分析Android日志文件中TAG的使用情况',
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例用法:
+  python analyze_log_tags.py logcat.txt
+  python analyze_log_tags.py logcat.txt --top 30
+  python analyze_log_tags.py logcat.txt --min-count 50
+  python analyze_log_tags.py logcat.txt --export report.csv
+  python analyze_log_tags.py logcat.txt --export-json report.json
+  python analyze_log_tags.py logcat.txt --suggest-cleanup
+        """
+    )
+    
+    parser.add_argument('logfile', help='日志文件路径')
+    parser.add_argument('--top', type=int, help='只显示TOP N个TAG', metavar='N')
+    parser.add_argument('--min-count', type=int, default=0, 
+                       help='只显示出现次数 >= N 的TAG', metavar='N')
+    parser.add_argument('--export', help='导出CSV报告到指定文件', metavar='FILE')
+    parser.add_argument('--export-json', help='导出JSON报告到指定文件', metavar='FILE')
+    parser.add_argument('--suggest-cleanup', action='store_true',
+                       help='建议可以清理的低频TAG')
+    parser.add_argument('--threshold', type=float, default=0.01,
+                       help='清理建议的阈值(默认0.01,即0.01%%)', metavar='PERCENT')
+    parser.add_argument('--show-long-logs', type=int, nargs='?', const=5, metavar='N',
+                       help='显示超长日志的示例(默认TOP 5)')
+    
+    args = parser.parse_args()
+    
+    # 检查文件是否存在
+    if not Path(args.logfile).exists():
+        print(f"错误: 文件不存在: {args.logfile}")
+        sys.exit(1)
+    
+    # 分析日志
+    analyzer = LogTagAnalyzer()
+    analyzer.analyze_file(args.logfile)
+    
+    # 打印报告
+    analyzer.print_report(top_n=args.top, min_count=args.min_count)
+    
+    # 导出CSV
+    if args.export:
+        analyzer.export_csv(args.export, min_count=args.min_count)
+    
+    # 导出JSON
+    if args.export_json:
+        analyzer.export_json(args.export_json)
+    
+    # 显示超长日志示例
+    if args.show_long_logs is not None:
+        analyzer.show_long_log_samples(top_n=args.show_long_logs)
+    
+    # 清理建议
+    if args.suggest_cleanup:
+        suggestions = analyzer.suggest_cleanup(threshold_percentage=args.threshold)
+        if suggestions:
+            print(f"\n建议清理的TAG(占比 < {args.threshold}%):")
+            print("=" * 100)
+            for i, tag in enumerate(suggestions, 1):
+                print(f"{i}. {tag}")
+            print(f"\n共 {len(suggestions)} 个TAG建议清理")
+        else:
+            print(f"\n没有找到需要清理的TAG(占比 < {args.threshold}%)")
+
+
+if __name__ == "__main__":
+    main()
+

+ 555 - 0
tool/generate_log_report_html.py

@@ -0,0 +1,555 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+生成日志分析HTML可视化报告
+
+功能特性:
+    - 自动根据输入文件名生成输出报告文件名
+    - 统计和标记超过400字符的超长日志
+    - 可视化展示TAG使用情况和超长日志分布
+    - 提供优化建议
+
+使用方法:
+    # 自动生成报告文件名(推荐)
+    python generate_log_report_html.py com.partyjoy.yoki_20250922.xlog.log
+    # 输出: com.partyjoy.yoki_20250922_report.html
+    
+    # 指定输出文件名
+    python generate_log_report_html.py logcat.txt --output custom_report.html
+"""
+
+import sys
+import argparse
+from pathlib import Path
+from analyze_log_tags import LogTagAnalyzer
+
+
+HTML_TEMPLATE = """
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>日志TAG分析报告</title>
+    <style>
+        * {{{{
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }}}}
+        
+        body {{
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            background: #f5f5f5;
+            padding: 20px;
+            color: #333;
+        }}
+        
+        .container {{
+            max-width: 1400px;
+            margin: 0 auto;
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+            padding: 30px;
+        }}
+        
+        h1 {{
+            color: #1a73e8;
+            margin-bottom: 10px;
+            font-size: 28px;
+        }}
+        
+        .subtitle {{
+            color: #666;
+            margin-bottom: 30px;
+            font-size: 14px;
+        }}
+        
+        .summary {{
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+            gap: 20px;
+            margin-bottom: 30px;
+        }}
+        
+        .summary-card {{
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            padding: 20px;
+            border-radius: 8px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+        }}
+        
+        .summary-card.info {{
+            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+        }}
+        
+        .summary-card.success {{
+            background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+        }}
+        
+        .summary-card.warning {{
+            background: linear-gradient(135deg, #ffa751 0%, #ffe259 100%);
+        }}
+        
+        .summary-card h3 {{
+            font-size: 14px;
+            margin-bottom: 10px;
+            opacity: 0.9;
+        }}
+        
+        .summary-card .value {{
+            font-size: 32px;
+            font-weight: bold;
+        }}
+        
+        .summary-card .sub-value {{
+            font-size: 14px;
+            margin-top: 5px;
+            opacity: 0.9;
+        }}
+        
+        .chart-container {{
+            margin: 30px 0;
+            background: #fafafa;
+            padding: 20px;
+            border-radius: 8px;
+        }}
+        
+        .chart-title {{
+            font-size: 18px;
+            font-weight: bold;
+            margin-bottom: 15px;
+            color: #333;
+        }}
+        
+        .bar-chart {{
+            margin: 20px 0;
+        }}
+        
+        .bar-item {{
+            margin-bottom: 12px;
+        }}
+        
+        .bar-label {{
+            display: flex;
+            justify-content: space-between;
+            margin-bottom: 5px;
+            font-size: 13px;
+        }}
+        
+        .tag-name {{
+            font-weight: 500;
+            color: #333;
+        }}
+        
+        .tag-stats {{
+            color: #666;
+        }}
+        
+        .bar-bg {{
+            background: #e0e0e0;
+            height: 24px;
+            border-radius: 4px;
+            overflow: hidden;
+            position: relative;
+        }}
+        
+        .bar-fill {{
+            height: 100%;
+            background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+            display: flex;
+            align-items: center;
+            justify-content: flex-end;
+            padding-right: 8px;
+            color: white;
+            font-size: 12px;
+            font-weight: bold;
+            transition: width 0.3s ease;
+        }}
+        
+        table {{
+            width: 100%;
+            border-collapse: collapse;
+            margin: 20px 0;
+            font-size: 14px;
+        }}
+        
+        th, td {{
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #e0e0e0;
+        }}
+        
+        th {{
+            background: #f5f5f5;
+            font-weight: 600;
+            color: #333;
+            position: sticky;
+            top: 0;
+        }}
+        
+        tr:hover {{
+            background: #fafafa;
+        }}
+        
+        .rank {{
+            font-weight: bold;
+            color: #666;
+        }}
+        
+        .percentage {{
+            font-weight: 600;
+        }}
+        
+        .high-freq {{
+            color: #d32f2f;
+        }}
+        
+        .med-freq {{
+            color: #f57c00;
+        }}
+        
+        .low-freq {{
+            color: #388e3c;
+        }}
+        
+        .level-badge {{
+            display: inline-block;
+            padding: 2px 6px;
+            border-radius: 3px;
+            font-size: 11px;
+            font-weight: bold;
+            margin-right: 4px;
+        }}
+        
+        .level-v {{ background: #9e9e9e; color: white; }}
+        .level-d {{ background: #2196f3; color: white; }}
+        .level-i {{ background: #4caf50; color: white; }}
+        .level-w {{ background: #ff9800; color: white; }}
+        .level-e {{ background: #f44336; color: white; }}
+        
+        .section {{
+            margin: 40px 0;
+        }}
+        
+        .section-title {{
+            font-size: 20px;
+            font-weight: bold;
+            margin-bottom: 20px;
+            color: #333;
+            border-left: 4px solid #1a73e8;
+            padding-left: 12px;
+        }}
+        
+        .suggestion-box {{
+            background: #fff3cd;
+            border-left: 4px solid #ffc107;
+            padding: 15px;
+            border-radius: 4px;
+            margin: 20px 0;
+        }}
+        
+        .suggestion-title {{
+            font-weight: bold;
+            margin-bottom: 10px;
+            color: #856404;
+        }}
+        
+        .suggestion-list {{
+            margin-left: 20px;
+            color: #856404;
+        }}
+        
+        footer {{
+            margin-top: 40px;
+            padding-top: 20px;
+            border-top: 1px solid #e0e0e0;
+            text-align: center;
+            color: #666;
+            font-size: 12px;
+        }}
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>📊 日志TAG分析报告</h1>
+        <div class="subtitle">生成时间: {timestamp} | 文件: {filename}</div>
+        
+        <div class="summary">
+            <div class="summary-card">
+                <h3>总日志行数</h3>
+                <div class="value">{total_lines:,}</div>
+            </div>
+            <div class="summary-card info">
+                <h3>有效日志行</h3>
+                <div class="value">{matched_lines:,}</div>
+            </div>
+            <div class="summary-card success">
+                <h3>不同TAG数量</h3>
+                <div class="value">{unique_tags}</div>
+            </div>
+            <div class="summary-card warning">
+                <h3>超长日志数</h3>
+                <div class="value">{long_logs_count:,}</div>
+                <div class="sub-value">占比 {long_logs_percentage:.2f}%</div>
+            </div>
+        </div>
+        
+        <div class="section">
+            <div class="section-title">TOP 20 高频TAG</div>
+            <div class="chart-container">
+                <div class="bar-chart">
+                    {top20_bars}
+                </div>
+            </div>
+        </div>
+        
+        <div class="section">
+            <div class="section-title">超长日志TOP 10(>400字符)</div>
+            <div class="chart-container">
+                <div class="bar-chart">
+                    {long_logs_bars}
+                </div>
+            </div>
+        </div>
+        
+        <div class="section">
+            <div class="section-title">完整TAG列表</div>
+            <table>
+                <thead>
+                    <tr>
+                        <th>排名</th>
+                        <th>TAG名称</th>
+                        <th>出现次数</th>
+                        <th>占比</th>
+                        <th>超长日志</th>
+                        <th>日志级别分布</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {table_rows}
+                </tbody>
+            </table>
+        </div>
+        
+        <div class="section">
+            <div class="suggestion-box">
+                <div class="suggestion-title">💡 优化建议</div>
+                <div class="suggestion-list">
+                    {suggestions}
+                </div>
+            </div>
+        </div>
+        
+        <footer>
+            报告由 analyze_log_tags.py 生成
+        </footer>
+    </div>
+</body>
+</html>
+"""
+
+
+def generate_html_report(analyzer: LogTagAnalyzer, filename: str, output_path: str):
+    """生成HTML报告"""
+    from datetime import datetime
+    
+    stats = analyzer.get_statistics()
+    
+    # 计算超长日志统计
+    total_long_logs = sum(analyzer.long_log_counter.values())
+    long_logs_percentage = (total_long_logs / analyzer.matched_lines * 100) if analyzer.matched_lines > 0 else 0
+    
+    # 生成超长日志TOP 10柱状图
+    long_logs_bars = ""
+    if analyzer.long_log_counter:
+        long_log_stats = analyzer.long_log_counter.most_common(10)
+        max_long_count = long_log_stats[0][1] if long_log_stats else 1
+        
+        for i, (tag, long_count) in enumerate(long_log_stats, 1):
+            tag_total = analyzer.tag_counter[tag]
+            long_percentage_in_tag = (long_count / tag_total) * 100
+            bar_width = (long_count / max_long_count) * 100
+            
+            long_logs_bars += f"""
+            <div class="bar-item">
+                <div class="bar-label">
+                    <span class="tag-name">{i}. {tag}</span>
+                    <span class="tag-stats high-freq">{long_count:,} 条 ({long_percentage_in_tag:.1f}% 的该TAG)</span>
+                </div>
+                <div class="bar-bg">
+                    <div class="bar-fill" style="width: {bar_width}%; background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);">{long_count:,}</div>
+                </div>
+            </div>
+            """
+    else:
+        long_logs_bars = '<p style="color: #666; text-align: center; padding: 20px;">未发现超长日志</p>'
+    
+    # 生成TOP 20柱状图
+    top20_bars = ""
+    for i, (tag, count, percentage) in enumerate(stats[:20], 1):
+        freq_class = "high-freq" if percentage > 5 else ("med-freq" if percentage > 1 else "low-freq")
+        top20_bars += f"""
+        <div class="bar-item">
+            <div class="bar-label">
+                <span class="tag-name">{i}. {tag}</span>
+                <span class="tag-stats {freq_class}">{count:,} 次 ({percentage:.2f}%)</span>
+            </div>
+            <div class="bar-bg">
+                <div class="bar-fill" style="width: {min(percentage * 2, 100)}%">{percentage:.2f}%</div>
+            </div>
+        </div>
+        """
+    
+    # 生成表格行
+    table_rows = ""
+    for i, (tag, count, percentage) in enumerate(stats, 1):
+        freq_class = "high-freq" if percentage > 5 else ("med-freq" if percentage > 1 else "low-freq")
+        
+        # 超长日志数量
+        long_count = analyzer.long_log_counter.get(tag, 0)
+        if long_count > 0:
+            long_percentage = (long_count / count) * 100
+            long_info = f'<span class="high-freq">{long_count:,} ({long_percentage:.1f}%)</span>'
+        else:
+            long_info = '<span style="color: #999;">-</span>'
+        
+        # 级别分布
+        level_badges = ""
+        if tag in analyzer.tag_level_counter:
+            for level in ['V', 'D', 'I', 'W', 'E']:
+                if level in analyzer.tag_level_counter[tag]:
+                    level_count = analyzer.tag_level_counter[tag][level]
+                    level_badges += f'<span class="level-badge level-{level.lower()}">{level}:{level_count}</span>'
+        
+        table_rows += f"""
+        <tr>
+            <td class="rank">{i}</td>
+            <td><code>{tag}</code></td>
+            <td>{count:,}</td>
+            <td class="percentage {freq_class}">{percentage:.2f}%</td>
+            <td>{long_info}</td>
+            <td>{level_badges}</td>
+        </tr>
+        """
+    
+    # 生成优化建议
+    suggestions_html = "<ul>"
+    
+    # 超长日志建议
+    if total_long_logs > 0:
+        suggestions_html += f"<li><strong>⚠️ 超长日志优化:</strong>发现 {total_long_logs:,} 条超长日志({long_logs_percentage:.2f}%),建议优化以下TAG:"
+        suggestions_html += "<ul>"
+        top_long_tags = analyzer.long_log_counter.most_common(5)
+        for tag, long_count in top_long_tags:
+            tag_total = analyzer.tag_counter[tag]
+            long_pct = (long_count / tag_total) * 100
+            suggestions_html += f"<li>{tag}: {long_count:,} 条超长日志(占该TAG的 {long_pct:.1f}%)</li>"
+        suggestions_html += "</ul></li>"
+    
+    # 高频TAG建议
+    high_freq_tags = [(tag, p) for tag, _, p in stats[:10] if p > 10]
+    if high_freq_tags:
+        suggestions_html += "<li><strong>高频TAG优化:</strong>以下TAG占比较高,建议检查是否有冗余日志<ul>"
+        for tag, percentage in high_freq_tags:
+            suggestions_html += f"<li>{tag} ({percentage:.2f}%)</li>"
+        suggestions_html += "</ul></li>"
+    
+    # 低频TAG建议
+    low_freq_tags = [tag for tag, count, _ in stats if count < 5]
+    if len(low_freq_tags) > 10:
+        suggestions_html += f"<li><strong>低频TAG清理:</strong>发现 {len(low_freq_tags)} 个出现次数 < 5 的TAG,建议review并移除不必要的日志</li>"
+    
+    # TOP占比分析
+    if len(stats) >= 20:
+        top20_percentage = sum(p for _, _, p in stats[:20])
+        suggestions_html += f"<li><strong>集中度分析:</strong>TOP 20 TAG占总日志的 {top20_percentage:.2f}%,"
+        if top20_percentage > 80:
+            suggestions_html += "日志较为集中,重点优化这些TAG即可获得明显效果</li>"
+        else:
+            suggestions_html += "日志较为分散,建议统一TAG命名规范</li>"
+    
+    suggestions_html += "</ul>"
+    
+    # 填充模板
+    html = HTML_TEMPLATE.format(
+        timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+        filename=filename,
+        total_lines=analyzer.total_lines,
+        matched_lines=analyzer.matched_lines,
+        unique_tags=len(analyzer.tag_counter),
+        long_logs_count=total_long_logs,
+        long_logs_percentage=long_logs_percentage,
+        long_logs_bars=long_logs_bars,
+        top20_bars=top20_bars,
+        table_rows=table_rows,
+        suggestions=suggestions_html
+    )
+    
+    # 写入文件
+    with open(output_path, 'w', encoding='utf-8') as f:
+        f.write(html)
+    
+    print(f"\n✅ HTML报告已生成: {output_path}")
+    print(f"   用浏览器打开查看: file://{Path(output_path).absolute()}")
+
+
+def generate_output_filename(input_file: str) -> str:
+    """根据输入文件名生成输出报告文件名"""
+    input_path = Path(input_file)
+    
+    # 获取文件名(不含扩展名)
+    base_name = input_path.stem
+    
+    # 移除常见的日志文件后缀
+    # 例如: com.partyjoy.yoki_20250922.xlog.log -> com.partyjoy.yoki_20250922
+    for suffix in ['.xlog', '.log', '.txt']:
+        if base_name.endswith(suffix):
+            base_name = base_name[:-len(suffix)]
+    
+    # 生成报告文件名: 原文件名_report.html
+    output_name = f"{base_name}_report.html"
+    
+    # 如果输入文件在某个目录下,输出到同一目录
+    if input_path.parent != Path('.'):
+        output_path = input_path.parent / output_name
+    else:
+        output_path = Path(output_name)
+    
+    return str(output_path)
+
+
+def main():
+    parser = argparse.ArgumentParser(description='生成日志TAG分析的HTML可视化报告')
+    parser.add_argument('logfile', help='日志文件路径')
+    parser.add_argument('--output', '-o', default=None,
+                       help='输出HTML文件路径(默认: 根据输入文件名自动生成)')
+    
+    args = parser.parse_args()
+    
+    # 检查文件
+    if not Path(args.logfile).exists():
+        print(f"❌ 错误: 文件不存在: {args.logfile}")
+        sys.exit(1)
+    
+    # 确定输出文件名
+    if args.output:
+        output_path = args.output
+    else:
+        output_path = generate_output_filename(args.logfile)
+        print(f"📝 自动生成输出文件名: {output_path}")
+    
+    # 分析日志
+    print(f"正在分析日志文件: {args.logfile}")
+    analyzer = LogTagAnalyzer()
+    analyzer.analyze_file(args.logfile)
+    
+    # 生成HTML
+    generate_html_report(analyzer, args.logfile, output_path)
+
+
+if __name__ == "__main__":
+    main()
+