Android WebView实现图片视频分享报主线程调用错误问题求解
问题原因
你遇到的Method call should not happen from the main thread报错本质是线程调用违规,核心触发点有两个:
- 所有被
@JavascriptInterface注解标记的方法,默认运行在WebView专属的JS后台线程,并非Android主线程,直接在这个线程里调用涉及UI、系统组件的操作本身就违反Android单线程模型规则 - 你在Picasso的加载回调里直接调用了
MediaStore.Images.Media.insertImage和startActivity,这两个API强制要求在主线程调用,后台线程触发就会直接抛出你看到的异常 - 补充说明:
navigator.shareAPI确实不支持Android系统WebView环境,你通过JavascriptInterface桥接原生分享能力的思路是可行的,只是线程处理逻辑存在问题。
另外你的代码还存在一个隐藏bug:Picasso对Target是弱引用持有,你把Target定义为方法内局部变量,很容易被GC回收,导致分享回调不触发。
修复方案
核心调整点
- 所有涉及UI操作、MediaStore写入、Activity启动的逻辑,必须切到主线程执行
- 将
Target改为全局强引用持有,避免被GC回收导致回调失效 - 给分享Intent添加临时读权限标记,避免第三方应用无法读取待分享文件
- 适配Android版本权限差异:Android 10及以上版本不需要读写外置存储权限即可写入MediaStore,无需在高版本申请冗余权限
修正后的核心代码
public class Principal extends AppCompatActivity { private WebView mBibouView; // 全局持有Target,避免被GC回收 private Target shareTarget; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_principal); mBibouView=findViewById(R.id.bibouview); WebSettings webSettings = mBibouView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setDomStorageEnabled(true); webSettings.setAllowFileAccess(true); webSettings.setAllowContentAccess(true); webSettings.setJavaScriptCanOpenWindowsAutomatically(true); mBibouView.setWebViewClient(new WebViewClient()); mBibouView.addJavascriptInterface(new JavascriptShareInterface(), "AndroidShareHandler"); mBibouView.loadUrl("https://www.myadress.fr"); } public class JavascriptShareInterface { @JavascriptInterface public void share(String url){ // JS接口运行在后台线程,切主线程执行后续操作 runOnUiThread(() -> { shareTarget = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { // Picasso主线程回调,可直接调用系统API String path = MediaStore.Images.Media.insertImage(getContentResolver(), bitmap, "ShareImage", null); if (path == null) { Log.e("Share", "Write image to MediaStore failed"); return; } Uri shareUri = Uri.parse(path); Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("image/*"); shareIntent.putExtra(Intent.EXTRA_TEXT, "Hey view/download this image"); shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri); // 给第三方应用授予临时文件读权限 shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, "Share image via...")); } @Override public void onBitmapFailed(Drawable errorDrawable) { Log.e("Share", "Load image from url failed"); } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { // 可在此处添加加载中提示 } }; Picasso.with(Principal.this) .load(url) .into(shareTarget); }); } } @Override public void onBackPressed(){ mBibouView.evaluateJavascript("document.getElementById('div_form').style.display='none';",null); mBibouView.evaluateJavascript("document.getElementById('div_menu').style.display='none';",null); } }
额外优化建议
- 你当前使用的是旧版Picasso(
Picasso.with()初始化),建议升级到最新版本使用Picasso.get()初始化,避免老版本存在的线程调度bug - 视频分享逻辑和图片基本一致,只需要将MIME类型替换为
video/*,对应加载视频资源写入MediaStore即可 - 清单文件中的读写外置存储权限,可以添加
android:maxSdkVersion="28"配置,在Android 10及以上版本自动跳过该权限申请,减少权限弹窗
内容的提问来源于stack exchange,提问作者damii1
相关产品推荐
相关产品推荐

