編輯:關於Android編程
Socket通信是網絡通信中最常用的技術之一,通過Socket建立的可靠連接,可以讓多個終端與服務器保持通信,最典型的應用是建立一個多人聊天程序。本實例使用ServerSocket建立聊天服務器。將服務器端所有的通訊線程保存到一個集合當中,當有用戶發來數據,則轉發給所有用戶,實現聊天室效果。Android端通過使用Socket建立客戶端鏈接,並且在AsyncTask中執行網絡讀寫的任務,將用戶輸入的內容發送到服務器,並接收服務器發來的數據,顯示到界面上。開啟多個虛擬機模擬多人聊天效果。
1,建立socket服務
2,等待連接
3,將建立的連接放在新的線程裡
4,由於每個socket客戶端連接相互獨立,所以他們之間無法通信
5,使用一個類對新建的客戶端線程進行管理,然後實現相互通信,管理類要做成單例,保證唯一性,並通過服務類進行轉發來實現客戶端相互通信
2,將db_chat.sql導入到mysql數據庫
3,將SocketChatServer項目導入到Eclipse,配置Tomcat下運行。
修改hibernate配置文件,改成修改成自己的環境
4,將SocketChat項目導入到Eclipse,配置Android環境並編譯運行。
使用Servlet init方法中調用啟動線程,並在web.xml配置自啟動。就可以在服務啟動的時候啟動socket服務。
package com.jie.socket.chat; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; /** * @ClassName: MyServerSocket.java * @Description: TODO(Socket服務端) * @author yangjie * @version V1.0 * @Date 2016年5月23日 下午3:03:16 */ public class MyServerSocket extends HttpServlet { private static final long serialVersionUID = 1L; @Override public void init() throws ServletException { // TODO Auto-generated method stub new ServerListenerThread().start();// 開啟線程 System.out.println("開啟socket服務…………………………"); // 在windows cmd下輸入 // telnet localhost 12345 // 即可建立socket連接 } }
用於開啟socket服務,並進行監聽客戶端連接
客戶端首次接入,傳遞自己的名稱,格式“name:***”
將連接的客戶端,傳入到客戶端管理類中。
package com.jie.socket.chat; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import javax.swing.JOptionPane; /** * @ClassName: ServerListenerThread.java * @Description: TODO(ServerSoket監聽線程) * @author yangjie * @version V1.0 * @Date 2016年5月23日 下午3:38:17 */ public class ServerListenerThread extends Thread { int i=1001; @Override public void run() { // TODO Auto-generated method stub super.run(); try { ServerSocket serverSocket = new ServerSocket(12345); // 循環監聽連接 while (true) { // 阻塞block Socket socket = serverSocket.accept(); // 建立連接 // 在浏覽器中輸入:http://localhost:12345/ // 會彈出提示框,點擊確定後斷開連接 //JOptionPane.showMessageDialog(null, "有客戶端連接到本機"); // 將Socket傳遞給新的線程,每個socket享受獨立的連接 // new ChatSocket(socket).start();//開啟chatSocket線程 BufferedReader br = new BufferedReader(new InputStreamReader( socket.getInputStream(), "utf-8")); String line = null; String name = null; if ((line = br.readLine())!=null) {//接收到客戶端數據 if(line.indexOf("name:")!=-1){ name = line.substring(line.indexOf("name:")+5); System.out.println(name+":連接到本機"); } } //br.close();//關閉輸入流 ChatSocket chatSocket = new ChatSocket(socket, name); chatSocket.start(); ChatManager.getChatManager().add(chatSocket); i++; } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
Socket客戶端線程,通過該線程類發送和接收信息。
package com.jie.socket.chat; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.Socket; /** * @ClassName: ChatSocket.java * @Description: TODO(socket客戶端線程) * @author yangjie * @version V1.0 * @Date 2016年5月23日 下午3:41:13 */ public class ChatSocket extends Thread { Socket socket; String sName; /** * 構造函數 */ public ChatSocket(Socket socket, String sName) { // TODO Auto-generated constructor stub this.socket = socket; this.sName = sName; } @Override public void run() { // TODO Auto-generated method stub // 輸入 try { BufferedReader br = new BufferedReader(new InputStreamReader( socket.getInputStream(), "utf-8")); String line = null; while ((line = br.readLine()) != null) {// 接收到客戶端數據 System.out.println(getsName() + ":" + line); ChatManager.getChatManager().publish(this, line); } br.close();// 關閉輸入流 } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void out(String out) { // TODO Auto-generated method stub try { socket.getOutputStream().write(out.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block if ("Socket is closed".equals(e.getMessage())) {// 對於已經關閉的連接從管理器中刪除 System.out.println(e.getMessage()); ChatManager.getChatManager().remove(this); } // e.printStackTrace(); } } public String getsName() { return sName; } public void setsName(String sName) { this.sName = sName; } }
用於管理客戶端,保存所有客戶端信息,傳輸客戶端發送的信息。
package com.jie.socket.chat; import java.util.Vector; /** * @ClassName: ChatManager.java * @Description: TODO(socket客戶端連接管理類,單例) * @author yangjie * @version V1.0 * @Date 2016年5月23日 下午4:25:38 */ public class ChatManager { private static final ChatManager chatManager = new ChatManager(); private ChatManager(){ } public static ChatManager getChatManager(){ return chatManager; } Vectorvector = new Vector (); /** * @Title: add * @Description: TODO(向集合中增加ChatSocket) * @param chatSocket * @return: void * @throws */ public void add(ChatSocket chatSocket){ vector.add(chatSocket); } public void remove(ChatSocket chatSocket){ vector.remove(chatSocket); } /** * @Title: publish * @Description: TODO(向其他ChatSocket連接發送信息) * @param chatSocket * @param out * @return: void * @throws */ public void publish(ChatSocket chatSocket,String out){ for(int i =0;i
1.3.5Hibernate工具類
hibernate工具類,用於連接數據庫,操作數據庫
package com.jie.socket.chart.util; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import org.hibernate.service.ServiceRegistry; import org.hibernate.service.ServiceRegistryBuilder; /** * @ClassName: HibernateUtil.java * @Description: TODO(Hibernate數據庫工具類) * @author yangjie * @version V1.0 * @Date 2016-5-16 上午11:42:11 */ @SuppressWarnings("deprecation") public class HibernateUtil { private static SessionFactory sessionFacotry = buildSessionFactory(); private static SessionFactory buildSessionFactory() { // TODO Auto-generated method stub try { Configuration cfg = new Configuration(); cfg.configure(); ServiceRegistry sr = new ServiceRegistryBuilder().applySettings( cfg.getProperties()).buildServiceRegistry(); return cfg.buildSessionFactory(sr); } catch (Exception e) { } return null; } public static SessionFactory getSessionFacotry() { return sessionFacotry; } }
1.3.6用戶實體
用於保存用戶信息,關聯數據庫中的用戶表
package com.jie.socket.chart.vo; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; import org.hibernate.annotations.GenericGenerator; /** * @ClassName: User.java * @Description: TODO(用戶實體類) * @author yangjie * @version V1.0 * @Date 2016-5-24 下午9:17:10 */ @Entity @Table(name = "User") public class User { @Id @GeneratedValue(generator = "userIdGenerator") @GenericGenerator(name = "userIdGenerator", strategy = "uuid") private String id; @Column private String name; @Column private String password; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
1.3.7用戶登錄Servlet
處理用戶登陸驗證,通過數據庫驗證。
package com.jie.socket.chat; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.hibernate.Criteria; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.criterion.Restrictions; import com.jie.socket.chat.util.HibernateUtil; import com.jie.socket.chat.vo.User; /** * @ClassName: ChatLogin.java * @Description: TODO(登陸) * @author yangjie * @version V1.0 * @Date 2016-5-16 上午11:20:16 */ public class ChatLogin extends HttpServlet { private static final long serialVersionUID = 1L; public ChatLogin() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //int uuid = Integer.parseInt(request.getParameter("uuid")); String name = request.getParameter("name"); String password = request.getParameter("password"); //System.out.println(uuid); System.out.println(name); System.out.println(password); //是否登陸 boolean islogin = false; if (!islogin) { // 查詢用戶表,驗證登錄 SessionFactory sf = HibernateUtil.getSessionFacotry(); Session session = sf.openSession(); Criteria criteria = session.createCriteria(User.class); //criteria.add(Restrictions.eq("uuid", uuid)); criteria.add(Restrictions.eq("name", name));//按照姓名查找 criteria.add(Restrictions.eq("password", password));//按照密碼查找 Listresult = criteria.list(); if (result.size() ==1) {//登陸成功 System.out.println(name+",登陸成功"); islogin = true; }else{//登錄失敗 System.out.println(password+",登陸失敗"); } session.close(); sf = null; } PrintWriter out = response.getWriter(); out.print(islogin);//傳遞登陸是否成功 out.flush(); out.close(); } }
1.3.8hibernate.cfg.xml配置文件
配置數據庫信息,實體類信息。
com.mysql.jdbc.Driver jdbc:mysql://192.168.0.99:3306/jie-db root bonc123, 1 org.hibernate.dialect.MySQLDialect thread org.hibernate.cache.internal.NoCacheProvider true update
1.3.9web.xml
配置servlet登陸類,自啟動socket服務。
自啟動servlet,用於開啟socket服務 myServerSocket com.jie.socket.chat.MyServerSocket 1 手機登陸 chatLogin com.jie.socket.chat.ChatLogin chatLogin /chatLogin index.jsp
1.4Android手機端
基於socket技術,通過網絡端開啟SocketServer服務,手機端登陸並連接SocketServer服務,通過socket客戶端將消息發送到服務端,服務端分發給連接到該服務的所有客戶端。實現手機群聊的效果。
1.4.1用戶信息類
通過SharedPreferences將用戶名稱存儲到手機中。
package com.jie.socket.chat.info; import java.io.Serializable; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; public class UserInfo implements Serializable { private static final long serialVersionUID = 1L; private static final String PREFERENCES_TAG = "com.jie.socket.chart"; private static UserInfo instance; private String name = ""; public static UserInfo getInstance() { if (instance == null) { synchronized (UserInfo.class) { if (instance == null) { instance = new UserInfo(); } } } return instance; } public static void saveName(Context context) { SharedPreferences mySharedPreferences = context.getSharedPreferences( PREFERENCES_TAG, Activity.MODE_PRIVATE); SharedPreferences.Editor edit = mySharedPreferences.edit(); edit.putString("name", getInstance().getName()); edit.commit(); } public static String readName(Context context) { SharedPreferences mySharedPreferences = context.getSharedPreferences( PREFERENCES_TAG, Activity.MODE_PRIVATE); String mName = mySharedPreferences.getString("name", ""); getInstance().setName(mName); return mName; } public static void readAll(Context context) { readName(context); } public void cleanUserInfo(Context context) { SharedPreferences mySharedPreferences = context.getSharedPreferences( PREFERENCES_TAG, Activity.MODE_PRIVATE); SharedPreferences.Editor editor = mySharedPreferences.edit(); editor.remove("name"); editor.commit(); } public String getName() { return name; } public void setName(String name) { this.name = name; } }
1.4.2activity_main.xml布局
應用啟動過渡階段的時候顯示
1.4.3MainActivity入口類
MainActivity入口,監測用戶是否登陸狀況,判斷進入主界面還是進入登陸界面
package com.jie.socket.chat; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import com.jie.socket.chat.info.UserInfo; /** * @ClassName: MainActivity.java * @Description: TODO(Activity入口,監測用戶是否登陸狀況,判斷進入主界面還是進入登陸界面) * @author yangjie * @version V1.0 * @Date 2016年5月25日 下午8:35:48 */ public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); checkLogin(); } /** * @Title: checkLogin * @Description: TODO(監測是否登陸) * @return: void * @throws */ public void checkLogin() { // TODO Auto-generated method stub UserInfo.readAll(getApplicationContext()); String name = UserInfo.getInstance().getName(); System.out.println("username:" + name); if (name != null && !"".equals(name)) { gotoNext(); } else { gotoLogin(); } } public void gotoLogin() { Intent intent = new Intent(); intent.setClass(MainActivity.this, LoginActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); finish(); } private void gotoNext() { Intent intent = new Intent(); intent.setClass(MainActivity.this, ChatActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); finish(); } }
1.4.4activity_chat.xml布局文件
1.4.5ChatActivity聊天類
通過socket連接服務器端,建立連接,然後通過socket接收和發送信息。
package com.jie.socket.chat; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.UnknownHostException; import android.annotation.SuppressLint; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.alibaba.fastjson.JSONObject; import com.jie.socket.chat.info.UserInfo; /** * @ClassName: ChatActivity.java * @Description: TODO(聊天) * @author yangjie * @version V1.0 * @Date 2016年5月25日 下午7:47:00 */ public class ChatActivity extends BaseActivity { EditText et_ip; EditText et_msg; TextView tv_conent; Button btn_send; Button btn_connect; Socket socket; BufferedWriter writer; BufferedReader reader; String TAG = "Socket Chart"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_chat); et_ip = (EditText) findViewById(R.id.et_ip); et_msg = (EditText) findViewById(R.id.et_msg); tv_conent = (TextView) findViewById(R.id.tv_content); btn_connect = (Button) findViewById(R.id.btn_connect); btn_send = (Button) findViewById(R.id.btn_send); btn_send.setEnabled(false); et_msg.setEnabled(false); eventListener(); } private void eventListener() { // TODO Auto-generated method stub btn_connect.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub connnet(); } }); btn_send.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub send(); } }); et_msg.setOnKeyListener(onKey); } StringBuffer text = new StringBuffer(); /** * @Title: connnet * @Description: TODO(連接) * @return: void * @throws */ private void connnet() { // TODO Auto-generated method stub read.execute(); } /** * @Title: send * @Description: TODO(發送消息) * @return: void * @throws */ private void send() { // TODO Auto-generated method stub try { String msg = et_msg.getText().toString(); if (msg != null && !"".equals(msg)) { text.append(name + ":" + msg + "\n"); tv_conent.setText(text); // 傳遞給sokect服務,json數據{name:1212,msg:'121212'} JSONObject jsonObject = new JSONObject(); jsonObject.put("name", name); jsonObject.put("msg", msg); writer.write(jsonObject.toString() + "\n"); writer.flush();// 刷新緩沖區,強制輸出 Log.i(TAG, msg); et_msg.setText(""); } else { showToast("請輸入發送內容"); } } catch (NullPointerException excNull) { Log.i(TAG, "writer error:" + "空指針異常"); showToast("請點擊連接,連接到服務器"); } catch (Exception e) { // TODO Auto-generated catch block Log.i(TAG, "Error:" + e.getMessage()); e.printStackTrace(); } } @SuppressLint("ShowToast") public void showToast(String msg) { Toast.makeText(getApplicationContext(), msg, 1000).show(); } OnKeyListener onKey = new OnKeyListener() {// 回車鍵監聽 @Override public boolean onKey(View v, int keyCode, KeyEvent event) { // 回車鍵,並且為鼠標按下,防止按下回車鍵兩次調用 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) { Log.i(TAG, "回車鍵:" + keyCode); send(); return true; } return false; } }; OutputStreamWriter outputStreamWriter; InputStreamReader inputStreamReader; AsyncTaskread = new AsyncTask () { @Override protected Void doInBackground(Void... params) { // TODO Auto-generated method stub try { socket = new Socket(et_ip.getText().toString().trim(), 12345); outputStreamWriter = new OutputStreamWriter( socket.getOutputStream()); inputStreamReader = new InputStreamReader( socket.getInputStream()); writer = new BufferedWriter(outputStreamWriter); reader = new BufferedReader(inputStreamReader); publishProgress("@success"); } catch (UnknownHostException e) { // TODO Auto-generated catch block Log.i("socket", "連接失敗"); e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); // showToast("連接失敗"); Log.i("socket", "連接失敗"); } String line; // 異步線程未關閉 if (!isCancelled()) { while (true) { try { if ((line = reader.readLine()) != null) { publishProgress(line); } } catch (IOException e) { // TODO Auto-generated catch block if ("Socket closed".equals(e.getMessage())) { Log.i(TAG, "連接已關閉"); } else { e.printStackTrace(); } break; } } } return null; } @Override protected void onProgressUpdate(String... values) { // TODO Auto-generated method stub if (values[0] != null && !"".equals(values[0])) { if (values[0] == "@success") { // text.append(values[0]); btn_connect.setEnabled(false); Log.i(TAG, "連接服務器成功!"); tv_conent.setText("連接服務器成功!"); btn_send.setEnabled(true); et_msg.setEnabled(true); name = UserInfo.readName(getApplicationContext()); if (name != null && !"".equals(name)) ; { Log.i(TAG, "name:" + name); try { writer.write("name:" + name + "\n"); writer.flush();// 強制輸出 } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } else { JSONObject obj = JSONObject.parseObject(values[0]);// 將json字符串轉換為對象 String cName = obj.getString("name"); String cMsg = obj.getString("msg"); text.append(cName + ":" + cMsg + "\n"); Log.i(TAG, values[0]); tv_conent.setText(text); } } } }; @Override protected void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); // Log.i(TAG, "socket != null:"+(socket != null) // +"----socket.isClosed():"+(!socket.isClosed())); if (socket != null && !socket.isClosed()) { Log.i(TAG, "關閉連接"); try { read.cancel(true);// 線程關閉 socket.close(); reader.close(); writer.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }// 關閉連接 } } }
1.4.6基礎Activity
用於設置Activity公用的字段和方法。
package com.jie.socket.chat; import android.app.Activity; import android.os.Bundle; import android.view.KeyEvent; import android.view.Window; import android.widget.Toast; import com.jie.socket.chat.info.UserInfo; public class BaseActivity extends Activity { String name; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); initDatas(); } public void findViews() { } public void initDatas() { name = UserInfo.readName(getApplicationContext()); } public void clickEvents() { } @Override protected void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // TODO Auto-generated method stub return super.onKeyDown(keyCode, event); } @Override protected void onResume() { // TODO Auto-generated method stub super.onResume(); } @Override protected void onPause() { // TODO Auto-generated method stub super.onPause(); } @Override public void onBackPressed() { super.onBackPressed(); } public void showToast(CharSequence message) { if (message != null && !"".equals(message)) { Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT) .show(); } else { Toast.makeText(getApplicationContext(), "連接超時,請檢查網絡", Toast.LENGTH_SHORT).show(); } } }
1.4.7activity_login.xml登陸布局
1.4.8LoginActivity登陸類
package com.jie.socket.chat; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import com.jie.socket.chat.info.UserInfo; import com.lidroid.xutils.HttpUtils; import com.lidroid.xutils.exception.HttpException; import com.lidroid.xutils.http.RequestParams; import com.lidroid.xutils.http.ResponseInfo; import com.lidroid.xutils.http.callback.RequestCallBack; import com.lidroid.xutils.http.client.HttpRequest.HttpMethod; public class LoginActivity extends BaseActivity { EditText et_userName; EditText et_userPassword; Button btn_login; String name; String password; protected String httpUrl = "http://192.168.0.199:8080/SocketChatServer/chatLogin"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); et_userName = (EditText) findViewById(R.id.userName); et_userPassword = (EditText) findViewById(R.id.userPassword); btn_login = (Button) findViewById(R.id.btn_login); btn_login.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub name = et_userName.getText().toString(); password = et_userPassword.getText().toString(); if (!"".equals(name) && !"".equals(password)) { HttpUtils http = new HttpUtils(); RequestParams params = new RequestParams(); params.addBodyParameter("name", name); params.addBodyParameter("password", password); http.send(HttpMethod.POST, httpUrl, params, new RequestCallBack() { @Override public void onFailure(HttpException arg0, String arg1) { // TODO Auto-generated method stub Log.i("http.send", "failuer"); } @Override public void onSuccess(ResponseInfo arg0) { // TODO Auto-generated method stub String isLogin = arg0.result; if ("true".equals(isLogin)) {// 登陸成功 Intent intent = new Intent(); intent.setClass(LoginActivity.this, ChatActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); finish(); Log.i("http.send", "success" + isLogin); UserInfo.getInstance().setName(name); UserInfo.saveName(getApplicationContext()); } else { showToast("用戶名或密碼錯誤"); } } }); } } }); } }
Notification可以讓我們在獲得消息的時候,在狀態欄,鎖屏界面來顯示相應的信息,很難想象如果沒有Notification,那我們的qq和微信以及其他應用沒法主動通
介紹圓角控件常用於頭像,按鈕,圖標等,用途十分廣泛,而且常常配合board使用。在IOS中,UIVIew的CALayer層已經提供了圓角和board的方法,所以圓角控件的
注冊很多app或者網絡賬戶的時候,經常需要手機獲取驗證碼,來完成注冊,那時年少,只是覺得手機獲取驗證碼這件事兒很好玩,並沒有關心太多,她是如何實現的,以及她背後的故事到底
有關android的彈窗界面相信大家見過不少了,手機上很多應用軟件都涉及到彈窗控件,比如典型的每次刪除一個圖片或者卸載一個等都會彈出一個窗口詢問是否刪除/卸載等,還有我們