文章大綱 一、Android崩潰日誌管理簡介二、崩潰日誌管理實戰三、項目源碼下載 一、Android崩潰日誌管理簡介 1. 什麼是android崩潰日誌管理 開發中有些地方未註意可能造成異常拋出未能caught到,然後彈出系統對話框強制退出。這種交互不好,而且開發者也不能及時獲取到底哪裡出問題。因此 ...
文章大綱
一、Android崩潰日誌管理簡介
二、崩潰日誌管理實戰
三、項目源碼下載
一、Android崩潰日誌管理簡介
1. 什麼是android崩潰日誌管理
開發中有些地方未註意可能造成異常拋出未能caught到,然後彈出系統對話框強制退出。這種交互不好,而且開發者也不能及時獲取到底哪裡出問題。因此我們可以使用android的UncaughtExceptionHandler來處理這種異常。
2. 操作邏輯
用戶端(出現崩潰)
我們會封裝一個通用的jar包,該jar包包括日誌列印、捕獲異常信息邏輯、網路傳輸、設置Debug和Release模式、獲取本機的相關信息等,當出現異常時,將異常信息以文件方式保存在用戶手機中,並且發送到後臺,當後臺接收成功時,自動刪除用戶手機的崩潰信息文件,若接收失敗,在下次發生崩潰時,將歷史發送失敗的崩潰一同發送。
接收端(後臺)
我們會編寫一個地址,用於接收異常的具體信息,並儲存在本地文件中,以此作為日誌進行管理。
二、崩潰日誌管理實戰
1. 後臺端
在該實戰中,我以簡單的servlet進行講解,實際項目中,可以以ssm或spring boot等框架進行操作。
/**
* 接收崩潰信息,併進行列印(實際項目中,需要以文件形式歸檔)
* @author wxc
*
*/
public class Test extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//獲取客戶端傳送過來的信息流
BufferedReader in=new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = in.readLine()) != null) {
//將信息流進行列印
System.out.println(line);
}
}
}
2. 客戶端通用項目
網路請求相關的配置管理類:HttpManager.java
/**
*
* 網路請求相關的配置管理
*
* @author 吳曉暢
*
*/
public class HttpManager {
private static final int SET_CONNECTION_TIMEOUT = 5 * 1000;
private static final int SET_SOCKET_TIMEOUT = 20 * 1000;
private static final String BOUNDARY = getBoundry();// UUID.randomUUID().toString();
private static final String MP_BOUNDARY = "--" + BOUNDARY;
private static final String END_MP_BOUNDARY = "--" + BOUNDARY + "--";
private static final String LINEND = "\r\n";
private static final String CHARSET = "UTF-8";
public static String uploadFile(String url, HttpParameters params,
File logFile) throws IOException{
HttpClient client = getHttpClient();
HttpPost post = new HttpPost(url);
ByteArrayOutputStream bos = null;
FileInputStream logFileInputStream = null;
String result = null;
try {
bos = new ByteArrayOutputStream();
if(params != null){
String key = "";
for (int i = 0; i < params.size(); i++) {
key = params.getKey(i);
StringBuilder temp = new StringBuilder(10);
temp.setLength(0);
temp.append(MP_BOUNDARY).append(LINEND);
temp.append("content-disposition: form-data; name=\"").append(key)
.append("\"").append(LINEND + LINEND);
temp.append(params.getValue(key)).append(LINEND);
bos.write(temp.toString().getBytes());
}
}
StringBuilder temp = new StringBuilder();
temp.append(MP_BOUNDARY).append(LINEND);
temp.append(
"content-disposition: form-data; name=\"logfile\"; filename=\"")
.append(logFile.getName()).append("\"").append(LINEND);
temp.append("Content-Type: application/octet-stream; charset=utf-8").append(LINEND + LINEND);
bos.write(temp.toString().getBytes());
logFileInputStream = new FileInputStream(logFile);
byte[] buffer = new byte[1024*8];//8k
while(true){
int count = logFileInputStream.read(buffer);
if(count == -1){
break;
}
bos.write(buffer, 0, count);
}
bos.write((LINEND+LINEND).getBytes());
bos.write((END_MP_BOUNDARY+LINEND).getBytes());
ByteArrayEntity formEntity = new ByteArrayEntity(bos.toByteArray());
post.setEntity(formEntity);
HttpResponse response = client.execute(post);
StatusLine status = response.getStatusLine();
int statusCode = status.getStatusCode();
Log.i("HttpManager", "返回結果為"+statusCode);
if(statusCode == HttpStatus.SC_OK){
result = readHttpResponse(response);
}
} catch (IOException e) {
throw e;
}finally{
if(bos != null){
try {
bos.close();
} catch (IOException e) {
throw e;
}
}
if(logFileInputStream != null){
try {
logFileInputStream.close();
} catch (IOException e) {
throw e;
}
}
}
return result;
}
private static String readHttpResponse(HttpResponse response){
String result = null;
HttpEntity entity = response.getEntity();
InputStream inputStream;
try {
inputStream = entity.getContent();
ByteArrayOutputStream content = new ByteArrayOutputStream();
int readBytes = 0;
byte[] sBuffer = new byte[512];
while ((readBytes = inputStream.read(sBuffer)) != -1) {
content.write(sBuffer, 0, readBytes);
}
result = new String(content.toByteArray(), CHARSET);
return result;
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
private static HttpClient getHttpClient() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore
.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, 10000);
HttpConnectionParams.setSoTimeout(params, 10000);
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));
ClientConnectionManager ccm = new ThreadSafeClientConnManager(
params, registry);
HttpConnectionParams.setConnectionTimeout(params,
SET_CONNECTION_TIMEOUT);
HttpConnectionParams.setSoTimeout(params, SET_SOCKET_TIMEOUT);
HttpClient client = new DefaultHttpClient(ccm, params);
return client;
} catch (Exception e) {
// e.printStackTrace();
return new DefaultHttpClient();
}
}
private static class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore)
throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
// TODO Auto-generated method stub
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// TODO Auto-generated method stub
}
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// TODO Auto-generated method stub
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
@Override
public Socket createSocket(Socket socket, String host, int port,
boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host,
port, autoClose);
}
}
private static String getBoundry() {
StringBuffer _sb = new StringBuffer();
for (int t = 1; t < 12; t++) {
long time = System.currentTimeMillis() + t;
if (time % 3 == 0) {
_sb.append((char) time % 9);
} else if (time % 3 == 1) {
_sb.append((char) (65 + time % 26));
} else {
_sb.append((char) (97 + time % 26));
}
}
return _sb.toString();
}
}
文件上傳相關類:UploadLogManager.java
package com.qihoo.linker.logcollector.upload;
import java.io.File;
import java.io.IOException;
import java.util.logging.Logger;
import com.qihoo.linker.logcollector.capture.LogFileStorage;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
/**
*
* @author 吳曉暢
*
*/
public class UploadLogManager {
private static final String TAG = UploadLogManager.class.getName();
private static UploadLogManager sInstance;
private Context mContext;
private HandlerThread mHandlerThread;
private static volatile MyHandler mHandler;
private volatile Looper mLooper;
private volatile boolean isRunning = false;
private String url;
private HttpParameters params;
private UploadLogManager(Context c){
mContext = c.getApplicationContext();
mHandlerThread = new HandlerThread(TAG + ":HandlerThread");
mHandlerThread.start();
}
//初始化UploadLogManager類
public static synchronized UploadLogManager getInstance(Context c){
if(sInstance == null){
sInstance = new UploadLogManager(c);
}
return sInstance;
}
/**
* 執行文件上傳具體操作
*
* @param url
* @param params
*/
public void uploadLogFile(String url , HttpParameters params){
this.url = url;
this.params = params;
mLooper = mHandlerThread.getLooper();
mHandler = new MyHandler(mLooper);
if(mHandlerThread == null){
return;
}
if(isRunning){
return;
}
mHandler.sendMessage(mHandler.obtainMessage());
isRunning = true;
}
//用於uploadLogFile方法調用的線程
private final class MyHandler extends Handler{
public MyHandler(Looper looper) {
super(looper);
// TODO Auto-generated constructor stub
}
@Override
public void handleMessage(Message msg) {
File logFile = LogFileStorage.getInstance(mContext).getUploadLogFile();
if(logFile == null){
isRunning = false;
return;
}
try {
String result = HttpManager.uploadFile(url, params, logFile);
Log.i("UpLoad", "服務端返回數據為"+result);
if(result != null){
Boolean isSuccess = LogFileStorage.getInstance(mContext).deleteUploadLogFile();
Log.i("UpLoad", "刪除文件結果為"+isSuccess);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
isRunning = false;
}
}
}
}
客戶端崩潰日誌文件的刪除,保存等操作類:LogFileStorage.java
文件保存在Android/data/包名/Log/下
package com.qihoo.linker.logcollector.capture;
import java.io.File;
import java.io.FileOutputStream;
import com.qihoo.linker.logcollector.utils.LogCollectorUtility;
import com.qihoo.linker.logcollector.utils.LogHelper;
import android.content.Context;
import android.util.Log;
/**
*
* 客戶端崩潰日誌文件的刪除,保存等操作
*
* @author 吳曉暢
*
*/
public class LogFileStorage {
private static final String TAG = LogFileStorage.class.getName();
public static final String LOG_SUFFIX = ".log";
private static final String CHARSET = "UTF-8";
private static LogFileStorage sInstance;
private Context mContext;
private LogFileStorage(Context ctx) {
mContext = ctx.getApplicationContext();
}
public static synchronized LogFileStorage getInstance(Context ctx) {
if (ctx == null) {
LogHelper.e(TAG, "Context is null");
return null;
}
if (sInstance == null) {
sInstance = new LogFileStorage(ctx);
}
return sInstance;
}
public File getUploadLogFile(){
File dir = mContext.getFilesDir();
File logFile = new File(dir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
if(logFile.exists()){
return logFile;
}else{
return null;
}
}
//刪除客戶端中崩潰日誌文件
public boolean deleteUploadLogFile(){
File dir = mContext.getFilesDir();
File logFile = new File(dir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
Log.i("Log",
LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
return logFile.delete();
}
//保存文件
public boolean saveLogFile2Internal(String logString) {
try {
File dir = mContext.getFilesDir();
if (!dir.exists()) {
dir.mkdirs();
}
File logFile = new File(dir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
FileOutputStream fos = new FileOutputStream(logFile , true);
fos.write(logString.getBytes(CHARSET));
fos.close();
} catch (Exception e) {
e.printStackTrace();
LogHelper.e(TAG, "saveLogFile2Internal failed!");
return false;
}
return true;
}
public boolean saveLogFile2SDcard(String logString, boolean isAppend) {
if (!LogCollectorUtility.isSDcardExsit()) {
LogHelper.e(TAG, "sdcard not exist");
return false;
}
try {
File logDir = getExternalLogDir();
if (!logDir.exists()) {
logDir.mkdirs();
}
File logFile = new File(logDir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
/*if (!isAppend) {
if (logFile.exists() && !logFile.isFile())
logFile.delete();
}*/
LogHelper.d(TAG, logFile.getPath());
FileOutputStream fos = new FileOutputStream(logFile , isAppend);
fos.write(logString.getBytes(CHARSET));
fos.close();
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "saveLogFile2SDcard failed!");
return false;
}
return true;
}
private File getExternalLogDir() {
File logDir = LogCollectorUtility.getExternalDir(mContext, "Log");
LogHelper.d(TAG, logDir.getPath());
return logDir;
}
}
UncaughtExceptionHandler實現類:CrashHandler.java
當出現異常時,會進入public void uncaughtException(Thread thread, Throwable ex) 方法中。
/**
*
* 如果需要捕獲系統的未捕獲異常(如系統拋出了未知錯誤,這種異常沒有捕獲,這將導致系統莫名奇妙的關閉,使得用戶體驗差),
* 可以通過UncaughtExceptionHandler來處理這種異常。
*
* @author 吳曉暢
*
*/
public class CrashHandler implements UncaughtExceptionHandler {
private static final String TAG = CrashHandler.class.getName();
private static final String CHARSET = "UTF-8";
private static CrashHandler sInstance;
private Context mContext;
private Thread.UncaughtExceptionHandler mDefaultCrashHandler;
String appVerName;
String appVerCode;
String OsVer;
String vendor;
String model;
String mid;
//初始化該類
private CrashHandler(Context c) {
mContext = c.getApplicationContext();
// mContext = c;
appVerName = "appVerName:" + LogCollectorUtility.getVerName(mContext);
appVerCode = "appVerCode:" + LogCollectorUtility.getVerCode(mContext);
OsVer = "OsVer:" + Build.VERSION.RELEASE;
vendor = "vendor:" + Build.MANUFACTURER;
model = "model:" + Build.MODEL;
mid = "mid:" + LogCollectorUtility.getMid(mContext);
}
//初始化該類
public static CrashHandler getInstance(Context c) {
if (c == null) {
LogHelper.e(TAG, "Context is null");
return null;
}
if (sInstance == null) {
sInstance = new CrashHandler(c);
}
return sInstance;
}
public void init() {
if (mContext == null) {
return;
}
boolean b = LogCollectorUtility.hasPermission(mContext);
if (!b) {
return;
}
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 發生異常時候進來這裡
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
//
handleException(ex);
//
ex.printStackTrace();
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(thread, ex);
} else {
Process.killProcess(Process.myPid());
// System.exit(1);
}
}
//將異常信息保存成文件
private void handleException(Throwable ex) {
String s = fomatCrashInfo(ex);
// String bes = fomatCrashInfoEncode(ex);
LogHelper.d(TAG, s);
// LogHelper.d(TAG, bes);
//LogFileStorage.getInstance(mContext).saveLogFile2Internal(bes);
LogFileStorage.getInstance(mContext).saveLogFile2Internal(s);
if(Constants.DEBUG){
LogFileStorage.getInstance(mContext).saveLogFile2SDcard(s, true);
}
}
private String fomatCrashInfo(Throwable ex) {
/*
* String lineSeparator = System.getProperty("line.separator");
* if(TextUtils.isEmpty(lineSeparator)){ lineSeparator = "\n"; }
*/
String lineSeparator = "\r\n";
StringBuilder sb = new StringBuilder();
String logTime = "logTime:" + LogCollectorUtility.getCurrentTime();
String exception = "exception:" + ex.toString();
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
String dump = info.toString();
String crashMD5 = "crashMD5:"
+ LogCollectorUtility.getMD5Str(dump);
String crashDump = "crashDump:" + "{" + dump + "}";
printWriter.close();
sb.append("&start---").append(lineSeparator);
sb.append(logTime).append(lineSeparator);
sb.append(appVerName).append(lineSeparator);
sb.append(appVerCode).append(lineSeparator);
sb.append(OsVer).append(lineSeparator);
sb.append(vendor).append(lineSeparator);
sb.append(model).append(lineSeparator);
sb.append(mid).append(lineSeparator);
sb.append(exception).append(lineSeparator);
sb.append(crashMD5).append(lineSeparator);
sb.append(crashDump).append(lineSeparator);
sb.append("&end---").append(lineSeparator).append(lineSeparator)
.append(lineSeparator);
return sb.toString();
}
private String fomatCrashInfoEncode(Throwable ex) {
/*
* String lineSeparator = System.getProperty("line.separator");
* if(TextUtils.isEmpty(lineSeparator)){ lineSeparator = "\n"; }
*/
String lineSeparator = "\r\n";
StringBuilder sb = new StringBuilder();
String logTime = "logTime:" + LogCollectorUtility.getCurrentTime();
String exception = "exception:" + ex.toString();
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
String dump = info.toString();
String crashMD5 = "crashMD5:"
+ LogCollectorUtility.getMD5Str(dump);
try {
dump = URLEncoder.encode(dump, CHARSET);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String crashDump = "crashDump:" + "{" + dump + "}";
printWriter.close();
sb.append("&start---").append(lineSeparator);
sb.append(logTime).append(lineSeparator);
sb.append(appVerName).append(lineSeparator);
sb.append(appVerCode).append(lineSeparator);
sb.append(OsVer).append(lineSeparator);
sb.append(vendor).append(lineSeparator);
sb.append(model).append(lineSeparator);
sb.append(mid).append(lineSeparator);
sb.append(exception).append(lineSeparator);
sb.append(crashMD5).append(lineSeparator);
sb.append(crashDump).append(lineSeparator);
sb.append("&end---").append(lineSeparator).append(lineSeparator)
.append(lineSeparator);
String bes = Base64.encodeToString(sb.toString().getBytes(),
Base64.NO_WRAP);
return bes;
}
}
項目調用封裝類:LogCollector.java
/**
*
* 執行文件上傳相關的類
*
*
* @author 吳曉暢
*
*/
public class LogCollector {
private static final String TAG = LogCollector.class.getName();
private static String Upload_Url;
private static Context mContext;
private static boolean isInit = false;
private static HttpParameters mParams;
//初始化文件上傳的url,數據等內容
public static void init(Context c , String upload_url , HttpParameters params){
if(c == null){
return;
}
if(isInit){
return;
}
Upload_Url = upload_url;
mContext = c;
mParams = params;
//初始化自己定義的異常處理
CrashHandler crashHandler = CrashHandler.getInstance(c);
crashHandler.init();
isInit = true;
}
/**
* 執行文件上傳的網路請求
*
* if(isWifiOnly && !isWifiMode){
return;
}表示只在wifi狀態下執行文件上傳
*
* @param isWifiOnly
*/
public static void upload(boolean isWifiOnly){
if(mContext == null || Upload_Url == null){
Log.d(TAG, "please check if init() or not");
return;
}
if(!LogCollectorUtility.isNetworkConnected(mContext)){
return;
}
boolean isWifiMode = LogCollectorUtility.isWifiConnected(mContext);
if(isWifiOnly && !isWifiMode){
return;
}
UploadLogManager.getInstance(mContext).uploadLogFile(Upload_Url, mParams);
}
/**
* 用於設置是否為測試狀態
*
* @param isDebug true為是,false為否 如果是,能看到LOG日誌,同時能夠在將文件夾看到崩潰日誌
*/
public static void setDebugMode(boolean isDebug){
Constants.DEBUG = isDebug;
LogHelper.enableDefaultLog = isDebug;
}
}
3. 客戶端接入使用
為通用項目設置is Library模式
實際android項目使用
添加Library
在Application子類中進行初始化
public class MyApplication extends Application {
//後臺地址地址
private static final String UPLOAD_URL = "http://192.168.3.153:8080/bengkuitest/servlet/Test";
@Override
public void onCreate() {
super.onCreate();
boolean isDebug = true;
//設置是否為測試模式,如果是,同時能夠在將文件夾看到崩潰日誌
LogCollector.setDebugMode(isDebug);
//params的數據可以為空 初始化LogCollector的相關數據,用於文件上傳到伺服器
LogCollector.init(getApplicationContext(), UPLOAD_URL, null);
}
}
編寫異常並上傳異常
public class MainActivity extends Activity implements OnClickListener {
private Button btn_crash;
private Button btn_upload;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_crash = (Button) findViewById(R.id.button1);
btn_upload = (Button) findViewById(R.id.button2);
btn_crash.setOnClickListener(this);
btn_upload.setOnClickListener(this);
}
//產生異常
private void causeCrash(){
String s = null;
s.split("1");
}
//上傳文件
private void uploadLogFile(){
//設置為只在wifi下上傳文件
boolean isWifiOnly = true;//only wifi mode can upload
//執行文件上傳伺服器
LogCollector.upload(isWifiOnly);//upload at the right time
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
causeCrash();
break;
case R.id.button2:
//上傳文件
uploadLogFile();
break;
default:
break;
}
}
}
運行結果如下圖所示
--No1Qr4Tu7Wx
content-disposition: form-data; name="logfile"; filename="c5c63fec3651fdebdd411582793fa40c.log"
Content-Type: application/octet-stream; charset=utf-8
&start---
logTime:2019-04-07 10:54:47
appVerName:1.0
appVerCode:1
OsVer:5.1.1
vendor:samsung
model:SM-G955F
mid:c5c63fec3651fdebdd411582793fa40c
exception:java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String[] java.lang.String.split(java.lang.String)' on a null object reference
crashMD5:74861b8fb97ef57b82a87a826ab6b08f
crashDump:{java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String[] java.lang.String.split(java.lang.String)' on a null object reference
at com.jiabin.logcollectorexample.MainActivity.causeCrash(MainActivity.java:32)
at com.jiabin.logcollectorexample.MainActivity.onClick(MainActivity.java:45)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5293)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
}
&end---
--No1Qr4Tu7Wx--
三、項目源碼下載
鏈接:https://pan.baidu.com/s/1kEGfJ3PSoDnsyulCAoimjg
密碼:xy0l