Skip to content

Latest commit

 

History

History
476 lines (338 loc) · 17 KB

File metadata and controls

476 lines (338 loc) · 17 KB

XWidget 库代码审查报告

审查时间:2026-07-27
审查范围:xwidget/src/main/java/com/pichs/xwidget/ 全部 150 个源文件
所有问题均已通过阅读源码验证,非推测


一、Bug(确认存在的逻辑错误)

1. XRoundBackgroundHelper.setCheckedCubeSidesHeight() — 参数比较错误

文件: utils/XRoundBackgroundHelper.java:1701

// 当前代码(错误)
if (this.checkedCubeFrontHeight == left  // 应该是 == front
    && checkedCubeBackHeight == back
    && checkedCubeRightHeight == right
    && checkedCubeLeftHeight == left) {
    return;
}

checkedCubeFrontHeight 与参数 left 做比较,应与 front 比较。
后果:当只有 front 发生变化时,如果初始 front == left,提前 return,背景不更新。

修复:

if (this.checkedCubeFrontHeight == front
    && checkedCubeBackHeight == back
    && checkedCubeRightHeight == right
    && checkedCubeLeftHeight == left) {
    return;
}

2. XAlphaHelper.setScaleOnDisabled()isChangeScaleOnDisable 永远为 false

文件: utils/XAlphaHelper.java:208-210

public void setScaleOnDisabled(float scaleRate) {
    this.mDisabledScale = scaleRate;
    isChangeScaleOnDisable = mDisabledScale != scaleRate; // 刚赋值,永远 false
}

mDisabledScale 刚被赋值为 scaleRate,两者必然相等,isChangeScaleOnDisable 始终为 false,disabled 缩放效果永远不生效。

修复:

isChangeScaleOnDisable = mNormalScale != scaleRate;

3. XDisplayHelper.getPhysicalHeight() — 降级时返回宽度而非高度

文件: utils/XDisplayHelper.java:132-134

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
    return getScreenWidth(context);  // 错误:应该返回 getScreenHeight
}

API < 23 时直接返回了屏幕宽度,方法名和注释都是"物理高"。


4. XVerificationCodeEditText TYPE_HOLLOW 死代码

文件: edittext/XVerificationCodeEditText.java:501

for (int i = 0; i < maxLength; i++) {
    // i 的范围是 [0, maxLength-1],i == maxLength 永不为 true
    if (i == 0 || i == maxLength) {
        continue;
    }
    ...
}

i == maxLengthi < maxLength 的循环中永远不成立,意图可能是 i == maxLength - 1(跳过最后一格的分割线)。


二、性能问题

5. XRoundBackgroundHelper.onDraw() — 每帧分配 float 数组

文件: utils/XRoundBackgroundHelper.java:410-430

@SuppressLint("DrawAllocation")  // 用注解压制了警告,但问题依然存在
public void onDraw(Canvas canvas) {
    float[] radii;
    if (...) {
        radii = new float[]{...};  // 每次 draw 都 new
    } else {
        radii = new float[]{...};  // 每次 draw 都 new
    }
    mClipPath.addRoundRect(0f, 0f, mWidth, mHeight, radii, Path.Direction.CW);
}

float[8] 在每帧绘制时分配,对频繁重绘的 ImageView 会造成持续 GC 压力。

修复:float[] mRadii = new float[8] 作为成员变量,只在圆角值变化时更新。


6. XVerificationCodeEditText — 动画帧内持续创建 Paint 对象

文件: edittext/XVerificationCodeEditText.java:353-365

case 1:  // 每 50ms 触发一次
    if (isLoading) {
        borderPaint = new Paint();        // 每帧 new Paint
        borderLoadingPaint = new Paint(); // 每帧 new Paint
        ...
    }

加载动画每 50ms 触发一次,每次 new Paint() × 2,1 秒内创建 40 个对象。
此外 onTextChanged() 中也每次 new Paint()(第 519 行)。

修复: 直接调用 paint.setAlpha(alpha)paint.setColor(color) 更新已有对象,不要 new。


7. XWidgetCachecommit() 在主线程同步写磁盘

文件: utils/XWidgetCache.java:61,71

editor.commit(); // 阻塞调用

setStringsetInt 都用 commit(),字体设置路径(如 XTypefaceHelper.setGlobalTypefaceFromAssets)会在主线程触发磁盘 I/O。

修复: 改为 editor.apply(),如需立即写完才 commit()


8. XDisplayHelper.doGetRealScreenSize() — 重复反射调用

文件: utils/XDisplayHelper.java:180-210

