Socket通信

基本概念

对于程序开发来说,网络通信的基础就是Socket,但因为是基础,所以用起来不容易,今天我们就来谈谈Socket通信。计算机网络有个大名鼎鼎的TCP/IP协议,普通用户在电脑上设置本地连接的ip时,便经常看到下图的弹窗,注意红框部分已经很好地描述了TCP/IP协议的作用。

TCP/IP是个协议组,它分为三个层次:网络层、传输层和应用层:
网络层包括:IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。
传输层包括:TCP协议、UDP协议。
应用层包括:HTTP、FTP、TELNET、SMTP、DNS等协议。
之前我们提到的网络编程,其实都是应用层方面的http或ftp编程;而socket属于传输层的技术,它的api实现TCP协议后即可用于http通信,实现UDP协议后即可用于ftp通信,当然也可以直接在底层进行点对点通信,比如即时通信软件(QQ、微信)这样就是。

扯远了,言归正传,java的socket编程主要使用Socket和ServerSocket两个类,下面是相关类的使用说明。

Socket

Socket是最常用的,客户端和服务端都要用到,它描述了两边对套接字(即Socket)处理的一般行为,主要方法说明如下:
connect : 连接指定ip和端口。该方法用于客户端连接服务端。
getInputStream : 获取输入流。即自己收到对方发过来的数据。
getOutputStream : 获取输入流。即自己向对方发送的数据。
getInetAddress : 获取网络地址对象。该对象是一个InetAddress实例。
isConnected : 判断socket是否连上。
isClosed : 判断socket是否关闭。
close : 关闭socket。

ServerSocket

ServerSocket仅用于服务端,它在运行时不停地侦听指定端口,主要方法说明如下:
构造函数 : 指定侦听哪个端口。
accept : 开始接收客户端的连接。有客户端连上时就返回一个Socket对象,若要持续侦听连接,得在循环中调用该函数。
getInetAddress : 获取网络地址对象。该对象是一个InetAddress实例。
isClosed : 判断socket服务器是否关闭。
close : 关闭socket服务器。

InetAddress

InetAddress是对网络地址的一个封装,主要方法说明如下:
getByName : 根据主机ip/名称获取InetAddress对象。
getHostAddress : 获取主机的ip地址。
getHostName : 获取主机的名称。
isReachable : 判断该地址是否可到达,即是否连通。

聊天室应用

实现原理

Socket在app开发中主要用于聊天/即时通信,因为涉及到客户端与服务端的交互,所以流程稍微复杂。首先要划分聊天业务的功能点,以QQ为例,我们平常看到的是三种页面:登录页面、好友列表页面、聊天页面。因此,对应的Socket功能也分为三类:
登录/注销:登录操作对应建立Socket连接,而注销操作对应断开Socket连接。
获取好友列表:与Socket有关的是获取当前在线的好友列表,客户端到服务端查询当前已建立Socket连接的好友列表。
发送消息/接收消息:发送/接收消息对应的是Socket的数据传输,发送消息操作是客户端A向服务端发送Socket数据,接收消息操作是服务端将收到的A消息向客户端B发送Socket数据。

其次在app端需要实现以下功能:
1、至少三个页面:登录页面、好友列表页面、聊天页面;
2、一个用于Socket通信的线程。由于在app运行过程中都要保持Socket连接,因此该Socket线程要放在自定义的Application类中。
3、页面向Socket线程发送消息的机制,用于登录请求、注销请求、获取好友列表请求、发送消息等等。主线程与子线程通信,我们这里采用Handler+Message机制来处理,有关该机制的使用说明参见《 Android开发笔记(四十八)Thread类实现多线程》。
4、Socket线程向页面发送消息的机制,用于返回好友列表、接收消息等等。因为返回消息会分发到不同的页面,采用Handler机制有困难,所以这里我们采用Broadcast广播来处理,在好友列表页面和聊天页面各注册一个广播接收器,用于根据服务器返回数据刷新UI。有关Broadcast及其接收器的使用说明参见《 Android开发笔记(四十二)Broadcast的生命周期》。

