安卓中WebView和原生的通信示例Demo(含完整源码)

本文详细介绍了WebView与原生Android应用之间的通信方式,包括原生调用JavaScript的loadUrl()和evaluateJavascript()方法,以及JavaScript调用原生的addJavascriptInterface()、shouldOverrideUrlLoading()和onJsPrompt()方法。提供了完整的Java和HTML代码示例,帮助开发者更好地理解和实现两者间的交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

所谓通信,说白了就是WebView和原生互相调用对方的函数、互相传值。WebView和原生通信的方式也很简单,不过最好是自己动手实现一下,才能够记得牢固。不管是面试还是笔试,都会经常考到。我也看过其他大佬的文章,不过有些地方没有给出必要的说明,让人看后还是不知道怎么实现,所以本文的最后会给出整个项目的源码,若有不清楚的地方,下载下来跑一遍就能够理解了。Demo的截图如下:
在这里插入图片描述

二、原生调用js

方法1:通过WebView对象的loadUrl()方法

loadUrl()方法不仅可以加载一个html网页,还可以调用js的方法,是不是很神奇呢!这里给出的例子是原生调用js的cat()方法,loadUrl()方式的缺点是不方便获取js方法的返回值,如果你给这个cat()方法返回一个值,你会发现html页面的body直接就展示这个返回值了,这显然不是你想要的结果吧。

原生java代码

mWebView.loadUrl("javascript:cat()");

html

let flag1 = true
function cat() {
            document.getElementById('div1').innerHTML = flag1 ? "小肥咪" : "大肥咪"
            flag1 = !flag1
        }

方法2:通过WebView对象的evaluateJavascript()方法

上面说了loadUrl()方式的缺点是不方便获取js方法的返回值,那么想要方便地获取js方法的返回值,可以使用evaluateJavascript()这个方法。

原生java代码

 mWebView.evaluateJavascript("javascript:sayHello()", new ValueCallback<String>() {
                    @Override
                    public void onReceiveValue(String value) {//可以在这个回调方法里面拿到js方法的返回值
                        Log.d(TAG, "onReceiveValue: "+value);
                    }
                });

html

let flag2 = true
 function sayHello() {
            document.getElementById("div2").innerHTML = flag2 ? "喵喵喵喵叫" : "嗷嗷叫"
            flag2 = !flag2
            return "我是js方法的返回值"
        }

三、js调用原生

js调用原生的话,这里介绍3种方式。

方法1:通过WebView对象的addJavascriptInterface()方法

这种方法其实就是用了对象映射,使用起来也不复杂。我们需要写一个类,在里面写上自己的方法,再把@JavascriptInterface注解加到你的方法上面。addJavascriptInterface()这个方法接收两个参数,第一个参数直接new出来一个刚才你自定义的类的对象;addJavascriptInterface()方法的第二个参数是一个字符串,是可以自定义的,这里我直接自定义为"android",其实也就是js里面用到的,作为一个对象名。

原生java代码

//自定义一个类
class MyNative{
        @JavascriptInterface
        public void getNative1(String who){
            Log.d(TAG, "getNative1: "+who);
            Toast.makeText(MainActivity.this, "js通过对象映射addJavascriptInterface的方式调用了原生的方法", Toast.LENGTH_SHORT).show();
        }
    }
MyNative myNative = new MyNative();//不要使用匿名内部类放入addJavascriptInterface中
//让js调用原生的方法
 mWebView.addJavascriptInterface(myNative,"android");//这里的android也可以写成别的字符串,待会js里面需要用到这个名字

html

下面的android对应的就是原生java代码里面你取的对象名

<button class="btn" onclick="window.android.getNative1('张三')">addJavascriptInterafce</button>

方法2:通过重写WebViewClient对象的shouldOverrideUrlLoading()方法

shouldOverrideUrlLoading()这个方法返回true,可以将url拦截(不打开新的网页)。在js里面调用的话,我们其实是需要写上一个url的。在url上可以拼接一些参数,传给原生使用。

原生java代码

mWebView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {//js调用原生的方式二
                Log.d(TAG, "shouldOverrideUrlLoading: ");

                Uri url = request.getUrl();
                if (url!=null){
                    String schemenName = url.getScheme();//约定好的协议名称,这里我约定成了myscheme,经测试,协议名需要完全为小写字母
                    if ("myscheme".equals(schemenName)){
                        String authority = url.getAuthority();//约定好的地址名
                        if ("myAddress".equals(authority)){
                            String userName = url.getQueryParameter("userName");//获取用户名参数
                            String password = url.getQueryParameter("password");//获取密码参数
                            Log.d(TAG, "shouldOverrideUrlLoading: 获取到js传过来的用户名userName为:"+userName+",密码为:"+password);
                            Toast.makeText(MainActivity.this, "通过重写shouldOverrideUrlLoading的方式让js调用原生的方法", Toast.LENGTH_SHORT).show();
                        }
                        return true;
                    }
                }
                return super.shouldOverrideUrlLoading(view,request);
            }
        });

