Android OkHttp的Cookie自動化管理
Android中在使用OkHttp這個庫的時候,有時候需要持久化Cookie,那麼怎麼實現呢。OkHttp的內部原始碼過於複雜,不進行深究,這裡只看其中的HttpEngineer裡面的部分原始碼,在發起請求以及請求結束都會呼叫這個類的幾個方法。我們先看networkRequest方法,在裡面通過client.getCookieHandler()函式獲得了CookieHandler物件,通過該物件拿到cookie並設定到請求頭裡,請求結束後取得響應後通過networkResponse.headers()函式將請求頭獲得傳入receiveHeaders函式,並將取得的cookie存入getCookieHandler得到的一個CookieHandler物件中去
/**
* Populates request with defaults and cookies.
*
* <p>This client doesn't specify a default {@code Accept} header because it
* doesn't know what content types the application is interested in.
*/
private Request networkRequest(Request request) throws IOException {
Request.Builder result = request.newBuilder();
//此處省略n行程式碼......
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
// Capture the request headers added so far so that they can be offered to the CookieHandler.
// This is mostly to stay close to the RI; it is unlikely any of the headers above would
// affect cookie choice besides "Host".
Map<String, List<String>> headers = OkHeaders.toMultimap(result.build().headers(), null);
Map<String, List<String>> cookies = cookieHandler.get(request.uri(), headers);
// Add any new cookies to the request.
OkHeaders.addCookies(result, cookies);
}
//此處省略n行程式碼......
return result.build();
}
public void readResponse() throws IOException {
//此處省略n行程式碼......
receiveHeaders(networkResponse.headers());
//此處省略n行程式碼......
}
public void receiveHeaders(Headers headers) throws IOException {
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
cookieHandler.put(userRequest.uri(), OkHeaders.toMultimap(headers, null));
}
}
而這個CookieHandler物件是OkHttpClient類中的一個屬性,提供了getter和setter方法,預設的建構函式OkHttpClient client = new OkHttpClient();不會建立這個CookieHandler物件。假設我們傳入了這個物件,那麼OkHttp自然會進行cookie的自動管理了。
private CookieHandler cookieHandler;
public OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
this.cookieHandler = cookieHandler;
return this;
}
public CookieHandler getCookieHandler() {
return cookieHandler;
}
那麼假設我們將CookieHandler物件傳入
OkHttpClient client = new OkHttpClient();
client.setCookieHandler(CookieHandler cookieHanlder);
那麼,現在關鍵是如何去實現這個CookieHandler 物件。CookieManager是CookieHandler 的一個子類,其建構函式 public CookieManager(CookieStore store, CookiePolicy cookiePolicy)需要傳入兩個引數,CookieStore 是一個介面,因此我們實現CookieStore介面中的抽象方法,即可實現這個CookieHandler 物件。參考android-async-http這個庫,它具有cookie的自動管理功能,主要我們參考其中的兩個類
參考以上兩個類並做適當修改,得到了如下兩個類,他們的功能就是將cookie保持在SharedPreferences中。
package com.kltz88.okhttp.cookie;
/**
* User:lizhangqu([email protected])
* Date:2015-07-13
* Time: 17:31
*/
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.HttpCookie;
public class SerializableHttpCookie implements Serializable {
private static final long serialVersionUID = 6374381323722046732L;
private transient final HttpCookie cookie;
private transient HttpCookie clientCookie;
public SerializableHttpCookie(HttpCookie cookie) {
this.cookie = cookie;
}
public HttpCookie getCookie() {
HttpCookie bestCookie = cookie;
if (clientCookie != null) {
bestCookie = clientCookie;
}
return bestCookie;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(cookie.getName());
out.writeObject(cookie.getValue());
out.writeObject(cookie.getComment());
out.writeObject(cookie.getCommentURL());
out.writeObject(cookie.getDomain());
out.writeLong(cookie.getMaxAge());
out.writeObject(cookie.getPath());
out.writeObject(cookie.getPortlist());
out.writeInt(cookie.getVersion());
out.writeBoolean(cookie.getSecure());
out.writeBoolean(cookie.getDiscard());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
String name = (String) in.readObject();
String value = (String) in.readObject();
clientCookie = new HttpCookie(name, value);
clientCookie.setComment((String) in.readObject());
clientCookie.setCommentURL((String) in.readObject());
clientCookie.setDomain((String) in.readObject());
clientCookie.setMaxAge(in.readLong());
clientCookie.setPath((String) in.readObject());
clientCookie.setPortlist((String) in.readObject());
clientCookie.setVersion(in.readInt());
clientCookie.setSecure(in.readBoolean());
clientCookie.setDiscard(in.readBoolean());
}
}
> cookies;
private final SharedPreferences cookiePrefs;
/**
* Construct a persistent cookie store.
*
* @param context Context to attach cookie store to
*/
public PersistentCookieStore(Context context) {
cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
cookies = new HashMap>();
// Load any previously stored cookies into the store
Map prefsMap = cookiePrefs.getAll();
for(Map.Entry entry : prefsMap.entrySet()) {
if (((String)entry.getValue()) != null && !((String)entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {
String[] cookieNames = TextUtils.split((String)entry.getValue(), ",");
for (String name : cookieNames) {
String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
if (encodedCookie != null) {
HttpCookie decodedCookie = decodeCookie(encodedCookie);
if (decodedCookie != null) {
if(!cookies.containsKey(entry.getKey()))
cookies.put(entry.getKey(), new ConcurrentHashMap());
cookies.get(entry.getKey()).put(name, decodedCookie);
}
}
}
}
}
}
@Override
public void add(URI uri, HttpCookie cookie) {
String name = getCookieToken(uri, cookie);
// Save cookie into local store, or remove if expired
if (!cookie.hasExpired()) {
if(!cookies.containsKey(uri.getHost()))
cookies.put(uri.getHost(), new ConcurrentHashMap());
cookies.get(uri.getHost()).put(name, cookie);
} else {
if(cookies.containsKey(uri.toString()))
cookies.get(uri.getHost()).remove(name);
}
// Save cookie into persistent store
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new SerializableHttpCookie(cookie)));
prefsWriter.commit();
}
protected String getCookieToken(URI uri, HttpCookie cookie) {
return cookie.getName() + cookie.getDomain();
}
@Override
public List get(URI uri) {
ArrayList ret = new ArrayList();
if(cookies.containsKey(uri.getHost()))
ret.addAll(cookies.get(uri.getHost()).values());
return ret;
}
@Override
public boolean removeAll() {
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.clear();
prefsWriter.commit();
cookies.clear();
return true;
}
@Override
public boolean remove(URI uri, HttpCookie cookie) {
String name = getCookieToken(uri, cookie);
if(cookies.containsKey(uri.getHost()) && cookies.get(uri.getHost()).containsKey(name)) {
cookies.get(uri.getHost()).remove(name);
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
if(cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {
prefsWriter.remove(COOKIE_NAME_PREFIX + name);
}
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.commit();
return true;
} else {
return false;
}
}
@Override
public List getCookies() {
ArrayList ret = new ArrayList();
for (String key : cookies.keySet())
ret.addAll(cookies.get(key).values());
return ret;
}
@Override
public List getURIs() {
ArrayList ret = new ArrayList();
for (String key : cookies.keySet())
try {
ret.add(new URI(key));
} catch (URISyntaxException e) {
e.printStackTrace();
}
return ret;
}
/**
* Serializes Cookie object into String
*
* @param cookie cookie to be encoded, can be null
* @return cookie encoded as String
*/
protected String encodeCookie(SerializableHttpCookie cookie) {
if (cookie == null)
return null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(cookie);
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in encodeCookie", e);
return null;
}
return byteArrayToHexString(os.toByteArray());
}
/**
* Returns cookie decoded from cookie string
*
* @param cookieString string of cookie as returned from http request
* @return decoded cookie or null if exception occured
*/
protected HttpCookie decodeCookie(String cookieString) {
byte[] bytes = hexStringToByteArray(cookieString);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
HttpCookie cookie = null;
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
cookie = ((SerializableHttpCookie) objectInputStream.readObject()).getCookie();
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
}
return cookie;
}
/**
* Using some super basic byte array <-> hex conversions so we don't have to rely on any
* large Base64 libraries. Can be overridden if you like!
*
* @param bytes byte array to be converted
* @return string containing hex values
*/
protected String byteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte element : bytes) {
int v = element & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString().toUpperCase(Locale.US);
}
/**
* Converts hex values from strings to byte arra
*
* @param hexString string of hex-encoded values
* @return decoded byte array
*/
protected byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
}
return data;
}
}" data-snippet-id="ext.8d16b4a8fcca2def684a77bb76fa9bc7" data-snippet-saved="false" data-csrftoken="JJvR2MgM-MBb6XEHHuKsz9WziKXBKYfoLLYQ" data-codota-status="done">package com.kltz88.okhttp.cookie;
/**
* User:lizhangqu([email protected])
* Date:2015-07-13
* Time: 17:31
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import java.io.*;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* A persistent cookie store which implements the Apache HttpClient CookieStore interface.
* Cookies are stored and will persist on the user's device between application sessions since they
* are serialized and stored in SharedPreferences. Instances of this class are
* designed to be used with AsyncHttpClient#setCookieStore, but can also be used with a
* regular old apache HttpClient/HttpContext if you prefer.
*/
public class PersistentCookieStore implements CookieStore {
private static final String LOG_TAG = "PersistentCookieStore";
private static final String COOKIE_PREFS = "CookiePrefsFile";
private static final String COOKIE_NAME_PREFIX = "cookie_";
private final HashMap<String, ConcurrentHashMap<String, HttpCookie>> cookies;
private final SharedPreferences cookiePrefs;
/**
* Construct a persistent cookie store.
*
* @param context Context to attach cookie store to
*/
public PersistentCookieStore(Context context) {
cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
cookies = new HashMap<String, ConcurrentHashMap<String, HttpCookie>>();
// Load any previously stored cookies into the store
Map<String, ?> prefsMap = cookiePrefs.getAll();
for(Map.Entry<String, ?> entry : prefsMap.entrySet()) {
if (((String)entry.getValue()) != null && !((String)entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {
String[] cookieNames = TextUtils.split((String)entry.getValue(), ",");
for (String name : cookieNames) {
String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
if (encodedCookie != null) {
HttpCookie decodedCookie = decodeCookie(encodedCookie);
if (decodedCookie != null) {
if(!cookies.containsKey(entry.getKey()))
cookies.put(entry.getKey(), new ConcurrentHashMap<String, HttpCookie>());
cookies.get(entry.getKey()).put(name, decodedCookie);
}
}
}
}
}
}
@Override
public void add(URI uri, HttpCookie cookie) {
String name = getCookieToken(uri, cookie);
// Save cookie into local store, or remove if expired
if (!cookie.hasExpired()) {
if(!cookies.containsKey(uri.getHost()))
cookies.put(uri.getHost(), new ConcurrentHashMap<String, HttpCookie>());
cookies.get(uri.getHost()).put(name, cookie);
} else {
if(cookies.containsKey(uri.toString()))
cookies.get(uri.getHost()).remove(name);
}
// Save cookie into persistent store
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new SerializableHttpCookie(cookie)));
prefsWriter.commit();
}
protected String getCookieToken(URI uri, HttpCookie cookie) {
return cookie.getName() + cookie.getDomain();
}
@Override
public List<HttpCookie> get(URI uri) {
ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();
if(cookies.containsKey(uri.getHost()))
ret.addAll(cookies.get(uri.getHost()).values());
return ret;
}
@Override
public boolean removeAll() {
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.clear();
prefsWriter.commit();
cookies.clear();
return true;
}
@Override
public boolean remove(URI uri, HttpCookie cookie) {
String name = getCookieToken(uri, cookie);
if(cookies.containsKey(uri.getHost()) && cookies.get(uri.getHost()).containsKey(name)) {
cookies.get(uri.getHost()).remove(name);
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
if(cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {
prefsWriter.remove(COOKIE_NAME_PREFIX + name);
}
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.commit();
return true;
} else {
return false;
}
}
@Override
public List<HttpCookie> getCookies() {
ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();
for (String key : cookies.keySet())
ret.addAll(cookies.get(key).values());
return ret;
}
@Override
public List<URI> getURIs() {
ArrayList<URI> ret = new ArrayList<URI>();
for (String key : cookies.keySet())
try {
ret.add(new URI(key));
} catch (URISyntaxException e) {
e.printStackTrace();
}
return ret;
}
/**
* Serializes Cookie object into String
*
* @param cookie cookie to be encoded, can be null
* @return cookie encoded as String
*/
protected String encodeCookie(SerializableHttpCookie cookie) {
if (cookie == null)
return null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(cookie);
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in encodeCookie", e);
return null;
}
return byteArrayToHexString(os.toByteArray());
}
/**
* Returns cookie decoded from cookie string
*
* @param cookieString string of cookie as returned from http request
* @return decoded cookie or null if exception occured
*/
protected HttpCookie decodeCookie(String cookieString) {
byte[] bytes = hexStringToByteArray(cookieString);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
HttpCookie cookie = null;
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
cookie = ((SerializableHttpCookie) objectInputStream.readObject()).getCookie();
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
}
return cookie;
}
/**
* Using some super basic byte array <-> hex conversions so we don't have to rely on any
* large Base64 libraries. Can be overridden if you like!
*
* @param bytes byte array to be converted
* @return string containing hex values
*/
protected String byteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte element : bytes) {
int v = element & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString().toUpperCase(Locale.US);
}
/**
* Converts hex values from strings to byte arra
*
* @param hexString string of hex-encoded values
* @return decoded byte array
*/
protected byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
}
return data;
}
}
使用的時候,在發起請求前對CookieHandler進行設定,後續的cookie全都是自動管理,無需你關心cookie的保存於讀取
OkHttpClient client = new OkHttpClient();
client.setCookieHandler(new CookieManager(
new PersistentCookieStore(getApplicationContext()),
CookiePolicy.ACCEPT_ALL));
這樣,實現模擬登入,抓取一些資料就方便很多了,再也不用手動處理cookie了。
相關推薦
Android OkHttp的Cookie自動化管理
Android中在使用OkHttp這個庫的時候,有時候需要持久化Cookie,那麼怎麼實現呢。OkHttp的內部原始碼過於複雜,不進行深究,這裡只看其中的HttpEngineer裡面的部分原始碼,在發起請求以及請求結束都會呼叫這個類的幾個方法。我們先看netwo
android常用自動化測試框架
window 自動 瀏覽器 資料 對比 移動 ppi des 選擇 目錄: Monkey MonkeyRunner Instrumentation UiAutomator Espresso Selendroid Robotium Athrun Appi
python專項測試——Android App自動化測試框架
selenium sdk str ase val 測試環境 self ron app自動化 1 為什麽需要框架? 代碼混亂,難閱讀; 重復編碼,效率低;需求變化,難維護; 一 自動化實例 1 準備測試的app;準備測試環境;實現腳本; 2 測試app:只有登錄功能
Android權限管理知識學習記錄
人的 失效 管理 ext list tracer span man 之間 一、Android權限背景知識 在Android 6.0之前,所申請的權限只需要在AndroidManifest.xml列舉就可以了,從而容易導致一些安全隱患,因此,在Android 6.
Windows 下搭建 Appium + Android+python 自動化測試環境
細節 事情 android view package 效果 框架 比較 framework 前言 本來並不打算寫這麽一篇文章,但是實踐下來發現網上的各種教程裏大致有兩個問題。一是文章有些跟不上時代,目前android開發和測試的技術更新都比較快,內容有些過期。二是細節部分不
ssh服務及批量分發自動化管理
互聯網Ssh服務知識:Ssh包含openssh和openssl兩種包。Ssh客戶端包含ssh連接工具及scp拷貝、slogin、sftp等應用程序。Less /etc/ssh/sshd_config這是查看服務端配置,sshd是服務端,ssh是客戶端~/.ssh/known_hosts當客戶端ssh服務端後會
移動端測試===Android內存管理: 理解App的PSS
存儲器 什麽是 信息 圖片 == -s 使用情況 相同 src Android內存管理: 理解App的PSS 原文鏈接:http://www.littleeye.co/blog/2013/06/11/android-memory-management-understandi
ansible自動化管理windows系統實戰
右擊 收購 通信 tin adapter gin 批量 power print 一、簡述 1、說明日常系統自動化運維過程中難免會有windows系列服務器,就開源軟件來說目前大多的對windows批量管理兼容性不太好;不像Linux系統便捷,但現實中確實有些業務需要跑在wi
Appium做Android功能自動化測試
做了 指標 並不會 return 啟動 target socket cor enter 前言 做Android端功能自動化已有2年多的時間了,使用過的功能自動化框架有Robotium、Uiautomator、Appium。最近研究自動化case復用的方案,調研了Appium
【Python + uiaotumator2】之Android—APP自動化簡易例子
layout div ext widget service .py safety wid pre 上代碼: #!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2018/08/31 09:43 #
android RingtoneManager 鈴聲管理器
first list cat method fault next() 條目 getcount turn 獲取默認鈴聲Uri/ Uri String Uri sound = RingtoneManager.getDefaultUri(type);(type =
機械制造行業erp系統-減少人手操作-自動化管理
而且 重點 sql 我們 科技 采購管理 機械 軟件 加工 江門市信華軟件科技有限公司所設計的軟件是針對中小型的生產型企業的。企業人數一般在10-200人左右的廠都有適合的版本型號。針對中小型生產企業的管理特點:規模小、操作人員水平不高、管理混亂。所以,我們設計的軟件都簡潔
從零擼美團Android(一) - 統一管理 Gradle 依賴 提取到單獨檔案中
前言 從今天開始帶大家一起從零開始擼一個美團Android版App。 【從零擼美團】這個專題將持續更新,用以詳細記錄分享開發過程,歡迎關注。 原始碼地址:github.com/cachecats/L… 專題的第一篇文章本來想按慣例講專案介紹、整體架構、程式碼規範之類的。但今天有點躁動,不想講那麼正經
Ansible批量自動化管理工具入門
這樣的 ase 更新 系統版本 roo art 啟動服務 移除 nag 一、虛擬機版本 1、需要利用7.5版本虛擬機 2、7.5版註意事項: 【2.1】、網卡名叫ens32同樣配置文件也是ens32 【2.2】、命令:systemctl 統一管理命令, 例,systemct
Android+Jenkins自動化打包+上傳蒲公英+傳送郵件(測試必會)
Android+Jenkins自動化打包+上傳蒲公英+傳送郵件 各位好,由於經常要修改客戶端的伺服器地址和要區分渠道為了方便測試打包學習了一下Jenkins,期間遇到一些問題,反正就是問題比較多,網上也搜了很多東西但是比較散都講了一點,特地自己整理了一下 ,也將自己遇到的坑在這
Android 內存管理中的 Shallow heap Retained heap
內存 gc roots str 對象大小 數組元素 jprofiler 數組元素對 語言 profile 所有包含Heap Profling功能的工具(MAT,Yourkit,JProfiler,TPTP等)都會使用到兩個名詞,一個是Shallow heap Size,另一
自動化運維專題(二):Ansible批量自動化管理工具
一,工具簡介 1.1 ansible簡介 批量管理伺服器工具 無需部署agent,通過ssh進行管理 中小型公司常用的自動化運維工具 1.2 jenkins簡介 視覺化運維(主要用在視覺化部署) 持續構建,可以和git,snv結合 可結合ssh實
Android + Appium 自動化測試完整的環境配置及程式碼詳解
環境的的搭建 參考大神部落格:https://www.cnblogs.com/fnng/p/4540731.html 該部落格有一套詳細的入門教程,奈何時間有點久遠有些東西不能用了,但是參考價值還是有滴。 1.安裝各種SDK jre必須1.8以上 AndroidSDK需要8.0以下的測試,
用Appium讓Android功能自動化測試飛起來
turn 代碼片段 cti pass 三種 align sel 方式 sock p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 24.0px "Helvetica Neue"; color: #2f2f2f } p.p2 { ma
Android + Appium 自動化測試完整的環境配置及代碼詳解
完成 通知 文件的 lam tails contain version 自動化測試 開發 環境的的搭建 參考大神博客:https://www.cnblogs.com/fnng/p/4540731.html 該博客有一套詳細的入門教程,奈何時間有點久遠有些東西不能用了,但是參