然后在服务端启动Socket服务器,要实现的功能有:
1、定义一个Socket连接的队列,用于保存当前连上的Socket请求;
2、循环侦听指定端口,一旦有新连接进来,则将该连接加入Socket队列,并启动新线程为该连接服务;
3、每个服务线程持续从Socket中读取客户端发过来的数据,并对不同请求做相应的处理:
a、如果是登录请求,则标识该Socket连接的用户昵称、设备编号、登录时间等信息;
b、如果是注销请求,则断开Socket连接,并从Socket队列中移除该连接;
c、如果是获取好友列表请求,则遍历Socket队列,封装好友列表数据并返回;
d、如果是发送消息请求,则根据好友的设备编号到Socket队列中查找对应的Socket连接,并向该连接返回消息内容;

最后还要定义一下服务端与客户端之间传输消息的格式,按惯例消息包分为包头与包体两块,包头用于标识操作类型、操作对象、操作时间等基本要素,而包体用于存放具体的消息内容(如好友列表、消息文本等等)。demo工程为简单起见,就不用xml或json等标准格式,直接用分隔符划分包头与包体,以及包头内部的各元素。

效果截图

博主在测试时,模拟器上开了一个app,登录名称是“在水一方”,真机上开了一个app,登录名称是“振兴中华”,两个app连的都是电脑上的Socket服务,从而模拟真实的聊天室环境。登录页面与好友列表页面比较简单,就不再截图了,截的都是聊天窗口页面。为了做得更逼真,中间消息窗口采用对方消息靠左对齐,我方消息靠右对齐的布局,并给双方消息着不同的背景色。具体截图如下,左侧图片是真机截图,右侧图片是模拟器截图。


代码示例

app端

几个注意点:
1、自定义Application类需要采用单例模式,确保Socket线程的唯一性,详细原因参见《 Android开发笔记(八十九)单例模式》。
2、Socket数据包不可直接用换行符“\n”做分隔符,因为在Socket通信中,换行符表示该数据包结束了,所以加了一个换行符,原来一个数据包就变成了两个数据包。正因如此,用户聊天的消息文本中若有换行符,则要先进行转义后才能发给Socket传输。
3、如果广播接收器在代码中动态注册,则不会收到Socket线程发出的广播消息;只有在AndroidManifest.xml中对接收器做静态注册,才能收到Socket线程发出的广播消息,具体原因不明,可能与线程有关。

下面是自定义Application类的代码:

import com.example.exmsocket.thread.ClientThread;
import com.example.exmsocket.util.DateUtil;import android.app.Application;
import android.os.Build;
import android.os.Message;
import android.util.Log;public class MainApplication extends Application {private static final String TAG = "MainApplication";private static MainApplication mApp;private String mNickName;private ClientThread mClientThread;public static MainApplication getInstance() {return mApp;}@Overridepublic void onCreate() {super.onCreate();mApp = this;mClientThread = new ClientThread(mApp);new Thread(mClientThread).start();}public void sendAction(String action, String otherId, String msgText) {String content = String.format("%s,%s,%s,%s,%s%s%s\r\n", action, Build.SERIAL, getNickName(), DateUtil.getNowTime(), otherId, ClientThread.SPLIT_LINE, msgText);Log.d(TAG, "sendAction : " + content);Message msg = Message.obtain();msg.obj = content;if (mClientThread==null || mClientThread.mRecvHandler==null) {Log.d(TAG, "mClientThread or its mRecvHandler is null");} else {mClientThread.mRecvHandler.sendMessage(msg);}}public void setNickName(String nickName) {mApp.mNickName = nickName;}public String getNickName() {return mApp.mNickName;}}

下面是登录页面的代码:

import com.example.exmsocket.thread.ClientThread;import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;public class MainActivity extends Activity implements OnClickListener {private static final String TAG = "MainActivity";private EditText et_name;private Button btn_ok;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);et_name = (EditText) findViewById(R.id.et_name);btn_ok = (Button) findViewById(R.id.btn_ok);btn_ok.setOnClickListener(this);}@Overridepublic void onClick(View v) {if (v.getId() == R.id.btn_ok) {String nickName = et_name.getText().toString().trim();if (nickName.length() <= 0) {Toast.makeText(this, "请输入您的昵称", Toast.LENGTH_SHORT).show();} else {MainApplication.getInstance().setNickName(nickName);MainApplication.getInstance().sendAction(ClientThread.LOGIN, "", "");Intent intent = new Intent(this, FriendListActivity.class);startActivity(intent);finish();}}}
}