html

 <button class="btn" onclick="document.location = 'myscheme://myAddress?userName=张三&password=123456'">shouldOverrideUrlLoading</button>

方法3: 通过重写WebChromeClient对象的onJsAlert()、onJsConfirm()或者onJsPrompt()方法

这种方式其实就是拦截js中的三种弹框,我觉得有点投机取巧,假设网页不是放在安卓中的WebView里面展示,直接从浏览器打开,那么弹框其实还是可以看到的。

原生java代码

mWebView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                return super.onJsAlert(view, url, message, result);
            }

            @Override
            public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                return super.onJsConfirm(view, url, message, result);
            }

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                Log.d(TAG, "onJsPrompt: ");
                Uri uri = Uri.parse(message);
                if (uri!=null){
                    String schemeName = uri.getScheme();//自定义的协议名称
                    if("myscheme".equals(schemeName)){
                        String authority = uri.getAuthority();
                        if ("myAddress".equals(authority)){//自定义的地址名
                            String userName = uri.getQueryParameter("userName");//获取js传过来的参数userName
                            String password = uri.getQueryParameter("password"); //获取js传过来的password
                            Toast.makeText(MainActivity.this, "通过拦截prompt弹框的方式让js调用原生的方法,原生获取到的userName为:"+userName+",password为:"+password, Toast.LENGTH_SHORT).show();
                        }
                        result.cancel();//这行要加上,不然onJsPrompt只会被调用一次,而且后续的js方法也会失效
                        return true;//返回true表示拦截弹框,只要符合约定的协议,就将js中的prompt弹框拦截,不让其弹出来
                    }
                }
                return super.onJsPrompt(view,url,message,defaultValue,result);
            }
        });

html

<button class="btn" onclick="showPromptDialog()">onJsPrompt</button>
function showPromptDialog() {
            prompt("myscheme://myAddress?userName=张三&password=123456")
        }

四、代码汇总

看了上面零零散散的代码,你是不是还不明白具体怎么调用?没关系,我相信一次性粘贴完所有的代码,你可以很轻松地理解。

4.1 MainActivity.java代码

package com.example.webviewtest;

import androidx.appcompat.app.AppCompatActivity;

import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    private WebView mWebView;
    private Button loadUrlBtn;
    private Button evaluateBtn;
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        initEvents();
    }

    private void initViews() {
        mWebView=findViewById(R.id.webView);
        mWebView.loadUrl("file:///android_asset/index.html");
        mWebView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {//js调用原生的方式二
                Log.d(TAG, "shouldOverrideUrlLoading: ");

                Uri url = request.getUrl();
                if (url!=null){
                    String schemenName = url.getScheme();//约定好的协议名称,这里我约定成了myscheme,经测试,协议名需要完全为小写字母
                    if ("myscheme".equals(schemenName)){
                        String authority = url.getAuthority();//约定好的地址名
                        if ("myAddress".equals(authority)){
                            String userName = url.getQueryParameter("userName");//获取用户名参数
                            String password = url.getQueryParameter("password");//获取密码参数
                            Log.d(TAG, "shouldOverrideUrlLoading: 获取到js传过来的用户名userName为:"+userName+",密码为:"+password);
                            Toast.makeText(MainActivity.this, "通过重写shouldOverrideUrlLoading的方式让js调用原生的方法", Toast.LENGTH_SHORT).show();
                        }
                        return true;
                    }
                }
                return super.shouldOverrideUrlLoading(view,request);
            }
        });
        mWebView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                return super.onJsAlert(view, url, message, result);
            }

            @Override
            public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                return super.onJsConfirm(view, url, message, result);
            }

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                Log.d(TAG, "onJsPrompt: ");
                Uri uri = Uri.parse(message);
                if (uri!=null){
                    String schemeName = uri.getScheme();//自定义的协议名称
                    if("myscheme".equals(schemeName)){
                        String authority = uri.getAuthority();
                        if ("myAddress".equals(authority)){//自定义的地址名
                            String userName = uri.getQueryParameter("userName");//获取js传过来的参数userName
                            String password = uri.getQueryParameter("password"); //获取js传过来的password
                            Toast.makeText(MainActivity.this, "通过拦截prompt弹框的方式让js调用原生的方法,原生获取到的userName为:"+userName+",password为:"+password, Toast.LENGTH_SHORT).show();
                        }
                        result.cancel();//这行要加上,不然onJsPrompt只会被调用一次,而且后续的js方法也会失效
                        return true;//返回true表示拦截弹框,只要符合约定的协议,就将js中的prompt弹框拦截,不让其弹出来
                    }
                }
                return super.onJsPrompt(view,url,message,defaultValue,result);
            }
        });
        WebSettings settings = mWebView.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setCacheMode(WebSettings.LOAD_NO_CACHE);//不使用缓存
        loadUrlBtn=findViewById(R.id.loadUrlBtn);
        evaluateBtn=findViewById(R.id.evaluateBtn);
    }

    private void initEvents(){

        //原生调用js的第一种方式:调用WebView的loadUrl()方法,会使得页面重新刷新一次,不方便获取js函数的返回值
        loadUrlBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWebView.loadUrl("javascript:cat()");
            }
        });

        //原生调用js的第二种方式,调用WebView的evaluateJavascript()方法
        evaluateBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWebView.evaluateJavascript("javascript:sayHello()", new ValueCallback<String>() {
                    @Override
                    public void onReceiveValue(String value) {//可以在这个回调方法里面拿到js方法的返回值
                        Log.d(TAG, "onReceiveValue: "+value);
                    }
                });
            }
        });


        MyNative myNative = new MyNative();//不要使用匿名内部类放入addJavascriptInterface中
        //让js调用原生的方法
        mWebView.addJavascriptInterface(myNative,"android");
    }


    class MyNative{
        @JavascriptInterface
        public void getNative1(String who){
            Log.d(TAG, "getNative1: "+who);
            Toast.makeText(MainActivity.this, "js通过对象映射addJavascriptInterface的方式调用了原生的方法", Toast.LENGTH_SHORT).show();
        }
    }
}