每次调用都走多轮反射(getRawWidth/getRawHeightgetRealSize),且无缓存。
API 31+ 应改用 WindowMetricscontext.getSystemService(WindowManager.class).getCurrentWindowMetrics())。


三、安全问题

9. XWebView — JS 默认开启 + 混合内容无限制

文件: webview/XWebView.java:32-58

settings.setJavaScriptEnabled(true);
settings.setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW);

两个高危配置默认开启,且构造函数不提供关闭入口。
库使用者如果直接用 XWebView 加载内部页面,可能被 XSS 攻击或中间人劫持(HTTP 资源注入到 HTTPS 页)。

另外 SINGLE_COLUMN 布局算法在 API 23 已废弃。

建议:

  • 提供 setJavaScriptEnabled(false) 的可配置入口
  • MIXED_CONTENT_ALWAYS_ALLOW 改为 MIXED_CONTENT_NEVER_ALLOW 作为默认值
  • 替换废弃的 SINGLE_COLUMN 布局

四、API 设计问题

10. XVerificationCodeEditText — Timer 不能重复使用,View 重用后光标失效

文件: edittext/XVerificationCodeEditText.java:297-305, 536-545

// onAttachedToWindow
timer.scheduleAtFixedRate(timerTask, 0, cursorDuration);

// onDetachedFromWindow  
timer.cancel();  // cancel 后 Timer 和 TimerTask 均不可复用

Timer.cancel() 后该 Timer 实例报废,TimerTask 也不能重新提交。
View 从 RecyclerView 或 Fragment 中 detach 后再 attach,光标闪烁永久失效

修复:onDetachedFromWindow 中停止,在 onAttachedToWindow 中重新创建:

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    timerTask = new TimerTask() { ... };
    timer = new Timer();
    timer.scheduleAtFixedRate(timerTask, 0, cursorDuration);
}

或直接用 postDelayed 替代 Timer(更简洁,天然在主线程执行)。


11. XVerificationCodeEditText.drawText() — 每帧创建 String 对象

文件: edittext/XVerificationCodeEditText.java:569

canvas.drawText(String.valueOf(charSequence.charAt(i)), baseX, baseY, textPaint);

String.valueOf(char) 每次 draw 都创建新 String 对象。

修复: 使用 char[] buffer:

private final char[] mCharBuffer = new char[1];
// drawText 时:
mCharBuffer[0] = charSequence.charAt(i);
canvas.drawText(mCharBuffer, 0, 1, baseX, baseY, textPaint);

12. XTypefaceHelper — 静态可变状态 + 非线程安全

文件: utils/XTypefaceHelper.java

  • mTypefaceMapWeakHashMap,非线程安全,但 post()observer() 方法没有加锁,多线程访问时会抛 ConcurrentModificationException
  • isOpenTypefacepublic static 字段,可被外部直接赋值绕过 init() 逻辑
  • setGlobalTypefaceStyle() 会覆盖之前设置的 typeface:mGlobalTypeface = Typeface.create(mGlobalTypeface, style),若 mGlobalTypeface == null,相当于重置字体

13. XRoundBackgroundHelper / XBackgroundHelper — 大量重复代码

每个 set*() 方法都独立重建所有状态的背景 Drawable,5 个状态 × N 个 setter = 数百行重复逻辑。

setRadius()setBorderWidth()setCubeSidesBorderColor() 等方法内都有 100-200 行几乎一样的代码块。

建议: 提取 rebuildAllBackgrounds() 方法,每个 setter 只更新对应字段,然后调用统一的重建方法。


14. XWheelView.Adapter.getItem() 只支持 String

文件: wheel/XWheelView.java:356

@NonNull
public abstract String getItem(int position);

WheelView 只能显示字符串,不支持自定义数据类型绑定。
使用泛型 Adapter<T> 会更灵活,或至少加一个 getDisplayText(int position) 分离数据与显示逻辑。


15. XWheelView.setCurrentItem() 只有瞬移,没有平滑滚动

文件: wheel/XWheelView.java:279

public void setCurrentItem(int position) {
    mLayoutManager.scrollToPositionWithOffset(position, 0); // 直接跳转
}

缺少 smoothScrollToPosition 的重载,用户无法控制初始化时是否需要动画。


16. DEFAULT_COLOR_TRANSPARENT = 0x0000000f — 魔数作为哨兵值

文件: utils/XBackgroundHelper.java:28

public static final int DEFAULT_COLOR_TRANSPARENT = 0x0000000f;