下面是好友列表页面的代码:

import java.util.ArrayList;import com.example.exmsocket.adapter.Friend;
import com.example.exmsocket.adapter.FriendListAdapter;
import com.example.exmsocket.thread.ClientThread;import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;public class FriendListActivity extends Activity implements OnClickListener {private static final String TAG = "FriendListActivity";private static Context mContext;private static ListView lv_friend;private Button btn_refresh;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_list);mContext = getApplicationContext();lv_friend = (ListView) findViewById(R.id.lv_friend);btn_refresh = (Button) findViewById(R.id.btn_refresh);btn_refresh.setOnClickListener(this);}@Overrideprotected void onResume() {mHandler.postDelayed(mRefresh, 500);super.onResume();}@Overrideprotected void onDestroy() {MainApplication.getInstance().sendAction(ClientThread.LOGOUT, "", "");super.onDestroy();}private Handler mHandler = new Handler();private Runnable mRefresh = new Runnable() {@Overridepublic void run() {MainApplication.getInstance().sendAction(ClientThread.GETLIST, "", "");}};@Overridepublic void onClick(View v) {if (v.getId() == R.id.btn_refresh) {mHandler.post(mRefresh);}}public static class GetListReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {if (intent != null) {Log.d(TAG, "onReceive");String content = intent.getStringExtra(ClientThread.CONTENT);if (mContext != null && content != null && content.length() > 0) {int pos = content.indexOf(ClientThread.SPLIT_LINE);String head = content.substring(0, pos);String body = content.substring(pos + 1);String[] splitArray = head.split(ClientThread.SPLIT_ITEM);if (splitArray[0].equals(ClientThread.GETLIST)) {String[] bodyArray = body.split("\\|");ArrayList<Friend> friendList = new ArrayList<Friend>();for (int i = 0; i < bodyArray.length; i++) {String[] itemArray = bodyArray[i].split(ClientThread.SPLIT_ITEM);if (bodyArray[i].length() > 0 && itemArray != null && itemArray.length >= 3) {friendList.add(new Friend(itemArray[0], itemArray[1], itemArray[2]));}}if (friendList.size() > 0) {FriendListAdapter adapter = new FriendListAdapter(mContext, friendList);lv_friend.setAdapter(adapter);lv_friend.setOnItemClickListener(adapter);}} else {String hint = String.format("%s\n%s", splitArray[0], body);Toast.makeText(mContext, hint, Toast.LENGTH_SHORT).show();}}}}}}

下面是聊天页面的代码:

import com.example.exmsocket.thread.ClientThread;
import com.example.exmsocket.util.DateUtil;
import com.example.exmsocket.util.MetricsUtil;import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;public class ChatActivity extends Activity implements OnClickListener {private static final String TAG = "ChatActivity";private static Context mContext;private TextView tv_other;private EditText et_input;private static TextView tv_show;private static LinearLayout ll_show;private Button btn_send;private String mOtherId;private static int dip_margin;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_chat);mContext = getApplicationContext();tv_other = (TextView) findViewById(R.id.tv_other);et_input = (EditText) findViewById(R.id.et_input);tv_show = (TextView) findViewById(R.id.tv_show);ll_show = (LinearLayout) findViewById(R.id.ll_show);btn_send = (Button) findViewById(R.id.btn_send);btn_send.setOnClickListener(this);dip_margin = MetricsUtil.dip2px(mContext, 5);Bundle bundle = getIntent().getExtras();mOtherId = bundle.getString("otherId", "");String desc = String.format("与%s聊天", bundle.getString("otherName", ""));tv_other.setText(desc);}@Overridepublic void onClick(View v) {if (v.getId() == R.id.btn_send) {String body = et_input.getText().toString();String append = String.format("%s %s\n%s",MainApplication.getInstance().getNickName(),DateUtil.formatTime(DateUtil.getNowTime()), body);appendMsg(Build.SERIAL, append);MainApplication.getInstance().sendAction(ClientThread.SENDMSG, mOtherId, body);et_input.setText("");}}private static void appendMsg(String deviceId, String append) {//tv_show.setText(tv_show.getText().toString() + append);int gravity = deviceId.equals(Build.SERIAL) ? Gravity.RIGHT : Gravity.LEFT;int bg_color = deviceId.equals(Build.SERIAL) ? 0xffccccff : 0xffffcccc;LinearLayout ll_append = new LinearLayout(mContext);LinearLayout.LayoutParams ll_params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);ll_params.setMargins(dip_margin, dip_margin, dip_margin, dip_margin);ll_append.setLayoutParams(ll_params);ll_append.setGravity(gravity);TextView tv_append = new TextView(mContext);tv_append.setText(tv_show.getText().toString() + append);tv_append.setTextColor(Color.BLACK);LinearLayout.LayoutParams tv_params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);tv_append.setLayoutParams(tv_params);tv_append.setBackgroundColor(bg_color);ll_append.addView(tv_append);ll_show.addView(ll_append);}public static class RecvMsgReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {if (intent != null) {Log.d(TAG, "onReceive");String content = intent.getStringExtra(ClientThread.CONTENT);if (mContext != null && content != null && content.length() > 0) {int pos = content.indexOf(ClientThread.SPLIT_LINE);String head = content.substring(0, pos);String body = content.substring(pos + 1);String[] splitArray = head.split(ClientThread.SPLIT_ITEM);if (splitArray[0].equals(ClientThread.RECVMSG)) {String append = String.format("%s %s\n%s",splitArray[2], DateUtil.formatTime(splitArray[3]), body);appendMsg(splitArray[1], append);} else {String hint = String.format("%s\n%s", splitArray[0], body);Toast.makeText(mContext, hint, Toast.LENGTH_SHORT).show();}}}}}}

下面是Socket线程的代码:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;public class ClientThread implements Runnable {private static final String TAG = "ClientThread";private static final String SOCKET_IP = "192.168.0.212";  // 模拟器使用//private static final String SOCKET_IP = "192.168.253.1";  // 真机使用private static final int SOCKET_PORT = 52000;public static String ACTION_RECV_MSG = "com.example.exmsocket.RECV_MSG";public static String ACTION_GET_LIST = "com.example.exmsocket.GET_LIST";public static String CONTENT = "CONTENT";public static String SPLIT_LINE = "|";public static String SPLIT_ITEM = ",";public static String LOGIN = "LOGIN";public static String LOGOUT = "LOGOUT";public static String SENDMSG = "SENDMSG";public static String RECVMSG = "RECVMSG";public static String GETLIST = "GETLIST";private Context mContext;private Socket mSocket;// 定义接收UI线程的Handler对象public Handler mRecvHandler;private BufferedReader mReader = null;private OutputStream mWriter = null;public ClientThread(Context context) {mContext = context;}@Override  public void run() {mSocket = new Socket();try {mSocket.connect(new InetSocketAddress(SOCKET_IP, SOCKET_PORT), 3000);mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));mWriter = mSocket.getOutputStream();// 启动一条子线程来读取服务器相应的数据  new Thread() {@Override  public void run() {String content = null;try {while ((content = mReader.readLine()) != null) {// 读取到来自服务器的数据之后,发送消息通知ClientThread.this.notify(0, content);}} catch (Exception e) {e.printStackTrace();ClientThread.this.notify(97, e.getMessage());}}}.start();Looper.prepare();mRecvHandler = new Handler() {@Override  public void handleMessage(Message msg) {// 接收到UI线程的中用户输入的数据try {mWriter.write(msg.obj.toString().getBytes("utf8"));} catch (Exception e) {e.printStackTrace();ClientThread.this.notify(98, e.getMessage());}}};Looper.loop();} catch (Exception e) {e.printStackTrace();notify(99, e.getMessage());}}private void notify(int type, String message) {if (type == 99) {String content = String.format("%s%s%s%s", "ERROR", SPLIT_ITEM, SPLIT_LINE, message);Intent intent1 = new Intent(ACTION_RECV_MSG);intent1.putExtra(CONTENT, content);mContext.sendBroadcast(intent1);Intent intent2 = new Intent(ACTION_GET_LIST);intent2.putExtra(CONTENT, content);mContext.sendBroadcast(intent2);} else {int pos = message.indexOf(SPLIT_LINE);String head = message.substring(0, pos-1);String[] splitArray = head.split(SPLIT_ITEM);String action = "";if (splitArray[0].equals(RECVMSG)) {action = ACTION_RECV_MSG;} else if (splitArray[0].equals(GETLIST)) {action = ACTION_GET_LIST;}Log.d(TAG, "action="+action+", message="+message);Intent intent = new Intent(action);intent.putExtra(CONTENT, message);mContext.sendBroadcast(intent);}}}

服务端

下面是服务端的主程序代码:

import java.net.ServerSocket;
import java.util.ArrayList;public class ChatServer {private static final int SOCKET_PORT = 52000;public static ArrayList<SocketBean> mSocketList = new ArrayList<SocketBean>();private void initServer() {try {// 创建一个ServerSocket,用于监听客户端Socket的连接请求ServerSocket server = new ServerSocket(SOCKET_PORT);while (true) {// 每当接收到客户端的Socket请求,服务器端也相应的创建一个SocketSocketBean socket = new SocketBean(DateUtil.getTimeId(), server.accept());mSocketList.add(socket);// 每连接一个客户端,启动一个ServerThread线程为该客户端服务new Thread(new ServerThread(socket)).start();}} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) {ChatServer server = new ChatServer();server.initServer();}
}

下面是服务线程的代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;public class ServerThread implements Runnable {private SocketBean mSocket = null;private BufferedReader mReader = null;public ServerThread(SocketBean mSocket) throws IOException {this.mSocket = mSocket;mReader = new BufferedReader(new InputStreamReader(mSocket.socket.getInputStream()));}@Overridepublic void run() {try {String content = null;// 循环不断地从Socket中读取客户端发送过来的数据while ((content = mReader.readLine()) != null) {System.out.println("content="+content);int pos = content.indexOf("|");// 包头格式为:动作名称|设备编号|昵称|时间|对方设备编号String head = content.substring(0, pos);String body = content.substring(pos+1);String[] splitArray = head.split(",");String action = splitArray[0];System.out.println("action="+action);if (action.equals("LOGIN")) {login(splitArray[1], splitArray[2], splitArray[3]);} else if (action.equals("LOGOUT")) {logout(splitArray[1]);break;} else if (action.equals("SENDMSG")) {sendmsg(splitArray[2], splitArray[4], splitArray[1], body);} else if (action.equals("GETLIST")) {getlist(splitArray[1]);}}} catch (Exception e) {e.printStackTrace();}}private void login(String deviceId, String nickName, String loginTime) throws IOException {for (int i=0; i<ChatServer.mSocketList.size(); i++) {SocketBean item = ChatServer.mSocketList.get(i);if (item.id.equals(mSocket.id)) {item.deviceId = deviceId;item.nickName = nickName;item.loginTime = loginTime;ChatServer.mSocketList.set(i, item);break;}}}private String getFriend() {String friends = "GETLIST,";for (SocketBean item : ChatServer.mSocketList) {if (item.deviceId!=null && item.deviceId.length()>0) {String friend = String.format("|%s,%s,%s", item.deviceId, item.nickName, item.loginTime);friends += friend;}}return friends;}private void getlist(String deviceId) throws IOException {for (int i=0; i<ChatServer.mSocketList.size(); i++) {SocketBean item = ChatServer.mSocketList.get(i);if (item.id.equals(mSocket.id) && item.deviceId.equals(deviceId)) {PrintStream printStream = new PrintStream(item.socket.getOutputStream());printStream.println(getFriend());break;}}}private void logout(String deviceId) throws IOException {for (int i=0; i<ChatServer.mSocketList.size(); i++) {SocketBean item = ChatServer.mSocketList.get(i);if (item.id.equals(mSocket.id) && item.deviceId.equals(deviceId)) {PrintStream printStream = new PrintStream(item.socket.getOutputStream());printStream.println("LOGOUT,|");item.socket.close();ChatServer.mSocketList.remove(i);break;}}}private void sendmsg(String otherName, String otherId, String selfId, String message) throws IOException {for (int i=0; i<ChatServer.mSocketList.size(); i++) {SocketBean item = ChatServer.mSocketList.get(i);if (item.deviceId.equals(otherId)) {String content = String.format("%s,%s,%s,%s|%s", "RECVMSG", selfId, otherName, DateUtil.getNowTime(), message);PrintStream printStream = new PrintStream(item.socket.getOutputStream());printStream.println(content);break;}}}}

点此查看Android开发笔记的完整目录

__________________________________________________________________________

博主现已开通微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。

Android开发笔记(一百一十一)聊天室中的Socket通信相关推荐

  1. Android开发笔记(六十一)文件下载管理DownloadManager

    下载管理DownloadManager 文件下载其实是网络数据访问的一种特殊形式,使用普通的http请求也能完成,就是实现起来会繁琐一些.因为下载功能比较常用,而且业务功能相对统一,所以从Androi ...

  2. Android开发笔记(五十一)通过Messenger实现进程间通信

    进程间通信IPC IPC是"Inter-Process Communication"的缩写,即进程间通信.Android为APP提供了多进程工作模式,这是因为多线程存在若干局限: ...

  3. Android开发笔记(八十一)屏幕规格适配

    Configuration 适配各种屏幕规格,首先要取到系统对于屏幕的配置信息,这些配置可从工具类Configuration获得.Configuration对象在Activity中通过调用getRes ...

  4. Android开发笔记(七十一)区分开发模式和上线模式

    为什么要区分两种模式 许多开发者(包括博主在内)都是闷骚的程序员,为了开发调试方便,常常在代码里加上日志,还经常在页面上各种弹窗提示.这固然有利于发现bug.提高软件质量,但过多的调试信息往往容易泄露 ...

  5. Android开发笔记(四十一)Service的生命周期

    与生命周期有关的方法 onCreate : 创建服务 onStart : 开始服务,Android2.0以下版本使用 onStartCommand : 开始服务,Android2.0及以上版本使用.该 ...

  6. Android开发笔记(三十一)SQLite游标及其数据结构

    ContentValues ContentValues类似于映射,也是用于存储键值对.区别之处在于ContentValues的键只能是字符串,查看ContentValues的源码,会发现其内部保存键值 ...

  7. Android开发笔记(二十一)横幅轮播页Banner

    ViewPager ViewPager的概念 在前面的博文< Android开发笔记(十九)底部标签栏TabBar>中,我们提到可以在一个主页面里通过选项卡方式,切换到不同的子页面.那么在 ...

  8. Android开发笔记(序)写在前面的目录

    知识点分类 一方面写写自己走过的弯路掉进去的坑,避免以后再犯:另一方面希望通过分享自己的经验教训,与网友互相切磋,从而去芜存菁进一步提升自己的水平.因此博主就想,入门的东西咱就不写了,人不能老停留在入 ...

  9. Android开发笔记(序)

    本开发笔记,借鉴与其他开发者整理的文章范例与心得体会.在这里作为开发过程中的一个总结与笔记式记录. 如有侵犯作者权益,请及时联系告知删除.俗话说:集百家成一言,去粕成金. ************** ...

最新文章

  1. tomcat部署与Context
  2. Java中的数据结构
  3. 获取字段 命名空间和类_系统架构之命名规范
  4. Javascript性能优化【内联缓存】 V8引擎特性
  5. 服务端增加WCF服务全局异常处理机制
  6. Picture Box(1) 修正版
  7. nginx try_files
  8. WAF实现扫描器识别
  9. 随机森林模型sklearn_sklearn之随机森林
  10. 孙玄:转转如何打造AI工程架构体系
  11. ar5b97无线网卡驱动linux 版下载,atheros ar5b97驱动
  12. SVN:客户端与服务器端安装、配置与使用
  13. sublimeText3配置sublimeLinter
  14. idea下 git无法提交空的文件夹
  15. linux安装beyondcompare
  16. 八种酒吧里最时尚的喝酒法
  17. 创建Excel,创建pdf
  18. Cisco 开启三层交换机ip routing
  19. HX711电子秤数据转化原理
  20. mysql的安全权限_MySQL 权限与安全

热门文章

  1. Algorithm:递归思想及实例分析
  2. 蓝桥杯 算法训练 安慰奶牛
  3. oracle-j2sdk1.8,CDH agent无法安装
  4. 1.1 版本控管与Git
  5. Java 中使用JDBC连接数据库例程与注意事项
  6. php连贯操作,Thinkphp 3.2.3 sql的一些连贯操作方法
  7. python图片矫正后对比_python库skimage 对图像进行gamma校正和log校正
  8. 上海计算机二级python难吗_计算机二级难吗?
  9. android h5链接蓝牙,h5+runtime Native.js 混合APP连接蓝牙打印机
  10. android 程序更换字体,Android修改自己程序字体的方法详解