4.2 Html代码

<!DOCTYPE html>

<head>
    <title>首页</title>
    <script>
        let flag1 = true
        let flag2 = true

        //让原生调用的方法1
        function cat() {
            document.getElementById('div1').innerHTML = flag1 ? "小肥咪" : "大肥咪"
            flag1 = !flag1
        }

        //让原生调用的方法2
        function sayHello() {
            document.getElementById("div2").innerHTML = flag2 ? "喵喵喵喵叫" : "嗷嗷叫"
            flag2 = !flag2
            return "我是js方法的返回值"
        }


        function showPromptDialog() {
            prompt("myscheme://myAddress?userName=张三&password=123456")
        }
    </script>

    <style>
        .body {
            background-color: #c7edcc;
            margin: 0;
        }

        .container {
            height: 100vh;
            display: flex;
            flex-direction: column;
            margin-left: 6.0185vw;
            margin-right: 6.0185vw;
        }

        h1 {
            text-align: center;
        }

        .btnDiv {
            margin-top: 50px;
            display: flex;
            flex-direction: column;
        }

        .btn {
            height: 40px;
            margin-bottom: 10px;
        }
    </style>
</head>

<body class="body">
    <div class="container">
        <h1>我是WebView</h1>
        <div id="div1">从前有只猫</div>
        <div id="div2">奶牛猫好乖啊啊</div>
        <div class="btnDiv">
            <button class="btn" onclick="window.android.getNative1('张三')">addJavascriptInterafce</button>
            <button class="btn" onclick="document.location = 'myscheme://myAddress?userName=张三&password=123456'">shouldOverrideUrlLoading</button>
            <button class="btn" onclick="showPromptDialog()">onJsPrompt</button>
        </div>
    </div>
</body>

</html>

五、 总结

本篇文章介绍的通信方式共有5种,其中原生调用js有2种,js调用原生有3种。
整个项目的源码已经放到码云上了,点我跳转到码云
你可以将源码下载下来,在AS上跑一遍。