用一个 alpha=0、blue=15 的"几乎透明"颜色作为"未设置"的标志值。
若用户真的想把背景色设为 0x0000000f,会被误判为"未设置",背景显示异常。
更健壮的方案是用 Integer 可为 null,或独立的 boolean 标志位。


17. XColorUtils — 颜色算术运算可能整数溢出

文件: utils/XColorUtils.java:25-32

int[] colors = new int[]{
    tintColor - 0xAA000000,   // 危险:整数溢出
    tintColor - 0x99000000,
    tintColor | 0xFF000000,   // 强制 alpha = 0xFF,语义正确但不明显
    ...
};

tintColor - 0xAA000000 的意图是"降低 alpha 值",但这是整数减法,当 tintColor 的 alpha 字节小于 0xAA 时(如传入半透明颜色),会发生下溢,导致 alpha、R 通道同时被破坏。

修复:

Color.argb(0x55, Color.red(tintColor), Color.green(tintColor), Color.blue(tintColor))

18. ShineViewValueAnimator.setFrameDelay() 是全局静态调用

文件: shinebutton/ShineView.java:79, 99

ValueAnimator.setFrameDelay(FRAME_REFRESH_DELAY); // 25ms,静态方法!

ValueAnimator.setFrameDelay()静态方法,会影响整个 App 所有 ValueAnimator 的帧率(默认 ~16ms → 25ms)。
每次创建 ShineView 时都被调用,即使动画结束后也不会恢复,导致 App 内所有动画被永久降速。

修复: 删除此调用,或在动画结束后恢复默认值:

mShineAnimator.addListener(new AnimatorListenerAdapter() {
    public void onAnimationEnd(Animator a) {
        ValueAnimator.setFrameDelay(10); // 恢复默认
        ...
    }
});

19. ShineView.getConfigPaint()nextInt(0) 崩溃风险

文件: shinebutton/ShineView.java:214

private Paint getConfigPaint(Paint paint) {
    if (mEnableFlashing) {
        paint.setColor(mFlashingColors.get(mRandom.nextInt(mColorCount - 1)));
    }
    return paint;
}

mColorCount 来自 mFlashingColors.size()nextInt(mColorCount - 1)mColorCount == 1 时调用 nextInt(0),抛出 IllegalArgumentException
此外 mColorCount - 1 作为 bound 意味着永远不会取到最后一个颜色(off-by-one)。

修复: mRandom.nextInt(mColorCount)mColorCount > 0 时才调用。


20. ShineButton.fitFragment()getActivity() 可能返回 null

文件: shinebutton/ShineButton.java:111-113

public void fitFragment(Fragment fragment) {
    initWindow(fragment.getActivity()); // getActivity() 可能为 null
}

Fragment 未 attach 到 Activity 时,getActivity() 返回 null,initWindow(null)activity.getWindow() 直接 NPE。

修复:

public void fitFragment(Fragment fragment) {
    Activity activity = fragment.getActivity();
    if (activity != null) {
        initWindow(activity);
    }
}

21. ShineButton.showAnim() — 快速点击会叠加多个 ShineView 到 DecorView

文件: shinebutton/ShineButton.java:266-278

public void showAnim() {
    ShineView shineView = new ShineView(getContext(), this, mShineParams);
    ViewGroup rootView = (ViewGroup) getWindow().getDecorView();
    rootView.addView(shineView, ...); // 没有防重入检查
    ...
}

用户快速连击时,每次点击都会向 DecorView 添加一个新的 ShineView,多个动画同时运行且互相叠加,视觉混乱,同时也有内存泄漏风险(动画结束前 ShineView 持有对 ShineButton 的弱引用)。


22. XEditTextHelper — 多处反射访问私有字段,API 29+ 静默失效

文件: utils/XEditTextHelper.java:71-87, 126-142

Field editorField = TextView.class.getDeclaredField("mEditor");        // 私有字段
Field mInsertionControllerEnabledField = editorClass.getDeclaredField("mInsertionControllerEnabled");
// API 29+ 非 SDK 接口,反射被限制
@SuppressLint("BlockedPrivateApi")
Field eCursorDrawable = TextView.class.getDeclaredField("mCursorDrawable");

mEditor, mInsertionControllerEnabled, mCursorDrawable, mDrawableForCursor 均为 Android 私有字段,在 API 29+ 被列为 @hide 非 SDK 接口,反射调用会被系统拦截。
代码用 catch (Exception e) { e.printStackTrace(); } 静默吞掉了所有异常,导致光标设置和禁用复制粘贴功能在 API 29+ 上悄无声息地失效,但开发者不会收到任何提示。


