• 前言 uni-app是一个开源前端...而Native.js是使用uni-app开发时可以用到的一种开发技术,以下介绍摘自《5+ App开发Native.js入门指南》:http://ask.dcloud.net.cn/article/88 本文只是对上面这个文章的一些摘抄和...

    前言

    uni-app是一个开源前端框架,是用vue.js开发的,可以实现跨平台的开发。地址:https://github.com/dcloudio/uni-app
    而Native.js是使用uni-app开发时可以用到的一种开发技术,以下介绍摘自《5+ App开发Native.js入门指南》:http://ask.dcloud.net.cn/article/88
    本文只是对上面这个文章的一些摘抄和提炼,方便Android开发者参考。

    Native.js技术,简称NJS,是一种将手机操作系统的原生对象转义,映射为JS对象,在JS里编写原生代码的技术。
    NJS编写的代码,最终需要在HBuilder里打包发行为App安装包,或者在支持Native.js技术的浏览器里运行。目前Native.js技术不能在普通手机浏览器里直接运行。

    • NJS大幅扩展了HTML5的能力范围,原本只有原生或Hybrid App的原生插件才能实现的功能如今可以使用JS实现。
    • NJS大幅提升了App开发效率,将iOS、Android、Web的3个工程师组队才能完成的App,变为1个web工程师就搞定。
    • NJS不再需要配置原生开发和编译环境,调试、打包均在HBuilder里进行。没有mac和xcode一样可以开发iOS应用。

    再次强调,Native.js不是一个js库,不需要下载引入到页面的script中,也不像nodejs那样有单独的运行环境,Native.js的运行环境是集成在5+runtime里的,使用HBuilder打包的app或流应用都可以直接使用Native.js。

    简言之,Native.js可以实现用js语法来调用Android或者ios的功能,以实现一处开发,多处运行的目的。使用HBuilder这个开发工具完成功能开发后,需要把打包数据放入Android项目或者ios项目里,然后就可以编译得到Android或者ios安装包。
    HBuilder工具下载地址:https://www.dcloud.io/hbuilderx.html

    本文主要介绍Native.js如何配合Android开发。

    简单示例

    以一个简单的例子说明一下Native.js的使用:
    如果Android 项目里有个工具类ToastUtil.java是用来管理Toast的,其中有个静态方法用来显示Toast:

    package com.example.demo;
    public static void show(Context ctx, CharSequence msg, int duration) {
    	XXX
    }
    

    在Android项目里,如果是这样用的:

    ToastUtil.show(MainActivity.this, "提醒一下", Toast.LENGTH_LONG);
    

    对应到了native.js里,写法就是这样:

    var ToastUtil = plus.android.importClass("com.example.demo.ToastUtil");
    var Toast = plus.android.importClass("android.widget.Toast");
    ToastUtil.show(plus.android.runtimeMainActivity(), "提醒一下", Toast.LENGTH_LONG);
    

    其中plus.android.importClass是用来导入类的,然后由于ToastUtil类的show方法是静态方法,Toast类的LENGTH_LONG是静态常量,所以都是可以用类后面跟点号来使用的。plus.android.runtimeMainActivity()是用来获取当前的activity。详细语法后面会讲,这里先大概有个印象。

    当然上面的写法如果运行在ios平台上就会出错了,所以在native.js里,得针对不同操作系统做判断:

    switch ( plus.os.name ) {  
        case "Android":  
            // Android平台: plus.android.*  
            var ToastUtil = plus.android.importClass("com.example.demo.ToastUtil");
    		var Toast = plus.android.importClass("android.widget.Toast");
    		ToastUtil.show(plus.android.runtimeMainActivity(), "提醒一下", Toast.LENGTH_LONG);
            break;  
        case "iOS":  
            // iOS平台: plus.ios.*  
            break;  
        default:  
            // 其它平台  
            break;  
        }  
    

    如果还有兴趣继续看下去的话,接下来就是详细讲解了。


    用法详解

    以一个更全面的例子来讲解,下面是一个测试类:

    package io.dcloud;  
    
    // 定义类NjsHello  
    public class NjsHello {  
        // 静态常量  
        public static final int CTYPE = 1;  
        // 静态变量  
        public static int count;  
        // 成员常量  
        public final String BIRTHDAY = "2013-01-13";  
        // 成员变量  
        String name; //定义属性name  
        NjsHelloEvent observer;  
        public void updateName( String newname ) { //定义方法updateName  
            name = newname;  
        }  
        public void setEventObserver( NjsHelloEvent newobserver ) {  
            observer = newobserver;  
        }  
        public void test() { //定义方法test  
            System.out.printf( "My name is: %s", name );  
            observer.onEventInvoked( name );  
        }  
        public static void testCount() {  
            System.out.printf( "Static count is:%d", count );  
        }  
        static{  // 初始化类的静态变量  
            NjsHello.count = 0;  
        }  
    }  
    

    这是上面的代码中用到的接口类:

    package io.dcloud;  
    
    // 定义接口NjsHelloEvent  
    public interface NjsHelloEvent {  
        public void onEventInvoked( String name );  
    }  
    

    下面看如何在native.js中使用这个测试类:

    · 导入类后获取类的静态常量属性

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 获取类的静态常量属性  
    var type = NjsHello.CTYPE;  
    

    · 导入类后调用类的静态方法

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 调用类的静态方法  
    NjsHello.testCount();  
    

    · 导入类后获取类的静态变量值

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 获取类的静态变量 
    var count = NjsHello.plusGetAttribute( "count" );  
    console.log( "NjsHello Static's value: "+count ); // 输出“NjsHello Static's value: 0”  
    

    · 导入类后设置类的静态属性值

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 设置类的静态属性值  
    NjsHello.plusSetAttribute( "count", 2 );  
    console.log( "NjsHello Static's value: "+NjsHello.plusGetAttribute( "count" ) ); // 输出“NjsHello Static's value: 2”  
    

    · 导入类创建实例对象,调用对象的方法

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 创建NjsHello的实例对象  
    var hello = new NjsHello();  
    // 调用对象的方法  
    hello.updateName( "Tester" );  
    // ...  
    

    · 导入类创建实例对象,获取对象的常量属性

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 创建NjsHello的实例对象  
    var hello = new NjsHello();  
    // 访问对象的常量属性  
    var birthday = hello.BIRTHDAY;  
    console.log( "NjsHello Object Final's value: "+birthday ); // 输出“NjsHello Object Final's value: 2013-01-13”  
    // ...  
    

    · 导入类创建实例对象,获取对象的属性值

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 创建对象的实例  
    var hello = new NjsHello();  
    hello.updateName( "Tester" );  
    // 获取其name属性值  
    var name = hello.plusGetAttribute( "name" );  
    console.log( "NjsHello Object's name: "+name );  // 输出“NjsHello Object's name: Tester”  
    // ...  
    

    · 导入类创建实例对象,设置对象的属性值

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("NjsHello");  
    // 创建对象的实例  
    var hello = new NjsHello();  
    // 设置其name属性值  
    hello.plusSetAttribute( "name", "Tester" );  
    console.log( "NjsHello Object's name: "+hello.plusGetAttribute("name") ); // 输出“NjsHello Object's name: Tester”  
    // ...  
    

    · 实现接口,并触发接口中函数的运行

    // 导入测试类NjsHello  
    var NjsHello = plus.android.importClass("io.dcloud.NjsHello");  
    // 实现接口“NjsHelloEvent”对象  
    var hevent = plus.android.implements( "io.dcloud.NjsHelloEvent", {  
    "onEventInvoked":function( name ){  
        console.log( "Invoked Object’s name: "+name ); // 输出“Invoked Object’s name: Tester”  
    }  
    } );  
    // 创建对象的实例  
    var hello = new NjsHello();  
    // 调用updateName方法  
    hello.updateName( "Tester" );  
    // 设置监听对象  
    hello.setEventObserver( hevent );  
    // 调用test方法,触发代理事件  
    hello.test(); // 触发上面定义的匿名函数运行  
    // ...  
    

    高级用法

    有前述的常用API,已经可以完成各项业务开发。此处补充的高级API,是在熟悉NJS后,为了提升性能而使用的API。高级API无法直接用“.”操作符使用原生对象的方法,在debug时也无法watch原生对象,但高级API性能高于常规API。
    虽然导入类对象(plus.android.importClass和plus.ios.importClass)后,可以方便的通过“.”操作符来访问对象的常量、调用对象的方法,但导入类对象也需要消耗较多的系统资源,所以在实际开发时应该尽可能的减少导入类对象,以提高程序效率。

    如果我们不导入类对象则无法通过new操作符实例化类对象,这时可通过plus.ios.newObject()、plus.android.newObject()方法来创建实例对象。

    注意:由于没有导入类对象,所以通过此方法创建的实例对象无法通过“.”操作符直接调用对象的方法,而必须使用plus.android.invoke方法来调用。

    · 不导入类创建实例对象

    // 不调用plus.android.importClass("io.dcloud.NjsHello")导入类NjsHello  
    // 创建对象的实例  
    var hello = plus.android.newObject( "io.dcloud.NjsHello" );  
    // ...  
    

    · 不导入类对象获取类的静态常量属性

    // 不调用plus.android.importClass("io.dcloud.NjsHello")导入类NjsHello  
    // 访问类的静态常量属性  
    var type = plus.android.getAttribute( "io.dcloud.NjsHello", "CTYPE" );  
    console.log( "NjsHello Final's value: "+type ); // 输出“NjsHello Final's value: 1”  
    // ...  
    

    · 不导入类对象,创建实例对象,并获取其name属性值

    // 不调用plus.android.importClass("io.dcloud.NjsHello")导入类NjsHello  
    // 创建对象的实例  
    var hello = plus.android.newObject( "io.dcloud.NjsHello" );  
    // 获取其name属性值  
    var name = plus.android.getAttribute( hello, "name" );  
    console.log( "NjsHello Object's name: "+name );  // 输出“NjsHello Object's name: Tester”  
    // ...  
    

    · 不导入类对象设置类的静态属性值

    // 不调用plus.android.importClass("io.dcloud.NjsHello")导入类NjsHello  
    // 设置类的静态属性值  
    plus.android.setAttribute( "io.dcloud.NjsHello", "count", 2 );  
    console.log( "NjsHello Static's value: "+plus.android.getAttribute("io.dcloud.NjsHello","count") ); // 输出“NjsHello Static's value: 2”  
    // ...  
    

    · 不导入类对象,创建实例对象,并设置其name属性值

    // 不调用plus.android.importClass("io.dcloud.NjsHello")导入类NjsHello  
    // 创建对象的实例  
    var hello = plus.android.newObject( "io.dcloud.NjsHello" );  
    // 设置其name属性值  
    plus.android.setAttribute( hello, "name", "Tester" );  
    console.log( "NjsHello Object's name: "+hello.plusGetAttribute("name") ); // 输出“NjsHello Object's name: Tester”  
    // ...  
    

    · 不导入类对象,调用类的静态方法

    // 不调用plus.android.importClass("io.dcloud.NjsHello")导入类NjsHello  
    // 调用类的静态方法  
    plus.android.invoke( "io.dcloud.NjsHello", "testCount" );  
    // ...  
    

    · 不导入类对象,创建实例对象,并调用其updateNmae方法

    在这里插入代码片// 不调用plus.android.importClass("io.dcloud.NjsHello")导入类NjsHello  
    // 创建对象的实例  
    var hello = plus.android.newObject( "io.dcloud.NjsHello" );  
    // 调用updateName方法  
    plus.android.invoke( hello, "updateName", "Tester" );  
    console.log( "NjsHello Object's name: "+hello.getAttribute("name") ); // 输出“NjsHello Object's name: Tester”  
    // ...  
    

    关于Native.js调用其它Android系统功能,可以看这里:https://ask.dcloud.net.cn/article/114

    本文是从这篇讲解里提炼出来的,如果有问题,可以查看原文:
    http://ask.dcloud.net.cn/article/88

    展开全文
  • uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉)等多个平台官网地址 因为业务逻辑需要调用短信功能,但是uni-...

    uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉)等多个平台官网地址
    因为业务逻辑需要调用短信功能,但是uni-app 中未提供相关接口。查看uni-app的框架图可以看出
    是可以使用native.js去调用的android API的
    在这里插入图片描述
    native.js 介绍及文档:https://ask.dcloud.net.cn/article/88

    于是乎,在百度上搜索了 android 发送短信的 代码 将其改写成native.js调用
    原android代码

    public void sendMessages(String sPhoneNum, String sMesage, int id){ 
    SubscriptionInfo sInfo = null; final SubscriptionManager sManager = (SubscriptionManager)this .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); 
    List<SubscriptionInfo> lists = sManager.getActiveSubscriptionInfoList(); 
    if(lists == null || lists.isEmpty()){ 
    	return; 
    }
    sInfo = lists.get(id); 
    if(sInfo != null){ 
    int subId = sInfo.getSubscriptionId(); 
    SmsManager managers = SmsManager.getSmsManagerForSubscriptionId(subId); 			 	
    ArrayList<String> arrayListlist = managers.divideMessage(sMesage);
    managers.sendMultipartTextMessage(sPhoneNum,null,arrayLis tlist,null,null); 
    	} 
    }
    

    native.js代码:
    //通过native.js 调用android API 发送短信

    sendMessage1() {
     if (plus.os.name == "Android") {					
    	//获取当前MainActivity
    	var main = plus.android.runtimeMainActivity();
    	//获取SystemService
    	var sManager = main.getSystemService("telephony_subscription_service");
    	console.log(sManager);
    	//获取所有卡列表
    	var subscriptionInfoList = plus.android.invoke(sManager,"getActiveSubscriptionInfoList");
    	//获取卡信息   
    	var sInfo = plus.android.invoke(subscriptionInfoList,"get",0);
    	//获取卡id
    	var id = plus.android.invoke(sInfo,"getSubscriptionId");
    	console.log(id);
    	//获取卡id获取SMSManagers对象
    	var managers = plus.android.invoke("android.telephony.SmsManager","getSmsManagerForSubscriptionId",id);
    	var eMessage = "哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈--------------";
    	//分割短信内容 短信规定不能大于70字,大于70会被分割,分开发送
    	var list = plus.android.invoke(managers,"divideMessage",eMessage);
    	//发送短信
    	var returns = plus.android.invoke(managers,"sendMultipartTextMessage","18829221189",null,list,null,null);		
    }
    

    另附一种解决方案 使用H5+掉起系统的短信发送界面

    // 通过 h5+ 掉起 系统短信发送界面
    			sendMessage2() {
    				//创建一条信息  短信/彩信/邮件
    				var msg = plus.messaging.createMessage(plus.messaging.TYPE_SMS); 
    					msg.to = ['18829221189'];  //发给谁
    					msg.body = '艾欧尼亚昂扬不灭';
    					// msg.silent = true;  静默发送(后台发送) 不好使
    					//发送信息
    					plus.messaging.sendMessage(msg,
    					function (){
    						uni.showModal({
    							content: 'successs',
    							showCancel: false
    						});
    					},function(){
    						uni.showModal({
    							content: 'error',
    							showCancel: false
    						});
    					});
    
    展开全文
  • 前言 参考来源 ...前人栽树,后台乘凉,本文参考了以下来源 ...JSBridge-Web与Native交互之iOS篇Ios Android Hybrid app 与 Js BridgeHybrid APP架构设计思路Android4.2下 WebView的addJavascriptInter

    http://www.cnblogs.com/liubei/p/Hybrid.html


    前言

    参考来源

    前人栽树,后台乘凉,本文参考了以下来源

    前置技术要求

    阅读本文前,建议先阅读以下文章

    楔子

    Hybrid APP的关键是原生页面与H5页面直接的交互,本文做简单介绍

    Android、iOS原生和H5的基本通信机制

    在Hybrid APP中,原生与H5的交互方式在Android和iOS上的实现是有异同的,原因是Android、iOS的通信机制有所区别,下面介绍原生和H5相互调用的方法

    Android端

    Native调JS

    4.4版本之前

    // mWebView = new WebView(this); //即当前webview对象			
    mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')"); 
    
    //ui线程中运行
     runOnUiThread(new Runnable() {  
            @Override  
            public void run() {  
                mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");  
                Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();  
            }  
    });  
    			

    4.4以后(包括4.4)

    //异步执行JS代码,并获取返回值	
    mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
            @Override
            public void onReceiveValue(String value) {
        		//这里的value即为对应JS方法的返回值
            }
    });
    			

    如上所示,Native用H5页面中的JS方法,有如下特点

    • 4.4之前Native通过loadUrl来调用JS方法,只能让某个JS方法执行,但是无法获取该方法的返回值
    • 4.4之后,通过evaluateJavascript异步调用JS方法,并且能在onReceiveValue中拿到返回值
    • 不适合传输大量数据(大量数据建议用接口方式获取)
    • mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");函数需在UI线程运行,因为mWebView为UI控件(但是有一个坏处是会阻塞UI线程)

    JS调Native

     WebSettings webSettings = mWebView.getSettings();  
     //Android容器允许JS脚本,必须要
    webSettings.setJavaScriptEnabled(true);
    //Android容器设置侨连对象
    mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
    			

    Android中JSBridge的代码

    //Android4.2版本以上,本地方法要加上注解@JavascriptInterface,否则会找不到方法。
    private Object getJSBridge(){  
        Object insertObj = new Object(){  
        	@JavascriptInterface
            public String foo(){  
                return "foo";  
            }  
            
            @JavascriptInterface
            public String foo2(final String param){  
                return "foo2:" + param;  
            }  
              
        };  
        return insertObj;  
    }  
    			

    Html中JS调用原生的代码

    //调用方法一
    window.JSBridge.foo(); //返回:'foo'
    //调用方法二
    window.JSBridge.foo2('test');//返回:'foo2:test'
    			

    如上所示,Native中通过addJavascriptInterface添加暴露出来的JS桥对象,然后再该对象内部声明对应的API方法,有如下特点:

    • 在Android4.2以上(api17后),暴露的api要加上注解@JavascriptInterface,否则会找不到方法。
    • 在api17以前,addJavascriptInterface有风险,hacker可以通过反编译获取Native注册的Js对象,然后在页面通过反射Java的内置 静态类,获取一些敏感的信息和破坏

      所以,也就是为什么Android中也会使用JSBridge来进行交互,而不是addJavascriptInterface直接暴露api

    • JS能调用到已经暴露的api,并且能得到相应返回值

    iOS端

    Native调JS

    //可以取得JS函数执行的返回值
    //方法必须是Html页面绑定在最顶层的window上对象的
    //如window.top.foo
    //Swift
    webview.stringByEvaluatingJavaScriptFromString("方法名(参数)")
    //OC
    [webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];
    			

    如上所示,Native通过stringByEvaluatingJavaScriptFromString调用Html绑定在window上的函数,有如下特点

    • Native调用JS方法时,能拿到JS方法的返回值
    • 不适合传输大量数据(大量数据建议用接口方式获取)

    JS调Native

    引入官方的库文件

    #import <JavaScriptCore/JavaScriptCore.h>

    Native注册api函数(OC)

    //webview加载完毕后设置一些js接口
    -(void)webViewDidFinishLoad:(UIWebView *)webView{
        [self hideProgress];
        [self setJSInterface];
    }
    
    -(void)setJSInterface{
        
        JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        
        // 注册名为foo的api方法
        context[@"foo"] = ^() {
        	
        	//获取参数
            NSArray *args = [JSContext currentArguments];
            NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
            //做一些自己的逻辑
            //返回一个值  'foo:'+title
            return [NSString stringWithFormat:@"foo:%@", title];
        };
        
    
        
    }				
    			

    Html中JS调用原生的代码

    //调用方法,用top是确保调用到最顶级,因为iframe要用top才能拿到顶级
    window.top.foo('test'); //返回:'foo:test'
    			

    如上所示,Native中通过引入官方提供的JavaScriptCore库(iOS7中出现的),然后可以将api绑定到JSContext上(然后Html中JS默认通过window.top.***可调用)。有如下特点

    • iOS7才出现这种方式,在这之前,js无法直接调用Native,只能通过JSBridge方式简介调用
    • JS能调用到已经暴露的api,并且能得到相应返回值
    • iOS原生本身是无法被JS调用的,但是通过引入官方提供的第三方"JavaScriptCore",即可开放api给JS调用

    原生和H5的另一种通讯方式:JSBridge

    实际上,Native与H5通信,除了前面提到的用基本方法外,还有一种广为流行的方法:JSBridge

    什么是JSBridge

    JSBridge是广为流行的Hybrid开发中JS和Native一种通信方式,各大公司的应用中都有用到这种方法

    简单的说,JSBridge就是定义Native和JS的通信,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用Native,基本原理是:

    H5->通过某种方式触发一个url->Native捕获到url,进行分析->原生做处理->Native调用H5的JSBridge对象传递回调。如下图

    上图简单的介绍了下JSBridge的核心原理,具体详细实现请参考后面详解。

    为什么要用JSBridge

    在上文中我们有提到Native和原生之间的基本通信,既然Native和原生已经能够实现通信了,那为什么还要这种通过url scheme的JSBridge方式呢,原因大致如下

    • Android4.2以下,addJavascriptInterface方式有安全漏掉
    • iOS7以下,JS无法调用Native
    • url scheme交互方式是一套现有的成熟方案,可以完美兼容各种版本,不存在上述问题

    另外,请注意,可以理解为JSBridge是一种交互理念,而上述的url scheme则是其中的一种实现,所以也就是说,就算后面实现变为了addJavascriptInterface,JavaScriptCore,也一样是JSBridge交互

    JSBridge交互的一个很大特点就是便于拓展,而且没有重大的安全性问题,所以也就是为什么它广为流行

    JSBridge原理以及实现

    JSBridge的原理和实现请参考 JSBridge实现原理

    展开全文
  • Hybrid App 使用 native.js 实现搜索蓝牙列表

    Hybrid App 使用 native.js 实现搜索蓝牙列表


    HTML
    <div class="mui-content">
                <button type="button" class="mui-btn mui-btn-blue mui-btn-block" id="bt1">搜索设备</button>
                <div class="mui-content-padded">
                    未配对蓝牙设备
                    <ul class="mui-table-view" id="list1">
                    </ul>
                </div>
    
            </div>
    JS
    mui.plusReady(function() {
        var ulList = '';
        var main = plus.android.runtimeMainActivity();
        var IntentFilter = plus.android.importClass('android.content.IntentFilter');
        var BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter");
        var BluetoothDevice = plus.android.importClass("android.bluetooth.BluetoothDevice");
        var BAdapter = BluetoothAdapter.getDefaultAdapter();
        plus.android.importClass(BAdapter);
        console.log("BAdapter.isEnabled():" + BAdapter.isEnabled());
        if(!BAdapter.isEnabled()) {
            BAdapter.enable();
        }
    
        var filter = new IntentFilter();
        var bdevice = new BluetoothDevice();
        var on = null;
        var un = null;
        var vlist1 = document.getElementById('list1'); //注册容器用来显示未配对设备
        vlist1.innerHTML = ''; //清空容器
        var button1 = document.getElementById('bt1');
        button1.disabled = true;
        button1.innerHTML = '正在搜索请稍候';
        BAdapter.startDiscovery(); //开启搜索
        var receiver;
        receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
            onReceive: function(context, intent) { //实现onReceiver回调函数
                plus.android.importClass(intent); //通过intent实例引入intent类,方便以后的‘.’操作
                console.log(intent.getAction()); //获取action
                if(intent.getAction() == "android.bluetooth.adapter.action.DISCOVERY_FINISHED") {
                    main.unregisterReceiver(receiver); //取消监听
                    button1.disabled = false;
                    button1.innerHTML = '搜索设备';
                    console.log("搜索结束");
                } else if(intent.getAction() == "android.bluetooth.adapter.action.DISCOVERY_STARTED") {
                    console.log("开始搜索设备");
                } else {
                    BleDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                    //判断是否配对
                    console.log("蓝牙设备:" + BleDevice.getName() + '    ' + BleDevice.getAddress());
                    var li = document.createElement('li');
                    li.setAttribute('class', 'mui-table-view-cell');
                    var p1 = document.createElement("p");
                    p1.appendChild(document.createTextNode('名称:' + BleDevice.getName()));
                    var p2 = document.createElement("p");
                    p2.appendChild(document.createTextNode('地址:' + BleDevice.getAddress()));
                    li.appendChild(p1);
                    li.appendChild(p2);
                    vlist1.appendChild(li);
                }
            }
        });
    
        filter.addAction(bdevice.ACTION_FOUND);
        filter.addAction(BAdapter.ACTION_DISCOVERY_STARTED);
        filter.addAction(BAdapter.ACTION_DISCOVERY_FINISHED);
        filter.addAction(BAdapter.ACTION_STATE_CHANGED);
    
        main.registerReceiver(receiver, filter); //注册监听
    });
    展开全文
  • 使用NativeActivity可以完全不使用java代码,全部使用native code来开发android程序。NativeActivity 为我们定制了native...我们会发现,demo中使用了一个胶水层“android_native_app_glue.h”封装了native层面的ANativ

        使用NativeActivity可以完全不使用java代码,全部使用native code来开发android程序。NativeActivity 为我们定制了native代码的各种接口回调,在ndk的samples里面,提供了一个例子如何使用NativeActivity。我们会发现,demo中使用了一个胶水层android_native_app_glue.h封装了native层面的ANativeActivity的事件回调。


        在native层面ANativeActivity对应的就是,java层面的NativeActivity。通过NativeActivity的java会代码会看到,ANativeActivity的回调函数都是在NativeActivity中被调用的。android_native_app_glue.c 主要做了以下几件事情。

    • 实现ANativeActivity启动函数,挂载ANativeActivity的事件回调函数。包括了事件处理,窗口生命周期等等。
    • 启动一个独立的线程,给使用者传递事件,和实现绘制。
    • 使用ALooper重新定义和实现了事件类型,让使用者实现回调处理。
        在整个android_native_app_glue代码的核心,就是如何使用ALooper串联起主线程事件,输入输出事件的。主线程事件,就是指在NativeActivity的回调事件。输入输出事件,就是input事件。


        简单介绍一个下ALooper,通过阅读java和c的定义代码。我的理解是,Looper可以把其它线程的事件放到队列里面,以后再一个线程里面独立处理。比如,OpenGL的调用就需要和Context在一个线程里面,就是典型的使用Looper的场景。我们从主线程拿到事件,然后在openGL的绘制线程里,处理looper收集的事件,这样事件处理和绘制就会在同一个线程里面了。


        从android/input.h和android/sensor.h我们可以了解到,要使用系统的输入输出和重力感应事件,我们需要传入Looper对象指针。在android/looper.h中可以了解到Looper的使用方式,每个线程可以附加一个Looper。我们在线程代码里看到下面的代码。
        
        ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
        ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL,
                &android_app->cmdPollSource);

    android_native_app_glue 使用Looper的方式是非回调模式,回调函数传入NULL。就是Looper负责收集和释放事件,事件的处理需要自己调用。当然也有回调模式。ALooper_addFd 就是向Looper注册事件处理的方法。fd从何而来呢,如下:
        int msgpipe[2];
        if (pipe(msgpipe)) {
            LOGE("could not create pipe: %s", strerror(errno));
            return NULL;
        }
        android_app->msgread = msgpipe[0];
        android_app->msgwrite = msgpipe[1];

    就是利用管道的读写句柄来进行数据通信的。当ANativeActivity的事件在主线程触发的时候,向msgwrite写入了一个自定义的数据,然后调用ALooper_pollAll来处理Looper中事件,这时候利用msgwrite去读取写入的数据。由于是非回调Looper处理,所以android_native_app_glue使用一个结构在存放通用的事件处理,作为最后一个自定义参数传递给ALooper_addFd。如下:
    /**
     * Data associated with an ALooper fd that will be returned as the "outData"
     * when that source has data ready.
     */
    struct android_poll_source {
        // The identifier of this source.  May be LOOPER_ID_MAIN or
        // LOOPER_ID_INPUT.
        int32_t id;
    
        // The android_app this ident is associated with.
        struct android_app* app;
    
        // Function to call to perform the standard processing of data from
        // this source.
        void (*process)(struct android_app* app, struct android_poll_source* source);
    };

    我们继续查看process的实现和赋值的代码如下:
        android_app->cmdPollSource.id = LOOPER_ID_MAIN;
        android_app->cmdPollSource.app = android_app;
        android_app->cmdPollSource.process = process_cmd;
        android_app->inputPollSource.id = LOOPER_ID_INPUT;
        android_app->inputPollSource.app = android_app;
        android_app->inputPollSource.process = process_input;

    static void process_input(struct android_app* app, struct android_poll_source* source) {
        AInputEvent* event = NULL;
        while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
            LOGV("New input event: type=%d\n", AInputEvent_getType(event));
            if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
                continue;
            }
            int32_t handled = 0;
            if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
            AInputQueue_finishEvent(app->inputQueue, event, handled);
        }
    }
    
    static void process_cmd(struct android_app* app, struct android_poll_source* source) {
        int8_t cmd = android_app_read_cmd(app);
        android_app_pre_exec_cmd(app, cmd);
        if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
        android_app_post_exec_cmd(app, cmd);
    }


        process 有两个实现,一个是处理input的事件,一个是处理主线程事件的。然后,process_input使用input.h的函数去处理,然后利用onInputEvent分发输入输出事件。process_cmd调用自己实现的pre和post以及onAppCmd去对主线程事件进行处理和分发。


        所以,使用android_native_app_glue的时候,只要我们去实现onAppCmd和onInputEvent,就可以挂载主线程的事件和输入输出事件处理。然后在循环绘制线程中不断的调用ALooper_pollAll去检测Looper是否有事件需要处理,有的话就调用process方法去处理分发我们自己的挂载。


        这里有些值得思考的地方。

    • 如果我们自己实现java版本的OpenGL绘制,我们会使用GLSurfaceView去处理openGL上下文初始化以及回调。native的方式我们查看NativeActivity的java代码发现并没有使用GLSurfaceView,而是使用了简单的View。这意味着,我们需要在native端自己去对EGL初始化以及销毁。
    • native的实现方式,绘制线程是用native 代码循环控制的。并不是java的循环利用jni回调给native code。我觉得这里就是提高绘制效率的一个地方。
    • android_native_app_glue 使用了大量互斥条件锁进行线程的同步工作。但是我发现这些不是必要的,甚至主线程的事件都可以简单的处理,不必使用Looper来控制。
        下一篇文章会介绍一下,我重写替换掉了android_native_app_glue这层代码。去除了线程同步,去除了自定义事件回调,没有使用Looper去控制主线程事件,简化了大量的代码和流程,直接使用ANativeActivity的生命周期回调去控制。

        最后,给出使用android_native_app_glue代码的框架结构。代码给出了一些思路可以作为参考。
    • 处理input事件
    • 处理senor事件
    • 绘制循环处理Looper事件
    • 挂载onAppCmd和onIputEvent回调
    • 使用c语言的接口计算每帧消耗的时间
    • OpenGL初始化和使用流程

    /*
     * AndroidGlue.c
     *
     * Author: scott.cgi
     */
    
    #include <android/asset_manager.h>
    #include <android/input.h>
    #include <android/looper.h>
    #include <android/native_window.h>
    #include <android/sensor.h>
    #include <android_native_app_glue.h>
    #include <EGL/egl.h>
    #include <EGL/eglplatform.h>
    #include <stdbool.h>
    #include <string.h>
    #include <time.h>
    
    //-------------------------------------------------------------------------
    
    typedef struct android_app         AndroidApp;
    typedef struct android_poll_source AndroidPollSource;
    
    
    //static const long EVENT_RATE =  (1000l / 60) * 1000l;
    
    static struct
    {
        EGLDisplay         display;
        EGLSurface         surface;
        EGLContext         context;
    
        AndroidApp*        app;
        ASensorManager*    sensorManager;
        const ASensor*     accelerometerSensor;
        ASensorEventQueue* sensorEventQueue;
        bool               isRunning;
    
    	struct timespec    last;
    }
    appData[1];
    
    
    static void OnResized()
    {
        EGLint w, h;
        eglQuerySurface(appData->display, appData->surface, EGL_WIDTH,  &w);
        eglQuerySurface(appData->display, appData->surface, EGL_HEIGHT, &h);
    
        AGLConvert->Init(w, h);
        AApplication->callbacks->OnResized(w, h);
    }
    
    
    
    static void OnInit()
    {
    	EGLConfig config;
    	bool result = AGLUtils->CreateEGL(appData->app->window, &appData->display, &appData->context, &appData->surface, &config);
    
    	ALog_A(result, "createEGLContext fail");
    
    	EGLint format;
        // EGL_NATIVE_VISUAL_ID is an attribute of the EGLConfig that is
        // guaranteed to be accepted by ANativeWindow_SetBuffersGeometry()
        // As soon as we picked a EGLConfig, we can safely reconfigure the
        // ANativeWindow buffers to match, using EGL_NATIVE_VISUAL_ID
        eglGetConfigAttrib(appData->display, config, EGL_NATIVE_VISUAL_ID, &format);
        ANativeWindow_setBuffersGeometry(appData->app->window, 0, 0, format);
    
        OnResized();
        AApplication->Init();
    
        // start clock
        clock_gettime(CLOCK_MONOTONIC, &appData->last);
    }
    
    
    
    static void OnDestroy()
    {
    	AGLUtils->DestroyEGL(&appData->display, &appData->context, &appData->surface);
    }
    
    /**
     * Process the next input event
     */
    static int32_t OnInputEvent(AndroidApp* app, AInputEvent* event)
    {
        switch (AInputEvent_getType(event))
        {
    
    		case AINPUT_EVENT_TYPE_MOTION:
    		{
    			int32_t action = AMotionEvent_getAction(event);
    			switch (action & AMOTION_EVENT_ACTION_MASK)
    			{
    				// first pointer down
    				case AMOTION_EVENT_ACTION_DOWN:
    				{
    
    					AApplication->OnTouch
    					(
    						application_subject_touch,
    						AArray_Create
    						(
    							EventTouchPoint, 1,
    							{
    								AGLConvert_ToGLX(AMotionEvent_getX(event, 0)),
    								AGLConvert_ToGLY(AMotionEvent_getY(event, 0)),
    								AMotionEvent_getPointerId(event, 0),
    								event_touch_down,
    							}
    						)
    					);
    				} 	break;
    
    
    				// not first pointer down
    				case AMOTION_EVENT_ACTION_POINTER_DOWN:
    				{
    					int indexDown = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
    
    					AApplication->OnTouch
    					(
    						application_subject_touch,
    						AArray_Create
    						(
    							EventTouchPoint, 1,
    							{
    								AGLConvert_ToGLX(AMotionEvent_getX(event, indexDown)),
    								AGLConvert_ToGLY(AMotionEvent_getY(event, indexDown)),
    								AMotionEvent_getPointerId(event, indexDown),
    								event_touch_down,
    							}
    						)
    					);
    				} 	break;
    
    
    				// first pinter up
    				case AMOTION_EVENT_ACTION_UP:
    				{
    
    					AApplication->OnTouch
    					(
    						application_subject_touch,
    						AArray_Create
    						(
    							EventTouchPoint, 1,
    							{
    								AGLConvert_ToGLX(AMotionEvent_getX(event, 0)),
    								AGLConvert_ToGLY(AMotionEvent_getY(event, 0)),
    								AMotionEvent_getPointerId(event, 0),
    								event_touch_up,
    							}
    						)
    					);
    				} 	break;
    
    
    				// not first pointer up
    				case AMOTION_EVENT_ACTION_POINTER_UP:
    				{
    					int  indexUp = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
    
    					AApplication->OnTouch
    					(
    						application_subject_touch,
    						AArray_Create
    						(
    							EventTouchPoint, 1,
    							{
    								AGLConvert_ToGLX(AMotionEvent_getX(event, indexUp)),
    								AGLConvert_ToGLY(AMotionEvent_getY(event, indexUp)),
    								AMotionEvent_getPointerId(event, indexUp),
    								event_touch_up,
    							}
    						)
    					);
    
    				} 	break;
    
    
    
    				case AMOTION_EVENT_ACTION_MOVE:
    				{
    					int count = AMotionEvent_getPointerCount(event);
    					EventTouchPoint points[count];
    
    					for (int i = 0; i < count; i++)
    					{
    						points[i].x    = AGLConvert_ToGLX(AMotionEvent_getX(event, i));
    						points[i].y    = AGLConvert_ToGLY(AMotionEvent_getY(event, i));
    						points[i].id   = AMotionEvent_getPointerId(event, i);
    						points[i].type = event_touch_move;
    					}
    
    					AApplication->OnTouch(application_subject_touch, (Array[]) {points, count});
    				} 	break;
    
    
    				case AMOTION_EVENT_ACTION_CANCEL:
    				{
    					int count = AMotionEvent_getPointerCount(event);
    					EventTouchPoint points[count];
    
    					for (int i = 0; i < count; i++)
    					{
    						points[i].x    = AGLConvert_ToGLX(AMotionEvent_getX(event, i));
    						points[i].y    = AGLConvert_ToGLY(AMotionEvent_getY(event, i));
    						points[i].id   = AMotionEvent_getPointerId(event, i);
    						points[i].type = event_touch_cancel;
    					}
    
    					AApplication->OnTouch(application_subject_touch, (Array[]) {points, count});
    
    				} 	break;
    
    				default:
    					return 0;
    			}
    
    			return 1;
    		}
    
    
    		case AINPUT_EVENT_TYPE_KEY:
    		{
    		}
    
        }
    
    
        // default dispatching
        return 0;
    }
    
    
    /**
     * Process the sensor event
     *
    static int OnSensorEvent(int fd, int events, void* data)
    {
    	ALog_D("OnSensorEvent events = %d", events);
    
    	if (appData->accelerometerSensor)
    	{
    		ASensorEvent event;
    		while (ASensorEventQueue_getEvents(appData->sensorEventQueue, &event, 1) > 0)
    		{
    			ALog_D
    			(
    				"accelerometer: x=%f y=%f z=%f",
    				event.acceleration.x, event.acceleration.y,
    				event.acceleration.z
    			);
    		}
    	}
    
    	return 1;
    }
    */
    
    
    static void OnCmd(AndroidApp* app, int32_t cmd)
    {
        switch (cmd)
        {
        	case APP_CMD_INPUT_CHANGED:
        		ALog_D("APP_CMD_INPUT_CHANGED");
        		break;
    
            case APP_CMD_INIT_WINDOW:
            	ALog_D("APP_CMD_INIT_WINDOW");
                // The window is being shown, get it ready
            	ALog_A(app->window, "Android window init failed");
    		    OnInit();
                break;
    
            case APP_CMD_TERM_WINDOW:
            	ALog_D("APP_CMD_TERM_WINDOW");
                // The window is being hidden or closed, clean it up.
                break;
    
            case APP_CMD_WINDOW_RESIZED:
            	ALog_D("APP_CMD_WINDOW_RESIZED");
            	OnResized();
            	break;
    
            case APP_CMD_WINDOW_REDRAW_NEEDED:
            	ALog_D("APP_CMD_WINDOW_REDRAW_NEEDED");
            	break;
    
            case APP_CMD_CONTENT_RECT_CHANGED:
            	ALog_D("APP_CMD_CONTENT_RECT_CHANGED");
            	break;
    
            case APP_CMD_GAINED_FOCUS:
            	ALog_D("APP_CMD_GAINED_FOCUS");
    
               // When our app gains focus, we start monitoring the accelerometer.
               /**
               if (appData->accelerometerSensor)
               {
                    ASensorEventQueue_enableSensor
    				(
    					appState->sensorEventQueue,
    					appState->accelerometerSensor
    				);
    
                    // We'd like to get 60 events per second
                    ASensorEventQueue_SetEventRate
    				(
    					appState->sensorEventQueue,
    					appState->accelerometerSensor, EVENT_RATE
    				);
                }
                */
    
                appData->isRunning = true;
                break;
    
            case APP_CMD_LOST_FOCUS:
            	ALog_D("APP_CMD_LOST_FOCUS");
    
                // When our app loses focus, we stop monitorisng the accelerometer
                // This is to avoid consuming battery while not being used
            	/**
                if (appData->accelerometerSensor)
            	{
                   ASensorEventQueue_disableSensor(appData->sensorEventQueue, appData->accelerometerSensor);
                }
                */
    
                // Also stop running.
                appData->isRunning = false;
                break;
    
            case APP_CMD_CONFIG_CHANGED:
            	ALog_D("APP_CMD_CONFIG_CHANGED");
            	break;
    
            case APP_CMD_LOW_MEMORY:
            	ALog_D("APP_CMD_LOW_MEMORY");
            	break;
    
            case APP_CMD_START:
            	// Application just start
            	ALog_D("APP_CMD_START");
            	break;
    
            case APP_CMD_RESUME:
            	// Called after application onStart, not visible yet
            	ALog_D("APP_CMD_RESUME");
            	break;
    
            case APP_CMD_SAVE_STATE:
            	// The system has asked us to save our current state
            	ALog_D("APP_CMD_SAVE_STATE");
                break;
    
            case APP_CMD_PAUSE:
            	// Called when application going into the background
            	ALog_D("APP_CMD_PAUSE");
            	AApplication->callbacks->OnPause();
            	break;
    
            case APP_CMD_STOP:
            	// Called after onPause, when application no longer visible
            	ALog_D("APP_CMD_STOP");
            	break;
    
            case APP_CMD_DESTROY:
            	// Called after onStop,  when application final before destroyed
            	ALog_D("APP_CMD_DESTROY");
            	OnDestroy();
            	break;
        }
    }
    
    /**
     * This is the main entry point of a native application that is using
     * android_native_app_glue.  It runs in its own thread, with its own
     * event loop for receiving input events and doing other things
     */
    void android_main(AndroidApp* app)
    {
    	ALog_D("android_main");
    
    	ApplicationMain();
    
        // make sure glue isn't stripped
        app_dummy();
        memset(appData, 0, sizeof(appData));
    
        app->onAppCmd     = OnCmd;
        app->onInputEvent = OnInputEvent;
        appData->app      = app;
    
    
        // Prepare to monitor accelerometer
    //	appData->sensorManager        = ASensorManager_getInstance();
    //	appData->accelerometerSensor  = ASensorManager_getDefaultSensor(appData->sensorManager, ASENSOR_TYPE_ACCELEROMETER);
    //	appData->sensorEventQueue     = ASensorManager_createEventQueue(appData->sensorManager, app->looper, LOOPER_ID_USER, OnSensorEvent, appData);
        AndroidPollSource* source     = NULL;
    
        // loop waiting for stuff to do
        while (true)
        {
        	// Read all pending events
            // If not running, we will block forever waiting for events
            // If running, we loop until all events are read
            // then continue to draw the next frame of run
            while (ALooper_pollAll(appData->isRunning ? 0 : -1, NULL, NULL, (void**) &source) > -1)
            {
    
                // Process this event
                if (source)
                {
                    source->process(app, source);
                }
    
                // Check if we are exiting
                if (app->destroyRequested != 0)
                {
                    return;
                }
            }
    
            struct timespec now;
        	clock_gettime(CLOCK_MONOTONIC, &now);
        	float deltaTime = (now.tv_nsec - appData->last.tv_nsec) * 0.000000001 +
        				      (now.tv_sec  - appData->last.tv_sec);
    
    		appData->last   = now;
    		AApplication->Loop(deltaTime);
    
    	    eglSwapBuffers(appData->display, appData->surface);
        }
    }
    展开全文
  • 项目中有一个书架的View提供给ReactNative使用,监听ReactNative相应的生命周期进行界面刷新,代码如下 //ViewManager public class BookShelfManager extends SimpleViewManager&lt;BookShelfView&gt; { ...
  • 前端开发是离用户最近的工程领域,需要在开发时间和体验上不断作出选择和权衡, 就像著名的论断 “php是最好的计算机语言"一样, js也能依靠(node, react native)一统天下. 我们都想要一个统一的框架搞定一切. 而...
  • 前言:好久没写博客了,因为这段时间一直在搞rn,程序员真是一个苦逼的职业啊,被逼着去学习了下React Native,这东西吧,感觉有点原生app开发经验的童鞋上手还是比较容易的,搞前端的就更不用说了~分分钟就上手了...
  • webview系列:Html5页面和Native App怎么进行交互混合开发的App(Hybrid App)就是在一个App中内嵌一个轻量级的浏览器,一部分原生的功能改为Html5来开发,这部分功能不仅能够在不升级App的情况下动态更新,而且可以...
  • webview系列:Html5页面和Native App怎么进行交互混合开发的App(Hybrid App)就是在一个App中内嵌一个轻量级的浏览器,一部分原生的功能改为Html5来开发,这部分功能不仅能够在不升级App的情况下动态更新,而且可以...
  • 概述React Native用iOS自带的JavaScriptCore作为JS的解析引擎,但并没有用到JavaScriptCore提供的一些可以让JS与OC互调的特性,而是自己实现了一套机制,这套机制可以通用于所有JS引擎上,在没有JavaScriptCore的...
  • React Native获取app版本号
  • NDK为我们提供了胶水层android_native_app_glue,但我们现在要抛弃它,我们能够有一个更简单更有效率的玩法。替换思路如下: 直接使用ANativeActivity事件回调,放弃使用Looper的方案 input.h和senor.h提供的接口...
  • 本文是基于Android 7.0源码,来分析Native Crash流程。 转自 http://gityuan.com/2016/06/25/android-native-crash/ 一、Native Crash 从系统全局来说,Crash分为Framework/App Crash, Native Crash,以及...
  • 我们的 APP 是基于 React Native 开发的,这些功能 Facebook 官方没有提供,需要我们开发对应的原生模块。 因为开发原生模块属于比较深入的内容了,写这篇文章时我就默认阅读者已经具有一定的 Objective-C 和 Java ...
  • self.driver.find_element_by_name('分享样式1').click() self.driver.find_element_by_name('UMS sina icon').click() sleep(2) print self.driver.contexts context_name = 'WEBV
  • 2018.12.28 博主苟延残喘中,可能会上演从入门到放弃
  • 前言众所周知,react native开发是前几...主要讲了react native和原生的app之间的通讯与沟通。首先,react native的配置就不做赘述。具体可参考:React Native中文网: https://reactnative.cn/docs/0.20/android-set...
  • 支付宝APP逆向工程笔记之一   最近自己想写一个APP,由于是计算密集型的APP,用到了 Android 的NDK。NDK这么好的东西,支付宝、微信、、、APP肯定会用上吧,打开APK文件一看果然是。而且里面好多都是我认识的...
  • native app 集成 cocos-2dx-js 3.11 (Android篇),各种坑
1 2 3 4 5 ... 20
收藏数 34,835
精华内容 13,934
关键字:

app context native