ULog遠端日誌——讓Android除錯更加方便直觀
在釋出U8SDK之後,使用U8SDK做SDK接入的同學,反饋比較多的一個問題就是除錯困難。這其實不是U8SDK的毛病,而是Android開發的通病。做Android開發,我們通常都是結合logcat來除錯,如果是原生的應用,有時我們還可以直接通過Debug斷點來除錯,但是,做遊戲開發,我們一般採用U3D,Cocos2dx遊戲引擎開發之後,釋出到對應的移動平臺。所以,斷點除錯就不太好實現了。很多時候,我們都是列印一些日誌,然後在logcat中定位對應的日誌,來查詢錯誤資訊。
之前也一直是使用logcat,但是logcat中日誌太多,過濾功能雖然有,但是有時候,有些機型,列印的日誌太多,你還沒有看清楚,日誌就被各種系統日誌給頂掉了。
而且,很多做遊戲開發的同學,可能也是遊戲快要做好了,要接各個平臺的渠道SDK的時候,才開始接觸Android開發。導致對logcat如何正確使用,就更找不到門路。
所以,我想幹脆在原有日誌的基礎上,再加一個遠端日誌列印,再接將日誌輸出到網頁上,這樣日誌方便搜尋,也不用理會各種亂七八糟的系統日誌。
然後,我們在U8SDK中將原有日誌簡單封裝了下,讓日誌列印可以支援遠端列印。遠端列印,顧名思義,我們需要一個伺服器端,來接收這些日誌,並輸出到網頁上。
首先想到的是,用java寫,部署到tomcat,但是想想,如此簡單的一個邏輯,用java真是有點浪費不說,還有點臃腫。最後,因為U8SDK打包工具用python寫的,那麼想就用python搭建一個吧,瞭解了下python中搭建一個web伺服器端,有幾種框架可以使用,最終選擇了一個最最簡單的web.py框架,這個框架要實現我們這個功能,真是再合適不過了,輕量的不能再輕量了。
我們只需要實現兩個功能介面, 一個是上報日誌的介面,一個是顯示日誌的介面。上報日誌,我們採用Http Post方式, 顯示日誌,採用Http Get瀏覽器中直接訪問。
為了讓不同級別的日誌顯示區別開來,我們給不同級別的日誌,配上不同的顏色。
客戶端上報的日誌,先在記憶體中放著,網頁上我們啟動一段js程式碼,每隔1秒鐘自動重新整理一次,並將滾動位置,始終保持在最後。這樣,我們的一個簡單的日誌收集和展示的web 伺服器就好了,程式碼片段:
import json
import web
web.config.debug = False
urls = (
'/','index'
)
localLogs = ""
class index:
def GET(self):
htmlFormat = "<html><head><title></title></head><body>%s <script type=\"text/javascript\">function myrefresh(){window.location.reload();window.scrollTo(0,document.body.scrollHeight);}setTimeout('myrefresh()',1000); </script></body></html>"
global localLogs
localLogs = localLogs.encode('gbk')
return htmlFormat % localLogs
def POST(self):
inputs=web.input()
content = inputs.get('log')
if content.startswith('{') and content.endswith('}'):
content = '[' + content + ']'
logs = json.loads(content)
for log in logs:
if 'stack' not in log:
log['stack'] = " "
color = '#808080'
if log['level'] == 'INFO':
color = '#008000'
elif log['level'] == 'WARNING':
color = '#FFA500'
elif log['level'] == 'ERROR':
color = '#FF0000'
strLog = '<div style="color:%s">%s %s: [%s] %s </div>' % (color, log['time'],log['level'], log['tag'], log['msg'])
stacks = log['stack'].split('\n')
strLog = strLog + ('<div color="%s">' % color)
for s in stacks:
strLog = strLog + ('<div>%s</div>' % (s.strip()))
strLog = strLog + '</div>'
global localLogs
localLogs = localLogs + strLog
return ""
if __name__ == '__main__':
app = web.application(urls, globals())
app.run()
上面這段程式碼,就是我們這個日誌伺服器的全部程式碼了,我們將這個檔案儲存為uconsole.py,然後,開啟命令終端,執行:
python uconsole.py
這樣,就可以啟動該伺服器了,預設監聽的埠是8080。如果該埠被佔用了,會啟動失敗,那麼我們可以換一個埠
python uconsole.py 8082
這樣,我們就變成監聽8082這個埠了。你可以開啟http:localhost:8082/ 你會發現網頁是空白的,但是每隔一秒網頁會自動重新整理一下,說明當前伺服器已經啟動好了,正在等待日誌的到來。
接下來,我們就要設計下客戶端部分的日誌框架,讓日誌以Http Post的方式上報到我們這個日誌伺服器端。
我們打算將Android原有的日誌(com.android.util.Log)進行一個簡單的封裝,因為我們僅僅要加一個遠端日誌介面,所以,我們不希望把它弄得過於複雜。
我們定義一個ILog介面,將日誌介面提取出來,這樣,我們將會有這個兩個實現,一個LocalLog,一個RemoteLog。LocalLog中,我們依然呼叫Android自帶的Log進行列印,RemoteLog中,我們將日誌放到一個佇列中,每隔一定的間隔,就將儲存的日誌一次性上報到日誌伺服器。
ILog介面:
public interface ILog {
public void d(String tag, String msg);
public void i(String tag, String msg);
public void w(String tag, String msg);
public void w(String tag, String msg, Throwable e);
public void e(String tag, String msg);
public void e(String tag, String msg, Throwable e);
public void destory();
}
ULocalLog實現:
/**
*
* Android本地日誌輸出
*
*/
public class ULocalLog implements ILog{
@Override
public void d(String tag, String msg) {
Log.d(tag, msg);
}
@Override
public void i(String tag, String msg) {
Log.i(tag, msg);
}
@Override
public void w(String tag, String msg) {
Log.w(tag, msg);
}
@Override
public void e(String tag, String msg) {
Log.e(tag, msg);
}
@Override
public void w(String tag, String msg, Throwable e) {
Log.w(tag, msg, e);
}
@Override
public void e(String tag, String msg, Throwable e) {
Log.e(tag, msg, e);
}
@Override
public void destory() {
}
}
URemoteLog實現:
public class URemoteLog implements ILog{
private URemoteLogPrinter printer;
public URemoteLog(String url, int interval){
printer = new URemoteLogPrinter(url, interval);
}
@Override
public void d(String tag, String msg) {
printer.print(new ULog(ULog.L_DEBUG, tag, msg));
}
@Override
public void i(String tag, String msg) {
printer.print(new ULog(ULog.L_INFO, tag, msg));
}
@Override
public void w(String tag, String msg) {
printer.print(new ULog(ULog.L_WARN, tag, msg));
}
@Override
public void w(String tag, String msg, Throwable e) {
printer.print(new ULog(ULog.L_WARN, tag, msg, e));
}
@Override
public void e(String tag, String msg) {
printer.print(new ULog(ULog.L_ERROR, tag, msg));
}
@Override
public void e(String tag, String msg, Throwable e) {
printer.print(new ULog(ULog.L_ERROR, tag, msg, e));
}
@Override
public void destory() {
printer.stop();
}
}
遠端日誌實現中,我們採用一個URemoteLogPrinter來臨時儲存日誌,並定時傳到日誌伺服器,該類實現如下:
public class URemoteLogPrinter {
private List<ULog> logs;
private String url;
private int interval = 1000; //單位 毫秒
private Timer timer;
private boolean running;
public URemoteLogPrinter(){
}
public URemoteLogPrinter(String remoteUrl, int interval){
this.logs = Collections.synchronizedList(new ArrayList<ULog>());
this.url = remoteUrl;
this.interval = interval;
}
public void print(ULog log){
start();
synchronized (logs) {
logs.add(log);
}
}
public void printImmediate(String url, ULog log){
Map<String, String> params = new HashMap<String,String>();
params.put("log", log.toJSON());
U8HttpUtils.httpPost(url, params);
}
public List<ULog> getAndClear(){
synchronized (logs) {
List<ULog> all = new ArrayList<ULog>(logs);
logs.clear();
return all;
}
}
public void start(){
if(running){
return;
}
running = true;
TimerTask task = new LogPrintTask();
timer = new Timer(true);
timer.scheduleAtFixedRate(task, 100, interval);
}
public void stop(){
if(timer != null){
timer.cancel();
}
running = false;
}
class LogPrintTask extends TimerTask{
@Override
public void run() {
try{
List<ULog> logs = getAndClear();
if(logs.size() > 0){
StringBuilder sb = new StringBuilder();
sb.append("[");
for(ULog log : logs){
sb.append(log.toJSON()).append(",");
}
sb.deleteCharAt(sb.length()-1).append("]");
Map<String, String> params = new HashMap<String,String>();
params.put("log", sb.toString());
U8HttpUtils.httpPost(url, params);
}
}catch(Exception e){
e.printStackTrace();
stop();
}
}
}
}
這樣,我們整個日誌封裝就可以,但是我們遠端日誌中,有幾個引數我們需要設定下,比如,遠端列印的時間間隔,遠端日誌伺服器地址等引數。這些引數,我們後面放到AndroidManifest.xml中的meta-data中,同時,我們需要有一個呼叫的介面,給應用來呼叫日誌。所以,我們封裝一個Log類:
public class Log{
private static Log instance = new Log();
private List<ILog> logPrinters;
private boolean isInited = false;
private boolean enable = false;
private String level = ULog.L_DEBUG;
private boolean local = true;
private boolean remote = true;
private int remoteInterval = 1000;
private String remoteUrl = "";
private Log(){
logPrinters = new ArrayList<ILog>();
}
public static void d(String tag, String msg) {
try{
if(!ULog.L_DEBUG.equalsIgnoreCase(instance.level)){
return;
}
for(ILog printer: instance.logPrinters){
printer.d(tag, msg);
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void i(String tag, String msg) {
try{
if(!ULog.L_DEBUG.equalsIgnoreCase(instance.level) &&
!ULog.L_INFO.equalsIgnoreCase(instance.level)){
return;
}
for(ILog printer: instance.logPrinters){
printer.i(tag, msg);
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void w(String tag, String msg) {
try{
if(ULog.L_ERROR.equalsIgnoreCase(instance.level)){
return;
}
for(ILog printer: instance.logPrinters){
printer.w(tag, msg);
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void w(String tag, String msg, Throwable e) {
try{
if(ULog.L_ERROR.equalsIgnoreCase(instance.level)){
return;
}
for(ILog printer: instance.logPrinters){
printer.w(tag, msg, e);
}
}catch(Exception e2){
e2.printStackTrace();
}
}
public static void e(String tag, String msg) {
try{
for(ILog printer: instance.logPrinters){
printer.e(tag, msg);
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void e(String tag, String msg, Throwable e) {
try{
for(ILog printer: instance.logPrinters){
printer.e(tag, msg, e);
}
}catch(Exception e2){
e2.printStackTrace();
}
}
/**
* 在Application的attachBaseContext中呼叫
* @param context
*/
public static void init(Context context){
try{
if(instance.isInited){
return;
}
instance.parseConfig(context);
instance.logPrinters.clear();
if(!instance.enable){
android.util.Log.d("ULOG", "the log is not enabled.");
return;
}
if(instance.local){
instance.logPrinters.add(new ULocalLog());
}
if(instance.remote){
instance.logPrinters.add(new URemoteLog(instance.remoteUrl, instance.remoteInterval));
}
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, final Throwable e) {
new Thread(new Runnable() {
@Override
public void run() {
try{
URemoteLogPrinter printer = new URemoteLogPrinter();
printer.printImmediate(instance.remoteUrl, new ULog(ULog.L_ERROR, "Crash", "Application Crashed!!!", e));
}catch(Exception e){
e.printStackTrace();
}finally{
System.exit(0);
}
}
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
});
instance.isInited = true;
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 在Application的onTerminate中呼叫銷燬
*/
public static void destory(){
try{
if(instance.logPrinters != null){
for(ILog printer : instance.logPrinters){
printer.destory();
}
}
}catch(Exception e){
e.printStackTrace();
}
}
private void parseConfig(Context ctx){
try{
ApplicationInfo appInfo = ctx.getPackageManager().getApplicationInfo(ctx.getPackageName(), PackageManager.GET_META_DATA);
if(appInfo != null && appInfo.metaData != null){
if(appInfo.metaData.containsKey("ulog.enable")){
enable = appInfo.metaData.getBoolean("ulog.enable");
}
if(appInfo.metaData.containsKey("ulog.level")){
level = appInfo.metaData.getString("ulog.level");
}
if(appInfo.metaData.containsKey("ulog.local")){
local = appInfo.metaData.getBoolean("ulog.local");
}
if(appInfo.metaData.containsKey("ulog.remote")){
remote = appInfo.metaData.getBoolean("ulog.remote");
}
if(appInfo.metaData.containsKey("ulog.remote_interval")){
remoteInterval = appInfo.metaData.getInt("ulog.remote_interval");
}
if(appInfo.metaData.containsKey("ulog.remote_url")){
remoteUrl = appInfo.metaData.getString("ulog.remote_url");
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
除了日誌列印的幾個介面,我們增加了兩個介面,一個是init,一個是destroy。我們需要在呼叫Log列印之前,呼叫init方法進行初始化,因為我們需要讀取配置引數,根據配置來設定對應的引數。同樣的,在應用退出的時候,我們需要呼叫destroy來回收資源。
一般init我們可以在Application的onCreate或者attachBaseContext中呼叫;destroy可以在Application的onTerminate中呼叫
一切準備好之後,我們還需要在應用的AndroidManifest.xml中增加幾個日誌的配置引數:
<meta-data android:name="ulog.enable" android:value="true" /> <!--是否開啟日誌,關閉之後,不會輸出到logcat也不會輸出到遠端-->
<meta-data android:name="ulog.level" android:value="DEBUG" /> <!--日誌級別(DEBUG|INFO|WARNING|ERROR)-->
<meta-data android:name="ulog.local" android:value="true" /> <!--是否在logcat中列印-->
<meta-data android:name="ulog.remote" android:value="true" /> <!--是否遠端列印-->
<meta-data android:name="ulog.remote_interval" android:value="500" /> <!--遠端列印時,日誌上報間隔,單位毫秒-->
<meta-data android:name="ulog.remote_url" android:value="http://192.168.18.9:8080/" /> <!--遠端日誌伺服器地址,就是uconsole監聽的地址-->
整個日誌框架和伺服器程式碼,已經放在github上,有需要的同學可以直接使用: