要使用SmartWeatherAPI获取天气必须要知道一下几点:
(1)要搞清楚自己需要什么天气数据(我看了半天API说明文档,才发现获取实时天气状况的数据里边竟然没有天气现象描述,只有温度、湿度、风力、风向等等)
(2)SmartWeatherAPI的key是需要加密算法之后才可以使用的(这也是一个坑,这个算法API文档里边提供的是一个PHP的一个方法,直接就可以得到,但是要移植到Java中可就不是那么容易了)
(3)SmartWeatherAPI中提供3中获取数据的方式,分别是:observe(实时天气)、forecast3d(未来3天天气)、index(指数,目前就一个穿衣指数)
(4)这一点是和第(1)点有关的,如果要获取全面的天气信息,当然是要3种获取方式都要用到,但是每一种获取方式有不同,导致获取天气的URL不一样,要使用AnyscTask并不是很好做,因此我就想到使用一个多线程并发的模式,这样技能一次获取到所有的天气数据,而且速度也很快(我之前的做法是一次用一种访问方式获取数据要快3倍)
下边开始上代码:
首先是整个项目的结构图:
PS:由于这里边也有我之前写的其他测试的方法,所以只需要看我选中的就可以了还有一点,这里没有使用XML进行布局,这个是和我现在公司做的项目有关,所以我也就在做这个测试的时候也没有用XML
代码顺序:
(1)SmartWeatherUrlUtil:生成SmartWeatherAPI所需的URL(这个前边也说过,所以只好专门写一个类来生成秘钥)
(2)WeatherServerUtil:这个里边封装一个链接服务器请求并保存天气数据
(3)ItemWeatherView:用于显示获取到的天气数据,具体来说就是一个界面,不过里边还有重要的东西:那就是我要说的核心了,多线程并发
(4)MainActivity:一个Acyivity,这个就不用说了
(5)用户权限,别忘了给程序添加网络权限
上代码:
(1)SmartWeatherUrlUtil:
package com.home.utils;
import android.util.Base64;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* JAVA版通过appid和key生成的使用SmartWeatherAPI的Url类
* 调用方法:String apiUrl = SmartWeatherUrlUtil.getInterfaceURL("城市编号","数据类型");
* 数据类型:实况,预报(3d),指数
* Created by Mrqiang on 2016/8/23.
*/
public class SmartWeatherUrlUtil {
private static final String MAC_NAME = "HmacSHA1";
private static final String ENCODING = "utf-8";
private static final String appid = "你自己申请的appid";
private static final String private_key = "你自己申请的private_key ";
private static final String url_head = "http://webapi.weather.com.cn/data/?";
/**
* 使用 HAC-SHA1 强命方法对 encryptText 进行签名
* url : 被签名的字符串
* privatekey : 密钥
* */
private static byte[] HmacSHA1Encrypt(String url, String privatekey) throws Exception{
byte[] data = privatekey.getBytes(ENCODING);
// 根据给定的字节数组构造一个密钥,第二个参数制定一个密钥算法的名称
SecretKey secretKey = new SecretKeySpec(data,MAC_NAME);
// 生成一个制定 Mac 算法的 Mac 对象
Mac mac = Mac.getInstance(MAC_NAME);
// 用给定密钥初始化 Mac 对象
mac.init(secretKey);
byte[] text = url.getBytes(ENCODING);
// 完成 Mac 操作
return mac.doFinal(text);
}
/**
* 获取 URL 通过 privatekey 加密后的码
* */
private static String getKey(String url , String privatekey) throws Exception{
byte[] key_bytes = HmacSHA1Encrypt(url,privatekey);
String base64encodeStr = Base64.encodeToString(key_bytes,Base64.NO_WRAP);
return URLEncoder.encode(base64encodeStr,ENCODING);
}
/**
* 获得接口的 URL 地址
* @author Mrqiang
* */
private static String getInterfaceURL(String areaid, String type, String date)
throws Exception{
String keyurl = url_head+"areaid="+areaid+"&type="+type+
"&date="+date+"&appid=";
String key = getKey(keyurl+appid,private_key);
String appid6 = appid.substring(0,6);
return keyurl+appid6+"&key="+key;
}
public static String getInterfaceURL(String areaid, String type){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
String date = dateFormat.format(new Date());
try {
return getInterfaceURL(areaid,type,date);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
这里要说一下,这个key的算法是基于你现有的秘钥基础上,如果没有的话还是申请一个比较好
(2)WeatherServerUtil:
package com.home.network;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by Mrqiang on 2016/9/6.
*/
public class WeatherServerUtil extends Thread {
private static final String TAG = "WeatherServerUtil";
private static final boolean DBG = true;
// 是否获取到天气
private boolean isCompleted = false;
// 天气数据
private String weatherJson = "";
// 获取天气的URL
private URL weatherUrl;
// 当前线程的名字
private String threadName;
public WeatherServerUtil(URL weatherUrl ,String threadName) {
this.weatherUrl = weatherUrl;
this.threadName = threadName;
this.setName( threadName );
}
@Override
public void run() {
InputStream is =null;
BufferedReader bf = null;
// 每次获取天气数据之前进行一次初始化
weatherJson = "";
isCompleted = false;
try {
if(DBG){
Log.i(TAG,"---threadName: " + threadName + " , weatherUrl : "+weatherUrl);
}
HttpURLConnection conn = (HttpURLConnection)weatherUrl.openConnection();
conn.setReadTimeout(10000);
conn.setConnectTimeout(15000);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.connect();
if(conn != null){
is = conn.getInputStream();
bf = new BufferedReader(new InputStreamReader(is));
String line = "";
while((line = bf.readLine()) != null){
weatherJson += line;
}
isCompleted = true;
if(DBG){
Log.i(TAG,"---threadName: " + threadName + " , weatherJson : "+weatherJson);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bf.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public boolean isCompleted(){
return isCompleted;
}
public String weatherJson(){
return weatherJson;
}
}
可以看到我在这个网络访问的类中加了两个看起来不起眼但是很重要的两个方法,这个是用来保证你每次使用到的数据都是最新获取的,因为我这个是在你每一次访问服务器之后只有获取到服务器返回的数据,我才会保存下来,而不是直接使用这个数据(我之前写的一种方法是使用一个方法,该方法中再启用一个线程,同时方法会返回从服务器获取到的天气数据,如果是这样的话,就可能出现在网络不好的情况下,你这个获取数据的线程还没有走完,但是你已经将数据返回了,这是你获取的返回数据就有可能是上一次的,而我也在每一次线程启动之后将天气数据刷新,保证数据位最新获取的,以下是我之前的获取天气数据的方法,大家可以对比一下,然后看看我所说的话,就会明白其中的道理)
原来的获取天气数据的方法:
package com.home.weather;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* This class is used to get Weather JsonObiect from Internet and translate into String Jsontext
* Created by Mrqiang on 2016/8/22.
*/
public class GetWeather {
private static final String TAG = "GetWeather";
private static final boolean DBG = true;
private String jsontext = "";
//Get Jsontext from Internet by URL
public String getJsontext(final URL url){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
if(DBG){
Log.i(TAG,"----getJsontext, Current Thread : "+Thread.currentThread().getName());
}
InputStream is =null;
BufferedReader bf = null;
try {
if(DBG){
Log.i(TAG,"---getJsontext : url:"+url);
}
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setReadTimeout(10000);
conn.setConnectTimeout(15000);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.connect();
if(conn != null){
is = conn.getInputStream();
bf = new BufferedReader(new InputStreamReader(is));
String line = "";
while((line = bf.readLine()) != null){
jsontext += line;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bf.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
thread.start();
try {
// 让主线程休眠1000ms
Thread.sleep( 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(DBG){
Log.i(TAG,"---getJsontext: Jsontext"+jsontext);
}
return jsontext;
}
}
一般情况下上边的方法也是可以的,但是有没有想过获取天气数据的子线程和主线程是同步进行的,如果主线程休眠1000ms之后就要返回数据,而这时由于网络不好,上边的子线程还没有执行完,那么这次返回的数据就会是之前的数据,或者返回的数据不完整等等,总之是有bug存在的
(3)ItemWeatherView :界面+多线程并发
package com.home.view;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.home.network.WeatherServerUtil;
import com.home.utils.SmartWeatherUrlUtil;
import java.net.MalformedURLException;
import java.net.URL;
/**
* Created by Mrqiang on 2016/9/6.
*/
public class ItemWeatherView extends LinearLayout {
private static final String TAG = "WeatherView";
private static final boolean DBG = true;
private Context mContext;
Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
String mProgress = msg.getData().getString("progress");
if("100".equals(mProgress)){
TextView mText1 = new TextView(mContext);
String[] weatherInfo = msg.getData().getStringArray("weatherJson");
if(DBG){
Log.i(TAG,"---handleMessage.weatherInfo.length: " +weatherInfo.length );
}
String weather = "" ;
for(int i = 0; i < 3; i++ ){
if(DBG){
Log.i(TAG,"---handleMessage.weatherinfo[" + i + "] :" + weatherInfo[i]);
}
weather = weather + weatherInfo[i];
}
mText1.setText("WeatherInfo : " +weather);
addView(mText1);
}else{
TextView mText2 = new TextView(mContext);
mText2.setText("正在获取天气...");
addView(mText2);
}
}
};
public ItemWeatherView(Context context) {
super(context);
this.mContext = context;
}
public ItemWeatherView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
WeatherTask mWeatherTask = new WeatherTask("101280601");
mWeatherTask.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
class WeatherTask extends Thread{
private String[] type = {"observe","forecast3d","index"};
private String areaId; //区域ID
private String[] weatherJson =new String[3];
public WeatherTask(String areaId) {
this.areaId = areaId;
}
@Override
public void run() {
WeatherServerUtil[] mWeatherServerUtils = new WeatherServerUtil[3];
try {
URL[] weatherUrl = new URL[3];
for (int i = 0; i < 3; i++){
weatherUrl[i] = new URL(SmartWeatherUrlUtil.getInterfaceURL(areaId,type[i]));
mWeatherServerUtils[i] = new WeatherServerUtil(weatherUrl[i],type[i]);
mWeatherServerUtils[i].start();
}
boolean isFinished = false;
while(!isFinished){
isFinished = true;
for( int j = 0; j < 3; j++){
weatherJson[j] = "";
if(!mWeatherServerUtils[j].isCompleted()){
isFinished = false;
}
}
}
Message msg = new Message();
if(isFinished){
for(int i = 0; i < 3; i++){
weatherJson[i] = mWeatherServerUtils[i].weatherJson();
if(DBG){
Log.i(TAG,"---WeatherTask.weatherJson["+ i +"]: "+weatherJson[i]);
}
}
msg.getData().putStringArray("weatherJson",weatherJson);
msg.getData().putString("progress","100");
}else{
msg.getData().putString("progress","50");
}
mHandler.sendMessage(msg);
Thread.sleep(200);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里我主要是为了演示效果,所以界面就没有美化,只是简单的显示数据
但是相比之前的获取天气数据的方法相比,这个反应速度确实提升了不少。
这里也要详细给大家讲一讲我当初的想法,当时我也是和很苦恼,既然要异步获取天气数据,肯定用AsyncTask比较好,可是我仔细研究了一下这个,发现AsyncTask适合只有一个Url的情况,而且每一次启动都需要new 一个对象,每一次都会更新UI,虽然是一个封装好的异步处理,但是在显然这里并不适用,因为这里获取天气数据需要3种方式,即有3个URL,于是我就在想该怎么办才好,是开一个多线程,然后每一种获取方式写进一个线程?这样岂不是要写3个方法?最后我想到将访问方式写进一个数组,然后将访问服务器的线程也写成一个数组,而且这两个数组对应起来,这样就可以同时多个线程进行访问了(之前是一个线程访问完之后,更换访问类型,然后再去访问,直到把所有的数据获取到,这样也太慢了,更改之后相当于提速3倍啊)
最重要的一点差点忘了说了,前边提到的返回数据的问题,这里我都有判断,虽然3个线程同时跑,但是只要有一个还没有跑完,我就不会提取数据,这个在程序里边体现就是isFinished是在3个子线程都isCompleted为true之后才会为true,然后才将数据返回到主线程
(4)MainActivity:
package com.home;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import com.home.view.ItemWeatherView;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final boolean DBG = true;
private ItemWeatherView mItemWeatherView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mItemWeatherView = new ItemWeatherView(this);
setContentView(mItemWeatherView);
if(DBG){
Log.i(TAG,"------onCreate :");
}
}
}
到这里,基本也讲完了,最后别忘了家网络权限!
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
Key算法参考文档:
java版SmartWeatherAPI 获取天气信息 工具类