<think>嗯,用户想要了解如何在制作OpenStack镜像时使用cloud-init自动扩容根分区。这个问题听起来涉及到镜像制作cloud-init配置,可能用户需要为云环境准备自定义镜像,并确保实例启动时根分区能自动扩展。首先,我需要回忆一下相关步骤,确保没有遗漏关键点。 首先,制作OpenStack镜像通常需要使用工具比如virt-manager、qemu-img等,或者直接从现有虚拟机导出镜像。而自动扩容根分区的部分,我记得cloud-init有一个growpart的模块,可以处理分区扩展。不过可能需要确认镜像中的分区表类型,比如是MBR还是GPT,以及文件系统是否支持resize,比如ext4或者xfs。 接下来,用户可能需要安装cloud-init到镜像中,并配置正确的数据源,因为OpenStack通常使用ConfigDrive或者Metadata服务。在配置cloud-init时,需要确保启用了growpartfs_resize模块。此外,镜像的磁盘布局也很重要,根分区必须是最后一个分区,这样growpart才能正确扩展。如果有交换分区或者其他分区在根分区之后,可能会导致问题。 然后,可能需要修改grub配置,确保内核参数中包rootflags=discard,这样文件系统扩展后可以释放空间。另外,检查cloud-init的配置文件,比如/etc/cloud/cloud.cfg,确保有正确的配置,比如datasource列表里包OpenStack,以及cloud_init_modules里包growpartresizefs。 用户可能遇到的常见问题包括:分区没有正确扩展,可能是因为分区不是最后一个,或者文件系统不支持在线调整大小。还有可能是cloud-init没有正确安装或配置,导致模块没有执行。此外,镜像本身可能需要安装必要的工具,比如cloud-utils、growpart或者dracut-modules-growroot,具体取决于发行版。 还有一点需要考虑的是,不同Linux发行版的处理方式可能不同。例如,CentOSUbuntu在cloud-init的配置包管理上可能有差异,需要分别处理。此外,如果使用LVM,步骤会有所不同,可能需要调整自动扩展的逻辑。 最后,测试镜像的步骤也很重要。用户可能需要上传镜像到OpenStack,启动实例,检查cloud-init日志,确认分区是否扩展成功。可能需要查看/var/log/cloud-init.log来调试问题。 总结来说,步骤大致包括:准备基础镜像,安装配置cloud-init,调整分区文件系统配置,验证并测试。需要确保每个环节都正确无误,才能实现自动扩容根分区的目标。</think>制作OpenStack镜像时,通过`cloud-init`实现根分区自动扩容需要以下步骤: --- ### 一、基础镜像准备 1. **选择基础镜像** 建议使用官方支持的云镜像(如Ubuntu Cloud、CentOS Generic Cloud),这类镜像已预装`cloud-init`并针对云环境优化。 2. **验证分区类型** - 使用`fdisk -l`确认根分区是磁盘的最后一个分区(自动扩容仅支持扩展最后一个分区) - 推荐使用`GPT`分区表(支持大容量磁盘)或`MBR`+`extended`分区 --- ### 二、安装配置cloud-init 1. **安装依赖** ```bash # Ubuntu/Debian apt install cloud-init cloud-utils growpart # CentOS/RHEL yum install cloud-init cloud-utils-growpart dracut-modules-growroot ``` 2. **配置数据源** 修改`/etc/cloud/cloud.cfg`,确保包OpenStack数据源: ```yaml datasource_list: [ ConfigDrive, OpenStack ] ``` 3. **启用自动扩容模块** 在`cloud-init`配置中激活以下模块: ```yaml cloud_init_modules: - growpart - resizefs ``` --- ### 三、分区与文件系统调整 1. **禁用交换分区(可选)** 如果交换分区位于根分区之后,建议删除或迁移,否则会阻碍扩容: ```bash swapoff -a && rm -f /swapfile ``` 2. **修改GRUB配置** 确保内核参数允许在线扩容: ```bash sed -i 's/GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 rootflags=discard"/' /etc/default/grub update-grub # Ubuntu/Debian grub2-mkconfig -o /boot/grub2/grub.cfg # CentOS/RHEL ``` 3. **重建initramfs** ```bash dracut -f # CentOS/RHEL update-initramfs -u # Ubuntu/Debian ``` --- ### 四、验证与镜像生成 1. **清理镜像** ```bash cloud-init clean rm -rf /var/log/cloud-init* ``` 2. **转换为OpenStack格式** ```bash qemu-img convert -f qcow2 -O raw input.qcow2 output.raw ``` 3. **上传镜像** 通过OpenStack Dashboard或CLI上传镜像,指定`hw_disk_bus=virtio``hw_scsi_model=virtio-scsi`属性。 --- ### 五、测试实例 1. **启动实例时指定磁盘大小** ```bash openstack server create --flavor m1.small --image my-image --block-device source=image,dest=volume,size=20,shutdown=preserve,bootindex=0 instance-01 ``` 2. **验证扩容结果** ```bash # 检查分区 lsblk # 查看文件系统 df -Th / # 查看cloud-init日志 tail -f /var/log/cloud-init.log ``` --- ### 六、故障排查 1. **常见问题** - **分区未扩展**:检查是否为最后一个分区,确认`growpart``resizefs`模块执行成功 - **文件系统未调整**:确保文件系统类型支持在线扩容(ext4/xfs) - **内核参数缺失**:验证`rootflags=discard`是否生效 2. **手动触发扩容** ```bash growpart /dev/vda 1 resize2fs /dev/vda1 # ext4 xfs_growfs / # xfs ``` --- 通过以上步骤,可在OpenStack实例启动时自动完成根分区扩容,适应不同规格的云主机需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值