android网络处理HttpClient
来源:互联网 发布:qq飞车赛车数据库 编辑:程序博客网 时间:2024/04/29 22:15
转载自:
http://android.tgbus.com/Android/tutorial/201108/364645.shtml (Android开发实现HttpClient工具类)
http://www.open-open.com/lib/view/open1329101420890.html (Android 网络数据的处理之 HttpClient)
http://blog.csdn.net/heng615975867/article/details/9012303 (Android网络优化之HttpClient)
文章一、Android开发实现HttpClient工具类
在Android开发中我们经常会用到网络连接功能与服务器进行数据的交互,为此Android的SDK提供了Apache的HttpClient来方便我们使用各种 Http服务。你可以把HttpClient想象成一个浏览器,通过它的API我们可以很方便的发出GET,POST请求(当然它的功能远不止这些)。比如你只需以下几行代码就能发出一个简单的GET请求并打印响应结果:
try {
// 创建一个默认的HttpClient
HttpClient httpclient = new DefaultHttpClient();
// 创建一个GET请求
HttpGet request = new HttpGet("www.google.com");
// 发送GET请求,并将响应内容转换成字符串
String response = httpclient.execute(request, new BasicResponseHandler());
Log.v("response text", response);
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
为什么要使用单例HttpClient?
这只是一段演示代码,实际的项目中的请求与响应处理会复杂一些,并且还要考虑到代码的容错性,但是这并不是本篇的重点。注意代码的第三行:
HttpClient httpclient = new DefaultHttpClient();
在发出HTTP请求前,我们先创建了一个HttpClient对象。那么,在实际项目中,我们很可能在多处需要进行HTTP通信,这时候我们不需要为每个请求都创建一个新的HttpClient。因为之前已经提到,HttpClient就像一个小型的浏览器,对于整个应用,我们只需要一个HttpClient就够了。看到这里,一定有人心里想,这有什么难的,用单例啊!!就像这样:
<strong><span style="font-size:14px;"> public class CustomerHttpClient { private static HttpClient customerHttpClient; private CustomerHttpClient() { } public static HttpClient getHttpClient() { if(null == customerHttpClient) { customerHttpClient = new DefaultHttpClient(); } return customerHttpClient; } }</span></strong>
多线程!试想,现在我们的应用程序使用同一个HttpClient来管理所有的Http请求,一旦出现并发请求,那么一定会出现多线程的问题。这就好像我们的浏览器只有一个标签页却有多个用户,A要上google,B要上baidu,这时浏览器就会忙不过来了。幸运的是,HttpClient提供了创建线程安全对象的API,帮助我们能很快地得到线程安全的“浏览器”。
解决多线程问题
<pre name="code" class="javascript"><strong><span style="font-size:14px;">public class CustomerHttpClient { private static final String CHARSET = HTTP.UTF_8; private static HttpClient customerHttpClient; private CustomerHttpClient() { } public static synchronized HttpClient getHttpClient() { if (null == customerHttpClient) { HttpParams params = new BasicHttpParams(); // 设置一些基本参数 HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(params, CHARSET); HttpProtocolParams.setUseExpectContinue(params, true); HttpProtocolParams .setUserAgent( params, "Mozilla/5.0(Linux;U;Android 2.2.1;en-us;Nexus One Build.FRG83) " + "AppleWebKit/553.1(KHTML,like Gecko) Version/4.0 Mobile Safari/533.1"); // 超时设置 /* 从连接池中取连接的超时时间 */ ConnManagerParams.setTimeout(params, 1000); /* 连接超时 */ HttpConnectionParams.setConnectionTimeout(params, 2000); /* 请求超时 */ HttpConnectionParams.setSoTimeout(params, 4000); // 设置我们的HttpClient支持HTTP和HTTPS两种模式 SchemeRegistry schReg = new SchemeRegistry(); schReg.register(new Scheme("http", PlainSocketFactory .getSocketFactory(), 80)); schReg.register(new Scheme("https", SSLSocketFactory .getSocketFactory(), 443)); // 使用线程安全的连接管理来创建HttpClient ClientConnectionManager conMgr = new ThreadSafeClientConnManager( params, schReg); customerHttpClient = new DefaultHttpClient(conMgr, params); } return customerHttpClient; } }</span></strong>
在上面的getHttpClient()方法中,我们为HttpClient配置了一些基本参数和超时设置,然后使用ThreadSafeClientConnManager来创建线程安全的HttpClient。上面的代码提到了3种超时设置,比较容易搞混,故在此特作辨析。
HttpClient的3种超时说明
/* 从连接池中取连接的超时时间 */
ConnManagerParams.setTimeout(params, 1000);
/* 连接超时 */
HttpConnectionParams.setConnectionTimeout(params, 2000);
/* 请求超时 */
HttpConnectionParams.setSoTimeout(params, 4000);
第一行设置ConnectionPoolTimeout:这定义了从ConnectionManager管理的连接池中取出连接的超时时间,此处设置为1秒。
第二行设置ConnectionTimeout:这定义了通过网络与服务器建立连接的超时时间。Httpclient包中通过一个异步线程去创建与服务器的socket连接,这就是该socket连接的超时时间,此处设置为2秒。
第三行设置SocketTimeout:这定义了Socket读数据的超时时间,即从服务器获取响应数据需要等待的时间,此处设置为4秒。
以上3种超时分别会抛出ConnectionPoolTimeoutException,ConnectionTimeoutException与SocketTimeoutException。
封装简单的POST请求
有了单例的HttpClient对象,我们就可以把一些常用的发出GET和POST请求的代码也封装起来,写进我们的工具类中了。目前我仅仅实现发出POST请求并返回响应字符串的方法以供大家参考。将以下代码加入我们的CustomerHttpClient类中:
private static final String TAG = "CustomerHttpClient";
private static final String TAG = "CustomerHttpClient"; public static String post(String url, NameValuePair... params) { try { // 编码参数 List<NameValuePair> formparams = new ArrayList<NameValuePair>(); // 请求参数 for (NameValuePair p : params) { formparams.add(p); } UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, CHARSET); // 创建POST请求 HttpPost request = new HttpPost(url); request.setEntity(entity); // 发送请求 HttpClient client = getHttpClient(); HttpResponse response = client.execute(request); if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { throw new RuntimeException("请求失败"); } HttpEntity resEntity = response.getEntity(); return (resEntity == null) ? null : EntityUtils.toString(resEntity, CHARSET); } catch (UnsupportedEncodingException e) { Log.w(TAG, e.getMessage()); return null; } catch (ClientProtocolException e) { Log.w(TAG, e.getMessage()); return null; } catch (IOException e) { throw new RuntimeException("连接失败", e); } }
使用我们的CustomerHttpClient工具类
现在,在整个项目中我们都能很方便的使用该工具类来进行网络通信的业务代码编写了。下面的代码演示了如何使用username和password注册一个账户并得到新账户ID。
</pre><pre name="code" class="java"> final String url = "http://yourdomain/context/adduser"; //准备数据 NameValuePair param1 = new BasicNameValuePair("username", "张三"); NameValuePair param2 = new BasicNameValuePair("password", "123456"); int resultId = -1; try { // 使用工具类直接发出POST请求,服务器返回json数据,比如"{userid:12}" String response = CustomerHttpClient.post(url, param1, param2); JSONObject root = new JSONObject(response); resultId = Integer.parseInt(root.getString("userid")); Log.i(TAG, "新用户ID:" + resultId); } catch (RuntimeException e) { // 请求失败或者连接失败 Log.w(TAG, e.getMessage()); Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT); } catch (Exception e) { // JSon解析出错 Log.w(TAG, e.getMessage());
文章二、Android 网络数据的处理之 HttpClient
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.UnsupportedEncodingException;import java.net.URLEncoder;import java.util.ArrayList;import java.util.List;import java.util.Map; import org.apache.http.HttpResponse;import org.apache.http.HttpStatus;import org.apache.http.NameValuePair;import org.apache.http.client.ClientProtocolException;import org.apache.http.client.HttpClient;import org.apache.http.client.entity.UrlEncodedFormEntity;import org.apache.http.client.methods.HttpGet;import org.apache.http.client.methods.HttpPost;import org.apache.http.client.methods.HttpUriRequest;import org.apache.http.impl.client.DefaultHttpClient;import org.apache.http.message.BasicNameValuePair; import android.os.Handler;import android.util.Log; public class HttpConnectionUtil{ public static enum HttpMethod { GET, POST } /** * 异步连接 * * @param url * 网址 * @param method * Http方法,POST跟GET * @param callback * 回调方法,返回给页面或其他的数据 */ public void asyncConnect(final String url, final HttpMethod method, final HttpConnectionCallback callback) { asyncConnect(url, null, method, callback); } /** * 同步方法 * * @param url * 网址 * @param method * Http方法,POST跟GET * @param callback * 回调方法,返回给页面或其他的数据 */ public void syncConnect(final String url, final HttpMethod method, final HttpConnectionCallback callback) { syncConnect(url, null, method, callback); } /** * 异步带参数方法 * * @param url * 网址 * @param params * POST或GET要传递的参数 * @param method * 方法,POST或GET * @param callback * 回调方法 */ public void asyncConnect(final String url, final Map<String, String> params, final HttpMethod method, final HttpConnectionCallback callback) { Handler handler = new Handler(); Runnable runnable = new Runnable() { public void run() { syncConnect(url, params, method, callback); } }; handler.post(runnable); } /** * 同步带参数方法 * * @param url * 网址 * @param params * POST或GET要传递的参数 * @param method * 方法,POST或GET * @param callback * 回调方法 */ public void syncConnect(final String url, final Map<String, String> params, final HttpMethod method, final HttpConnectionCallback callback) { String json = null; BufferedReader reader = null; try { HttpClient client = new DefaultHttpClient(); HttpUriRequest request = getRequest(url, params, method); HttpResponse response = client.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { reader = new BufferedReader(new InputStreamReader(response .getEntity().getContent())); StringBuilder sb = new StringBuilder(); for (String s = reader.readLine(); s != null; s = reader .readLine()) { sb.append(s); } json = sb.toString(); } } catch (ClientProtocolException e) { Log.e("HttpConnectionUtil", e.getMessage(), e); } catch (IOException e) { Log.e("HttpConnectionUtil", e.getMessage(), e); } finally { try { if (reader != null) { reader.close(); } } catch (IOException e) { // ignore me } } callback.execute(json); } /** * POST跟GET传递参数不同,POST是隐式传递,GET是显式传递 * * @param url * 网址 * @param params * 参数 * @param method * 方法 * @return */ private HttpUriRequest getRequest(String url, Map<String, String> params, HttpMethod method) { if (method.equals(HttpMethod.POST)) { List<NameValuePair> listParams = new ArrayList<NameValuePair>(); if (params != null) { for (String name : params.keySet()) { listParams.add(new BasicNameValuePair(name, params .get(name))); } } try { UrlEncodedFormEntity entity = new UrlEncodedFormEntity( listParams); HttpPost request = new HttpPost(url); request.setEntity(entity); return request; } catch (UnsupportedEncodingException e) { // Should not come here, ignore me. throw new java.lang.RuntimeException(e.getMessage(), e); } } else { if (url.indexOf("?") < 0) { url += "?"; } if (params != null) { for (String name : params.keySet()) { try { url += "&" + name + "=" + URLEncoder.encode(params.get(name), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } HttpGet request = new HttpGet(url); return request; } } /** * 回调接口 * @author Administrator * */ public interface HttpConnectionCallback { /** * Call back method will be execute after the http request return. * * @param response * the response of http request. The value will be null if * any error occur. */ void execute(String response); } }
这个类也是我从网上看到的,使用起来相当方便,希望读者能学会怎样使用,其实像java学习,可以将一些有用的类或是方法定义个自己包,将它们放进去,下次要用的话只要在主程序中调用就行了,这也是面向对象重要的方法.
这里面的方法,我就没有一行一行定义说明了,里面用的都是HttpClient的中方法
接下来,我们用这个类来进行Android的应用
main.xml(模板文件)
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/http_edit" android:text="http://"> <requestFocus></requestFocus> </EditText> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/relativeLayout1"> <Button android:text="取消" android:layout_width="wrap_content" android:id="@+id/http_cancal" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentRight="true"></Button> <Button android:text="确定" android:layout_width="wrap_content" android:id="@+id/http_ok" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_toLeftOf="@+id/http_cancal" android:layout_marginRight="14dp"></Button> </RelativeLayout> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/http_text" android:text="TextView" android:textAppearance="?android:attr/textAppearanceSmall" android:layout_height="match_parent" android:layout_width="match_parent"></TextView> </ScrollView> </LinearLayout>
然后就是主Actitiv的java代码了
import android.app.Activity;import android.os.Bundle;import android.text.Html;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.EditText;import android.widget.TextView; import com.kang.http.HttpConnectionUtil;import com.kang.http.HttpConnectionUtil.HttpConnectionCallback;import com.kang.http.HttpConnectionUtil.HttpMethod; public class HttpClientDemo extends Activity{ private Button ok_btn; private Button cancal_btn; private EditText edit_text; private TextView text; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.http_client); //确定按钮 ok_btn = (Button) findViewById(R.id.http_ok); ok_btn.setOnClickListener(new ClickEvent()); //取消按钮 cancal_btn = (Button) findViewById(R.id.http_cancal); cancal_btn.setOnClickListener(new ClickEvent()); //文本编辑框 edit_text = (EditText) findViewById(R.id.http_edit); //文本框 text = (TextView) findViewById(R.id.http_text); } //自定义按钮点击方法 public class ClickEvent implements OnClickListener { @Override public void onClick(View v) { switch (v.getId()) { case R.id.http_ok: //网址 String url = edit_text.getText().toString().trim(); if (!url.equals("http://") && !url.equals("")) { //自定义类,封装了GET/POST方法,而且同样也封装了同步跟异步的方法 HttpConnectionUtil conn = new HttpConnectionUtil(); conn.asyncConnect(url, HttpMethod.GET, new HttpConnectionCallback() { @Override public void execute(String response) { text.setText(Html.fromHtml(response)); } }); } break; case R.id.http_cancal: edit_text.setText("http://"); break; } } }}
你一定会奇怪,怎么会有其他一些代码呢?呵呵,这里我们取出的是它的源代码.OK,这一章讲完了
文章三、Android网络优化之HttpClient
(1)采用单例模式(重用HttpClient实例)
对于一个通信单元甚至是整个应用程序,Apache强烈推荐只使用一个HttpClient的实例。例如:
private static HttpClient httpClient = null;
private static synchronized HttpClient getHttpClient() {
if(httpClient == null) {
final HttpParams httpParams = new BasicHttpParams();
httpClient = new DefaultHttpClient(httpParams);
}
return httpClient;
}
(2)保持连接(重用连接)
对于已经和服务端建立了连接的应用来说,再次调用HttpClient进行网络数据传输时,就不必重新建立新连接了,而可以重用已经建立的连接。这样无疑可以减少开销,提升速度。
在这个方面,Apache已经做了“连接管理”,默认情况下,就会尽可能的重用已有连接,因此,不需要客户端程序员做任何配置。只是需要注意,Apache的连接管理并不会主动释放建立的连接,需要程序员在不用的时候手动关闭连接。
(3)多线程安全管理的配置
如果应用程序采用了多线程进行网络访问,则应该使用Apache封装好的线程安全管理类ThreadSafeClientConnManager来进行管理,这样能够更有效且更安全的管理多线程和连接池中的连接。
(在网上也看到有人用MultiThreadedHttpConnectionManager进行线程安全管理的,后查了下Apache的API,发现MultiThreadedHttpConnectionManager是API 2.0中的类,而ThreadSafeClientConnManager是API 4.0中的类,比前者更新,所以选择使用ThreadSafeClientConnManager。另外,还看到有PoolingClientConnectionManager这个类,是API 4.2中的类,比ThreadSafeClientConnManager更新,但Android SDK中找不到该类。所以目前还是选择了ThreadSafeClientConnManager进行管理)
例如:
ClientConnectionManager manager = new ThreadSafeClientConnManager(httpParams, schemeRegistry);
httpClient = new DefaultHttpClient(manager, httpParams);
(4)大量传输数据时,使用“请求流/响应流”的方式
当需要传输大量数据时,不应使用字符串(strings)或者字节数组(byte arrays),因为它们会将数据缓存至内存。当数据过多,尤其在多线程情况下,很容易造成内存溢出(out of memory,OOM)。
而HttpClient能够有效处理“实体流(stream)”。这些“流”不会缓存至内存、而会直接进行数据传输。采用“请求流/响应流”的方式进行传输,可以减少内存占用,降低内存溢出的风险。
例如:
// Get method: getResponseBodyAsStream()
// not use getResponseBody(), or getResponseBodyAsString()
GetMethod httpGet = new GetMethod(url);
InputStream inputStream = httpGet.getResponseBodyAsStream();
// Post method: getResponseBodyAsStream()
PostMethod httpPost = new PostMethod(url);
InputStream inputStream = httpPost.getResponseBodyAsStream();
(5)持续握手(Expect-continue handshake)
在认证系统或其他可能遭到服务器拒绝应答的情况下(如:登陆失败),如果发送整个请求体,则会大大降低效率。此时,可以先发送部分请求(如:只发送请求头)进行试探,如果服务器愿意接收,则继续发送请求体。此项优化主要进行以下配置:
// use expect-continue handshake
HttpProtocolParams.setUseExpectContinue(httpParams, true);
(6)“旧连接”检查(Stale connection check)
HttpClient为了提升性能,默认采用了“重用连接”机制,即在有传输数据需求时,会首先检查连接池中是否有可供重用的连接,如果有,则会重用连接。同时,为了确保该“被重用”的连接确实有效,会在重用之前对其进行有效性检查。这个检查大概会花费15-30毫秒。关闭该检查举措,会稍微提升传输速度,但也可能出现“旧连接”过久而被服务器端关闭、从而出现I/O异常。
关闭旧连接检查的配置为:
// disable stale check
HttpConnectionParams.setStaleCheckingEnabled(httpParams, false);
(7)超时设置
进行超时设置,让连接在超过时间后自动失效,释放占用资源。
// timeout: get connections from connection pool
ConnManagerParams.setTimeout(httpParams, 1000);
// timeout: connect to the server
HttpConnectionParams.setConnectionTimeout(httpParams, DEFAULT_SOCKET_TIMEOUT);
// timeout: transfer data from server
HttpConnectionParams.setSoTimeout(httpParams, DEFAULT_SOCKET_TIMEOUT);
(8)连接数限制
配置每台主机最多连接数和连接池中的最多连接总数,对连接数量进行限制。其中,DEFAULT_HOST_CONNECTIONS和DEFAULT_MAX_CONNECTIONS是由客户端程序员根据需要而设置的。
// set max connections per host
ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new ConnPerRouteBean(DEFAULT_HOST_CONNECTIONS));
// set max total connections
ConnManagerParams.setMaxTotalConnections(httpParams, DEFAULT_MAX_CONNECTIONS);
经过优化后,上一篇日志中的getHttpClient()方法代码如下:
- private static synchronized HttpClient getHttpClient() {
- if(httpClient == null) {
- final HttpParams httpParams = new BasicHttpParams();
- // timeout: get connections from connection pool
- ConnManagerParams.setTimeout(httpParams, 1000);
- // timeout: connect to the server
- HttpConnectionParams.setConnectionTimeout(httpParams, DEFAULT_SOCKET_TIMEOUT);
- // timeout: transfer data from server
- HttpConnectionParams.setSoTimeout(httpParams, DEFAULT_SOCKET_TIMEOUT);
- // set max connections per host
- ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new ConnPerRouteBean(DEFAULT_HOST_CONNECTIONS));
- // set max total connections
- ConnManagerParams.setMaxTotalConnections(httpParams, DEFAULT_MAX_CONNECTIONS);
- // use expect-continue handshake
- HttpProtocolParams.setUseExpectContinue(httpParams, true);
- // disable stale check
- HttpConnectionParams.setStaleCheckingEnabled(httpParams, false);
- HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1);
- HttpProtocolParams.setContentCharset(httpParams, HTTP.UTF_8);
- HttpClientParams.setRedirecting(httpParams, false);
- // set user agent
- String userAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2) Gecko/20100115 Firefox/3.6";
- HttpProtocolParams.setUserAgent(httpParams, userAgent);
- // disable Nagle algorithm
- HttpConnectionParams.setTcpNoDelay(httpParams, true);
- HttpConnectionParams.setSocketBufferSize(httpParams, DEFAULT_SOCKET_BUFFER_SIZE);
- // scheme: http and https
- SchemeRegistry schemeRegistry = new SchemeRegistry();
- schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
- schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
- ClientConnectionManager manager = new ThreadSafeClientConnManager(httpParams, schemeRegistry);
- httpClient = new DefaultHttpClient(manager, httpParams);
- }
- return httpClient;
- }
附录:关于HttpURLConnection的优化,网上资料不多。从Android官网上看到一点,整理如下:
(1)上传数据至服务器时(即:向服务器发送请求),如果知道上传数据的大小,应该显式使用setFixedLengthStreamingMode(int)来设置上传数据的精确值;如果不知道上传数据的大小,则应使用setChunkedStreamingMode(int)——通常使用默认值“0”作为实际参数传入。如果两个函数都未设置,则系统会强制将“请求体”中的所有内容都缓存至内存中(在通过网络进行传输之前),这样会浪费“堆”内存(甚至可能耗尽),并加重隐患。
(2)如果通过流(stream)输入或输出少量数据,则需要使用带缓冲区的流(如BufferedInputStream);大量读取或输出数据时,可忽略缓冲流(不使用缓冲流会增加磁盘I/O,默认的流操作是直接进行磁盘I/O的);
(3)当需要传输(输入或输出)大量数据时,使用“流”来限制内存中的数据量——即:将数据直接放在“流”中,而不是存储在字节数组或字符串中(这些都存储在内存中)。
- android网络处理HttpClient
- Android HttpClient网络通信
- Android HttpClient网络通信
- Android HttpClient网络通信
- Android HttpClient网络通信
- Android 网络编程 HttpClient
- android网络编程--HttpClient
- Android网络编程之httpclient
- Android网络优化之HttpClient
- Android网络优化之HttpClient
- Android网络优化之HttpClient
- Android网络优化之HttpClient
- Android网络HttpURLConnection和HttpClient
- Android网络优化之HttpClient
- Android网络优化之HttpClient
- Android网络编程之HttpClient
- Android网络优化之HttpClient
- Android网络编程 HttpClient Socket
- Windows不能用鼠标双击运行jar文件
- 第4周项目2-分数类的雏形
- IOS 内存泄漏 选择图片上传内存泄露
- 第四周(默认构造函数 无参为1)
- Interactive pivot tables with R
- android网络处理HttpClient
- 一个空类被编译器编译后产生了哪些默认函数
- 作为软件测试工程师你应该怎么规划你的职业发展
- java中的Iterator和Iterable 区别
- java jcmd命令(Java Command 内存分析工具使用)
- Coding上部署Ghost博客
- __FILE__预定义宏
- 第4周项目1-三角形类的构造函数
- 第四周程序阅读(2)