23. XNestedScrollLayout.scrollBy() — 向上滚动时忽略 topView 未消费的量

文件: nestedscroll/XNestedScrollLayout.java:357-363

public void scrollBy(int dy) {
    if ((dy > 0 || mBottomView == null) && mTopAreaBehavior != null) {
        mTopAreaBehavior.scroll(this, ((View) mTopView), dy);
    } else if (dy != 0 && mBottomView != null) {
        mBottomView.consumeScroll(dy); // dy < 0 时直接给 bottomView
    }
}

dy < 0(向上滚动)且 mBottomView != null 时,直接把滚动量全给 bottomView,不经过 topView 中间层。若 bottomView 已滚到顶部,剩余滚动量不会回传给 topView 继续消费,导致 topView 无法从代码触发向上滚动。


五、废弃 API 使用

位置 废弃 API 替代方案
XDisplayHelper.getRealScreenWidth/Height Display.getRealMetrics() (API 31 废弃) WindowMetrics.getBounds()
XDisplayHelper.doGetRealScreenSize Display.getRealSize() (API 31 废弃) WindowMetrics.getBounds()
XDisplayHelper.deviceHasNavigationBar WindowManagerService.hasNavigationBar() 私有反射 WindowInsets.isVisible(WindowInsets.Type.navigationBars())
XWebView LayoutAlgorithm.SINGLE_COLUMN (API 23 废弃) LayoutAlgorithm.TEXT_AUTOSIZING 或不设置
XViewHelper.setBackground View.setBackgroundDrawable() (API 16 废弃) View.setBackground() (已兼容到 API 16,可直接删除兼容分支)

六、问题优先级汇总

级别 编号 文件 问题简述
🔴 Bug 1 XRoundBackgroundHelper setCheckedCubeSidesHeight 参数比较用 left 代替了 front
🔴 Bug 2 XAlphaHelper setScaleOnDisabledisChangeScaleOnDisable 永远 false
🔴 Bug 3 XDisplayHelper getPhysicalHeight 降级时返回宽度而非高度
🔴 Bug 4 XVerificationCodeEditText TYPE_HOLLOW 中 i == maxLength 死代码,分割线逻辑错误
🔴 Bug 10 XVerificationCodeEditText Timer 不可复用,View 重用后光标永久失效
🔴 Bug 19 ShineView nextInt(0) 在单色时崩溃 + off-by-one
🔴 Bug 20 ShineButton fitFragment 未判断 getActivity() 为 null
🟠 性能 5 XRoundBackgroundHelper onDraw 每帧 new float[8],应缓存为成员变量
🟠 性能 6 XVerificationCodeEditText 动画帧/文字变化时重复 new Paint(),改用 setColor/setAlpha
🟠 性能 7 XWidgetCache editor.commit() 主线程同步写磁盘,改用 apply()
🟠 性能 11 XVerificationCodeEditText drawText 中每帧 String.valueOf(char) 创建对象
🟠 性能 18 ShineView ValueAnimator.setFrameDelay() 是全局静态调用,拖慢全 App 动画
🟡 安全 9 XWebView JS 默认开启 + MIXED_CONTENT_ALWAYS_ALLOW,无关闭入口
🟡 安全 22 XEditTextHelper 反射访问私有字段,API 29+ 静默失效,异常被吞掉
🟡 API设计 12 XTypefaceHelper 静态可变状态 + WeakHashMap 非线程安全
🟡 API设计 13 XRoundBackgroundHelper Setter 间大量重复代码,需提取 rebuildAllBackgrounds()
🟡 API设计 17 XColorUtils 颜色算术减法可能整数溢出,应改用 Color.argb()
🟡 API设计 21 ShineButton 快速点击叠加多个 ShineView,无防重入
🟡 API设计 23 XNestedScrollLayout scrollBy 向上滚动时不经过 topView,滚动链断裂
🔵 改进 8 XDisplayHelper doGetRealScreenSize 多层反射无缓存,API 31+ 用 WindowMetrics
🔵 改进 14 XWheelView Adapter.getItem() 只返回 String,无法自定义数据类型
🔵 改进 15 XWheelView setCurrentItem 仅支持瞬移,缺少平滑滚动重载
🔵 改进 16 XBackgroundHelper DEFAULT_COLOR_TRANSPARENT=0x0000000f 魔数作哨兵值,易误判
🔵 改进 废弃 API Display.getRealMetrics/getRealSizeSINGLE_COLUMN、私有 WM 反射