一、背景啰唆
这两天被临时要求帮忙解决一个安卓App上的定位问题。其实我是有点抵触心理的,因为我最近刚入手了Koa2和Vue,玩得不亦乐乎,不太想在这个时候被打断。但是作为一个有良好职业素养的搬砖工,同时也本着“自己也是一块砖,哪里需要哪里搬”的精神,同时感受到“这个问题好像只我能解决”的特殊使命感,我愉快地(不得不)接受了这个任务。经过分析,App是用Ionic开发的,用的是百度的Js定位api,多数机型,多数时候定位准确,但是出现不准的概率还是比较多,于是我决定给它做个原生插件,用百度的Android sdk重做定位(说得云淡风清,其实,过程让我好蛋疼)。
下图是过程总结,先看一下,做到心理有个大概:
一、环境准备
1.1 Android开发环境准备
1.1.1 JDK、Android-sdk、gradle、android-studio(不用as的,也可以用其它IDE)
以上三件套准备好,安装过程也不再赘述,有不清楚的地方可以百度,最终结果要达下图所示(相关版本是我本机的,不一定要一样):
环境变量
JDK版本
JDK下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
Gradle环境
Gradle下载地址:http://www.androiddevtools.cn/
Android SDK
下图是我本机的情况,实际当然不需要这么多版本的sdk,只需要一个你期望的编译版本就好。
这里有你想要的关于Android的一切
http://www.androiddevtools.cn/
1.2 Ionic开发环境准备
nodejs、ionic相关、cordova、vsCode/webstorm(由你喜欢)
nodejs环境
装完nodejs后,验证一下:
从这里下载nodejs
https://nodejs.org/en/
Ionic
通过指令安装:
npm install -g ionic
结果:
这个版本只是当时装的时候的最新稳定版本,每个人可能不一样,如果要装指定版本,请用类似“npm install -g ionic@3.0.5”的指令,其中3.0.5就是你要指定的版本号。
三、Ionic项目准备
3.1 生成官方标准项目
这里就用官方的标准项目为例,在cmd定位到你的项目目录,执行以下指令:
ionic start myApp tabs
这里选择y,同意给你的Ionic应用添加iOS和Android的cordova依赖平台。如果你是用ionic来做类似微信公众号那样的应用就可以不加这个依赖。
一堆日志后,会面临如图的第二个要选择的处理:
这个就是问你要不要用ionic的在线平台,这里可以选择n,不要。最后跑完之后,生成的项目目录是这样的:
cmd进入到该目录下:
执行:
ionic server
把项目跑一下,起来后效果如下图:
3.2 增加Android编译平台
给你的ionic增加cordova cli
在你的项目目录下,执行指令:
npm install --save -D cordova
安装cordovacli,
还是在你的项目目录下,执行指令:
ionic cordova platform add android@6.4.0
这里要下的东西比较多,时间也比较久。
这里一定要加一个android的版本号指明你要依赖哪个android版本的sdk来编译
成功后,你的项目目录下会多出这两个目录:
完成!
四、Android插件开发
我要开发的插件是百度地图的Android原生定位插件。因此要到百度平台去申请账号和创建App应用,到得相关的key,下载相关的so库和jar包
这个过程就不赘述了,百度的文档很清楚:http://lbsyun.baidu.com/
4.1 在Android原生应用上开发功能和自测
这里啰唆一下,可以先在Androidd原生应用上写一遍你的逻辑,并且自测一下功能,保证你理解了你要做的功能是什么,用原生代码逻辑上怎么实现,自己测试通过,然后再搬到插件项目中去,因为插件项目调试比较麻烦,不要把bug留到那个时候去调试。
4.2 创建cordova插件项目:
安装plugman
在cmd执行以下命令:
npm install plugman -g
找一个你准备放插件项目的目录:
在该目录下执行:
plugman -create --name BDLocation --plugin_id com.test.bdlocation --plugin_version 1.0.0
其中,BDLocation是我的插件项目名,com.test.bdlocation是我的主包名,作为插件的唯一标识。生成如下目录结构的项目:
在该目录下,执行:
plugman platform add --platform_name android
在项目的src下会创建android文件夹,下面会生成一个文件BDLocation.java,这就是我们cordova插件的入口文件。另外在项目的www文件夹下还有一个js的调用例子。
这里不详细讨论cordova的api,只作简单的例子介绍,希望后面有时间再详细讨论cordova的api。这是BDLocation.java里的内容:
package com.test.bdlocation;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* This class echoes a string called from JavaScript.
*/
public class BDLocation extends CordovaPlugin {
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("coolMethod")) {
String message = args.getString(0);
this.coolMethod(message, callbackContext);
return true;
}
return false;
}
private void coolMethod(String message, CallbackContext callbackContext) {
if (message != null && message.length() > 0) {
callbackContext.success(message);
} else {
callbackContext.error("Expected one non-empty string argument.");
}
}
}
下面来简单说一下execute方法:
这个方法是插件的入口方法,js通过传入类名来让cordova找到这个插件,找到这个插件后,默认执行入口方法execute。
参数action:通过js调用传进来,代表你要这个插件干什么,通常代表一个行为,这里默认的例子是调用了一个方法
参数args:通过js调用传进来,类似java main方法传参,在js中就是传对象数组进来
参数callbackContext:通过它来把结果回调给js,在js中会传来成功和失败的两个回调函数,对应callbackContext的success和error方法,如这里的coolMethod示例方法所示。
返回boolean值,根据英文理解是,如果你收到的action是你想要的,就返回true,否则返回false。(这里还可以测试一下,如果首次不处理这个action,并且返回false,下次js调用如果还是这个类的这个action,有可能cordova不帮你调插件了,这一点我还没验证,回头再补)
下面是wwww/BDLocation.js里调用示例:
var exec = require('cordova/exec');
exports.coolMethod = function (arg0, success, error) {
exec(success, error, 'BDLocation', 'coolMethod', [arg0]);
};
这里主要看exec函数的传参。一目了然,sucess和error分别对应BDLocation.java中的callbackContext成功与失败的回调,也就是说callbackContext中调用success方法,那么js中这个success方法就会被回调,error也是同理。
BDLocation是代表了java中插件的类名(也是插件名,一般保持同名,不同会怎样呢?可以再试。),arg0当然就是你要传给插件的业务参数了。
4.3 编写插件代码
现在我们回到前面在AndroidStudio上的应用:
我们把插件项目的BDLocation.java文件直接copy到应用中:
我们发现项目里根据还没有Cordova相关的依赖,代码会报错。于是我们还要添加一个模块:
回到我们之前创建的ionic项目,在项目目录下,进入路径:platforms\android,找到该路径下的CordovaLib目录,记下该目录。
现在再回到AndroidStudio,我把CordovaLib以模块的形式加入到应用项目依赖中,成功后如下图效果,BDLocation中的报错会消失。
根据之前在应用中写的百度地图的定位功能,在BDLocation上编写相应功能:
package com.test.bdlocation;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import com.baidu.location.Address;
import com.baidu.location.BDAbstractLocationListener;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Administrator on 2018/8/8.
*/
public class BDLocation extends CordovaPlugin{
private LocationClientOption option=new LocationClientOption();
private LocationClient ml=null;
private CallbackContext callbackContext;
private List<String> permissionNeedCheck=null;
public BDAbstractLocationListener myL=new BDAbstractLocationListener() {
@Override
public void onReceiveLocation(com.baidu.location.BDLocation bdLocation) {
if(bdLocation!=null){
JSONObject data=new JSONObject();
try {
JSONObject object=new JSONObject();
object.put("type", bdLocation.getLocType());
object.put("latitude",bdLocation.getLatitude());
object.put("longitude",bdLocation.getLongitude());
JSONObject address=new JSONObject();
address.put("adcode",bdLocation.getAddress().adcode);
address.put("address",bdLocation.getAddress().address);
address.put("city",bdLocation.getAddress().city);
address.put("cityCode",bdLocation.getAddress().cityCode);
address.put("country",bdLocation.getAddress().country);
address.put("countryCode",bdLocation.getAddress().countryCode);
address.put("district",bdLocation.getAddress().district);
address.put("province",bdLocation.getAddress().province);
address.put("street",bdLocation.getAddress().street);
address.put("streetNumber",bdLocation.getAddress().streetNumber);
object.put("address",address);
object.put("city",bdLocation.getCity());
object.put("cityCode",bdLocation.getCityCode());
data.put("status","success");
data.put("location",object);
data.put("code","000");
data.put("errMsg","");
PluginResult pluginResult=new PluginResult(PluginResult.Status.OK,data);
callbackContext.sendPluginResult(pluginResult);
} catch (JSONException e) {
e.printStackTrace();
}
ml.stop();
}
}
};
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if(Build.VERSION.SDK_INT>=23){
permissionNeedCheck=new ArrayList<String>();
permissionNeedCheck.add(Manifest.permission.ACCESS_COARSE_LOCATION);
permissionNeedCheck.add(Manifest.permission.ACCESS_FINE_LOCATION);
this.callbackContext=callbackContext;
if (action.equals("location")) {
this.location();
return true;
}
return false;
}else{
if(action.equals("location")){
this.startLocate();
return true;
}
return false;
}
}
private boolean permissionCheck(){
List<String> unGrantedPermission=new ArrayList<String>();
for(String permission:permissionNeedCheck){
if(PackageManager.PERMISSION_GRANTED!=ContextCompat.checkSelfPermission(cordova.getActivity(),permission)){
unGrantedPermission.add(permission);
}
}
this.permissionNeedCheck=unGrantedPermission;
return unGrantedPermission.size() > 0 ? false : true;
}
private void askPermission(){
String[] unGrantedPer=new String[permissionNeedCheck.size()];
for(int i=0;i<permissionNeedCheck.size();i++){
unGrantedPer[i]=permissionNeedCheck.get(i);
}
ActivityCompat.requestPermissions(cordova.getActivity(),unGrantedPer,1);
}
@Override
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
super.onRequestPermissionResult(requestCode, permissions, grantResults);
location();
}
private void location(){
if(permissionCheck()){
startLocate();
}else{
askPermission();
}
}
private void startLocate(){
option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
option.setCoorType("bd09ll");
option.setOpenGps(true);
option.setIsNeedAddress(true);
option.setIgnoreKillProcess(false);
ml=new LocationClient(cordova.getActivity());
ml.setLocOption(option);
ml.registerLocationListener(myL);
ml.start();
}
}
这里不再详细说明里面的代码了,主要是定位和权限相关的内容。
这里唯一可以说明一下的就是PluginResult,当你要从插件返回一个复杂结果给js的时候,就用它。
五、打包与调试
最后一个大环节就是打包与调试
5.1首先要进行文件准备
到目前为止我们只写了代码,我们在应用中调试了相关的逻辑,我们so库,jar包,等相关的东西都还没有放进插件相关的目录。
现在我们回到插件项目的目录
把在应用中写好的BDLocation.java文件Copy到插件项目的src/android文件夹,把初始生成的BDLocation.java覆盖掉:
库处理——在项目目录的src/android下新建libs文件夹,把相关的so库,jar包放到libs下:
plugin.xml文件处理:
默认生成的plugin.xml文件内容如下:
<?xml version='1.0' encoding='utf-8'?>
<plugin
id="com.test.bdlocation"
version="1.0.0"
xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<name>BDLocation</name>
<js-module name="BDLocation" src="www/BDLocation.js">
<clobbers target="cordova.plugins.BDLocation" />
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="BDLocation"><param name="android-package" value="com.test.bdlocation.BDLocation" />
</feature>
</config-file>
<config-file parent="/*" target="AndroidManifest.xml">
</config-file>
<source-file src="src/android/BDLocation.java" target-dir="src/com/test/bdlocation/BDLocation" />
</platform>
</plugin>
配置完成后,如下:
<?xml version='1.0' encoding='utf-8'?>
<plugin
id="com.test.bdlocation"
version="1.0.0"
xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<name>BDLocation</name>
<js-module name="BDLocation" src="www/BDLocation.js">
<clobbers target="cordova.plugins.BDLocation" />
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<!--这里的value值要对应你的ionic项目的android应用主包名-->
<feature name="BDLocation"><param name="android-package" value="你的ionic项目对应用android应用主包名,如com.jianshu.app" />
</feature>
</config-file>
<!--这个标签内的配置,将会copy到ionic项目的android主配置文件下<manifest>标签内,所以这里可以放权限-->
<config-file parent="/*" target="AndroidManifest.xml">
<!-- 这个权限用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
<!-- 这个权限用于访问GPS定位-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
<!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
<!-- 获取运营商信息,用于支持提供运营商信息相关的接口-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
<!-- 这个权限用于获取wifi的获取权限,wifi信息会用来进行网络定位-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission>
<!-- 用于读取手机当前的状态-->
<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
<!-- 写入扩展存储,向扩展卡写入数据,用于写入离线定位数据-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<!-- 访问网络,网络定位需要上网-->
<uses-permission android:name="android.permission.INTERNET" />
<!-- SD卡读取权限,用户写入离线定位数据-->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"></uses-permission>
</config-file>
<!--同理,这个标签内的配置,将会copy到ionic项目的android主配置文件下的<application>标签内-->
<config-file parent="/manifest/application" target="AndroidManifest.xml">
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="TVBiUUUYv8koeL2jysdG6GHU3TW0jcgM"></meta-data>
<service android:name="com.baidu.location.f" android:enabled="true" android:process=":remote"> </service>
</config-file>
<!--想必聪明的你已经看出来了,下面的配置就是把你插件的java文件,jar包,so库文件或者其它你的插件要用的文件,copy到你的ionic项目的android打包环境(就是我们的android原生项目结构)对应的目录下-->
<!--这里要特别留意,保持你的BDLocation.java文件内package声明的包名和你将要把它放置的包名要一致-->
<source-file src="src/android/BDLocation.java" target-dir="src/com/test/bdlocation/BDLocation" />
<source-file src="src/android/libs/arm64-v8a/libindoor.so" target-dir="libs/arm64-v8a" />
<source-file src="src/android/libs/arm64-v8a/liblocSDK7b.so" target-dir="libs/arm64-v8a" />
<source-file src="src/android/libs/armeabi/libindoor.so" target-dir="libs/armeabi" />
<source-file src="src/android/libs/armeabi/liblocSDK7b.so" target-dir="libs/armeabi" />
<source-file src="src/android/libs/armeabi-v7a/libindoor.so" target-dir="libs/armeabi-v7a" />
<source-file src="src/android/libs/armeabi-v7a/liblocSDK7b.so" target-dir="libs/armeabi-v7a" />
<source-file src="src/android/libs/x86/libindoor.so" target-dir="libs/x86" />
<source-file src="src/android/libs/x86/liblocSDK7b.so" target-dir="libs/x86" />
<source-file src="src/android/libs/x86_64/libindoor.so" target-dir="libs/x86_64" />
<source-file src="src/android/libs/x86_64/liblocSDK7b.so" target-dir="libs/x86_64" />
<source-file src="src/android/libs/BaiduLBS_Android.jar" target-dir="libs" />
</platform>
</plugin>
配置的注释我都尽量写详细了,我这里主要就是相关权限、地图的配置、对应jar、so、java文件的对位copy。
插件package.json文件生成与配置
在我们的插件主目录下,执行cmd指令:
npm init
生成初始package.json文件:
package.json初始内容如下:
{
"name": "bdlocation",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
配置完成后:
{
"name": "bdlocation",
"version": "1.0.0",
"description": "",
"main": "index.js",
"cordova": {
"id": "com.test.bdlocation",
"platforms": [
"android"
]
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
主要是加了cordova插件相关配置。
其中id是指你给你的插件自定义的唯一id,platform就是你的插件的运行平台,如果支持ios,你可以加上ios。
到此文件准备就完成了!!!
5.2给Ionic项目添加插件
现在回到我们的Ionic项目目录
添加自定义插件到Ionic项目
在ionic项目目录下,执行cmd指令:
ionic cordova plugin add ../BDLocation
其中,../BDLocation是自定义插件的项目的相对目录,完成后,Ionic项目目录下,会产生一个plugins文件夹,里面有我们安装好的插件:
5.3打包debug apk装备进行真机调试
在Ionic项目目录下,执行cmd指令:
ionic cordova build android --debug
这个过程相对比较久,第一次的时候还要联网下载不少东西,完成后在Ionic项目的platforms\android\build\outputs\apk的目录下,生成了相应的android-debug.apk文件就是了。
5.4真机调试
把上面的apk copy到手机上安装,或者通过adb指令安装都可以。这里我们用chrome的DevTools进行真机调试。在Chrome地址中输入chrome://inspect/#devices,用USB接上你的Android手机,打开调试:
如上图,我的pro5就被找到了。
运行你的app,你们看到多了一个Inspect标签,可以点击:
点击进去后,发现:
有日志输出了!!!!
当然你完全也可以用Android Studio来看日志,还可以在cmd用adb相关命令来看日志。
5.4打生产包与签名
在Ionic项目目录下,执行cmd指令:
ionic cordova build android --release
得到未签名的apk文件:android-unsign.apk(根据打包的输出日志,可以看到apk包被打出到哪个目录,叫什么名字)
把未签名apk,keystore文件放到同一个目录,在该目录下,执行cmd指令:
jarsigner -verbose -keystore your.keystore -signedjar complete.apk android-unsign.apk your.keystore
得到签名好的apk,complete.apk
虽然,还有不少是我还想写的详细一点的内容,还有一些是我原本计划要写但是又被我砍掉的内容,但是就写到这里吧,等下还要再校正一遍,我的原力快用完了!!!
有错误的地方欢迎指正。
最后我的环境如下: