You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin下TextView中HTML内嵌图片下载缓存优化方案求助

问题与优化方案

我曾参考Stack Overflow上的问题实现了两套代码,用于加载HTML内容中<img>标签内的图片,但遇到两个问题:一是部分图片无法被缓存,二是静态Picasso实例会造成应用内存泄漏。现寻求更优实现方案,要求支持Kaolin。


原有实现代码

PicassoCache

public class PicassoCache {

//    @SuppressLint("StaticFieldLeak")
    private static Picasso picassoInstance = null;

    private PicassoCache(Context context) {

        Downloader downloader = new OkHttp3Downloader(context, Integer.MAX_VALUE);
        Picasso.Builder builder = new Picasso.Builder(context);
        builder.downloader(downloader);
        picassoInstance = builder.build();
    }

    public static Picasso getPicassoInstance(Context context) {

        if (picassoInstance == null) {

            //noinspection InstantiationOfUtilityClass
            new PicassoCache(context);
            return picassoInstance;
        }

        return picassoInstance;
    }
}

PicassoImageGetter

public class PicassoImageGetter implements Html.ImageGetter {

    private static final String TAG = "PicassoImageGetter";

    private TextView textView = null;
    Context mContext;

    public PicassoImageGetter(TextView target, Context context) {
        textView = target;
        mContext = context;
    }

    public PicassoImageGetter() {

    }

    @Override
    public Drawable getDrawable(String source) {
        BitmapDrawablePlaceHolder drawable = new BitmapDrawablePlaceHolder();
//        Picasso.get().load(source).into(drawable);
        PicassoCache.getPicassoInstance(mContext)
                .load(source)
                .priority(Picasso.Priority.HIGH)
                .error(R.drawable.no_image)
                .into(drawable);

        return drawable;

    }

    @SuppressWarnings("deprecation")
    private class BitmapDrawablePlaceHolder extends BitmapDrawable implements com.squareup.picasso.Target {
        protected Drawable drawable;
        @Override
        public void draw(final Canvas canvas) {
            if (drawable != null) {
                drawable.draw(canvas);
            }
        }

        public void setDrawable(Drawable drawable) {
                this.drawable = drawable;
                int width = drawable.getIntrinsicWidth();
                int height = drawable.getIntrinsicHeight();
                drawable.setBounds(0, 0, width, height);
                setBounds(0, 0, width, height);
            if (textView != null) {
                textView.setText(textView.getText());
            }
        }

        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            setDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
        }

        @Override
        public void onBitmapFailed(Exception e, Drawable errorDrawable) {
            setDrawable(errorDrawable);
            Log.e(TAG, "onBitmapFailed: "+e.toString() );
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {

        }

    }
}

原有使用方式

val imageGetter = PicassoImageGetter(binding.blogContent, this)
val html: Spannable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, imageGetter, null) as Spannable
} else {
    Html.fromHtml(content, imageGetter, null) as Spannable
}
binding.blogContent.text = html

效果展示

效果展示


优化方案

1. 修复内存泄漏

移除静态Picasso实例,改用Application全局初始化,避免持有Activity/Fragment Context导致泄漏:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // 设置50MB磁盘缓存,避免缓存配置异常
        val downloader = OkHttp3Downloader(this, 50 * 1024 * 1024)
        val picasso = Picasso.Builder(this)
            .downloader(downloader)
            .build()
        Picasso.setSingletonInstance(picasso)
    }
}

2. 解决缓存不全问题

替换Integer.MAX_VALUE为合理缓存大小(如50MB),避免OkHttp缓存机制异常;Picasso默认已开启内存/磁盘缓存,无需额外配置。

3. 优化ImageGetter,防止Target被GC回收

持有所有加载目标的引用,避免Picasso回调时Target已被回收导致图片加载失败;同时在页面销毁时清理请求,释放资源:

public class PicassoImageGetter implements Html.ImageGetter {
    private static final String TAG = "PicassoImageGetter";
    private final TextView textView;
    private final Context context;
    private final List<BitmapDrawablePlaceHolder> targetList = new ArrayList<>();

    public PicassoImageGetter(TextView target, Context context) {
        this.textView = target;
        // 使用Application Context,避免泄漏
        this.context = context.getApplicationContext();
    }

    @Override
    public Drawable getDrawable(String source) {
        BitmapDrawablePlaceHolder drawable = new BitmapDrawablePlaceHolder();
        targetList.add(drawable);
        Picasso.get()
                .load(source)
                .priority(Picasso.Priority.HIGH)
                .error(R.drawable.no_image)
                .into(drawable);
        return drawable;
    }

    // 页面销毁时调用,清理请求和引用
    public void clear() {
        for (BitmapDrawablePlaceHolder target : targetList) {
            Picasso.get().cancelRequest(target);
        }
        targetList.clear();
    }

    @SuppressWarnings("deprecation")
    private class BitmapDrawablePlaceHolder extends BitmapDrawable implements com.squareup.picasso.Target {
        protected Drawable drawable;

        @Override
        public void draw(final Canvas canvas) {
            if (drawable != null) {
                drawable.draw(canvas);
            }
        }

        public void setDrawable(Drawable drawable) {
            this.drawable = drawable;
            int width = drawable.getIntrinsicWidth();
            int height = drawable.getIntrinsicHeight();
            drawable.setBounds(0, 0, width, height);
            setBounds(0, 0, width, height);
            if (textView != null) {
                textView.setText(textView.getText());
            }
        }

        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            setDrawable(new BitmapDrawable(context.getResources(), bitmap));
        }

        @Override
        public void onBitmapFailed(Exception e, Drawable errorDrawable) {
            setDrawable(errorDrawable);
            Log.e(TAG, "onBitmapFailed: " + e.toString());
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
        }
    }
}

4. 优化使用方式,适配生命周期

val imageGetter = PicassoImageGetter(binding.blogContent, this)
val html: Spannable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, imageGetter, null) as Spannable
} else {
    Html.fromHtml(content, imageGetter, null) as Spannable
}
binding.blogContent.text = html

// 在页面销毁时清理资源
override fun onDestroy() {
    super.onDestroy()
    imageGetter.clear()
}

5. Kaolin适配说明

如果是在Kaolin UI框架中使用,需确保:

  • 使用Kaolin提供的Context或生命周期感知组件获取上下文,避免持有无效引用。
  • 若Kaolin使用Compose,可将ImageGetter逻辑适配为Compose支持的图文加载方式,或通过AndroidView包裹TextView进行兼容。

内容的提问来源于stack exchange,提问作者Dr Mido

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 14:01:13