cocos2dx安卓調試中lua腳本錯誤報告和記錄的解決方案(適用於quick3.5)


在cocos2dx純lua腳本邏輯開發中,由於腳本與安卓之間的溝通問題,使得在測試中,腳本錯誤難以被報告和記錄,給測試工作帶來了很大的不便。


本文通過lua調用c++方法以及jni的使用,使lua提供的錯誤信息能夠在安卓應用的層面被報告和記錄。

主要步驟:

一、信息從lua發送到c++並處理:

1、在項目的c++代碼中,編寫方法並暴露給lua,使得該方法可以接受來自lua的文本信息並進行處理。

2、在打印錯誤的方法中,調用c++暴露的接口,傳遞錯誤信息並進行處理。

3、如有需要,在發送錯誤信息的同事暫停遊戲。

二、信息從c++發送到java並處理:

1、在項目的java代碼中,編寫方法,該方法以文本信息爲參數並對該信息進行處理。

2、在項目的c++代碼中,編寫方法,該方法被步驟一中c++代碼的方法所調用(或在原方法上處理),該方法使用jni調用java中的方法並將錯誤信息傳遞。

三、java對文本進行處理

1、在項目的java代碼中,編寫方法,被步驟二中的java方法調用(或原方法繼續處理),將錯誤信息打印到文件同時創建對話框顯示錯誤信息。


下面對該解決方案的詳細步驟進行介紹:

一、

1、在c++代碼的項目目錄下,創建一個類LogToAndroid,該類具有一個方法WriteLog,參數是一個字符串,用於被lua調用傳入錯誤信息進行處理。

void  LogToAndroid::WriteLog(string str)
{
	cout<<"receive a log from lua"<<endl;
	cout<<str<<endl;
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
<span style="white-space:pre">	</span>//此處執行步驟二調用jni方法
#else 
	
#endif

}
以上是簡單表示的代碼,從代碼中我們可以看到
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
這個宏,表示當在安卓上進行調試時,執行中間的方法。

然後我們使用tolua工具,將這個類的方法暴露給lua,具體方法可以參考其他教程。

最後得到一個包含int register_all_logtoandroid(lua_State* tolua_S);方法的註冊文件,在AppDelegate.cpp中直接或間接地調用這個方法,註冊LogToAndroid這個類給lua使用。


2、然後進入到lua腳本,找到main.lua文件,裏面有一個__G__TRACKBACK__的函數,這個函數我們可以在cocos引擎的luaStack.cpp中看到它的身影。

大概的作用是,當lua報錯給lua引擎時,c++層通過操作lua的stack調用lua中這個方法對錯誤信息進行打印等處理。這個方法的具體調用路徑等我弄明白了再做進一步解釋,可能在有些版本中是沒有的,那麼這個步驟可能需要找到替代方法(具體思路是在lua或者c++中找到打印錯誤的地方,截獲錯誤信息並調用下一步驟的方法)。

function __G__TRACKBACK__(errorMessage)
    local a = "----------------------------------------".."\n"
    local b = "LUA ERROR: " .. tostring(errorMessage) .. "\n"
    local c = debug.traceback("", 2).."\n"

    print(a..b..c..a)
    LogToAndroid:WriteLog(a..b..c..a)
end
這個方法在quick中的作用是通過print打印錯誤信息,這裏對錯誤信息進行整理後,又調用了
LogToAndroid:WriteLog(a..b..c..a)
這個方法,也就是剛纔c++中暴露的方法。

至此,我們如果運行代碼,可以看到c++將lua提供的錯誤信息又打印到了命令行界面上,這個時候錯誤信息以及到達c++層了。

PS:當然,截獲錯誤信息的方法不止這一種,也可以繞過暴露c++接口這個步驟,例如直接在c++中截也是可行的,但是可能會多處改用較底層的邏輯,影響範圍較大,以上方法是一種處理起來不方便但是後期改動很方便的方法(可以在lua中隨便組合信息寫入log中,只要調用LogToAndroid:WriteLog()即可)。


二、

1、在項目的java代碼中,編寫方法,該方法以文本信息爲參數並對該信息進行處理。

這一步我們打開項目的java代碼,可以看到AppActivity.java這個文件,先不動這個文件,在這個目錄下創建一個類,我這裏命名爲WriteLog,創建一個靜態方法

public static String WriteToLogFile(String str) throws IOException
    {
        String rstr = "i received your message";
<span style="white-space:pre">	</span>//處理錯誤信息
        //showTipDialog("腳本錯誤",str);
<span style="white-space:pre">	</span>return rstr;
    }
其中處理代碼被省略,這個方法以String爲輸入,String爲返回值,當它被c++調用時,傳入傳出一個String。

這裏跟上一步有點不同,c++暴露接口給lua調用是很主動的,但這裏,java就不需要這麼主動了,因爲c++夠厲害,你不主動我也搞得到你。

這一步驟java這邊的工作就結束了,既然java不主動,那麼c++那邊就稍微費勁一點了


2、在項目的c++代碼中,編寫方法,該方法被步驟一中c++代碼的方法所調用(或在原方法上處理),該方法使用jni調用java中的方法並將錯誤信息傳遞。

我們回顧第一個步驟,c++代碼中留了一段註釋,“//此處執行調用jni方法”,現在我們要把這個工作完成掉。

這裏的主角自然就是jni,沾了cocos的光,我們有一個類可以使用,使得jni的調用簡單不少,這個類就是JniHelper,調用它很簡單,只要在LogToAndroid中包含進來就可以了。

#pragma once
#include "cocos2d.h"
#include <string>
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#endif
using namespace std;
using namespace cocos2d;

class LogToAndroid : public Ref
{
public:
	static void WriteLog(string);
private:
};
這裏要特別注意#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)這個宏,是一定要加的。

不加會怎麼樣呢,在PC上使用VS編譯的時候,就會根據jniHelper裏面的引用去找各種源文件,但是這些文件在ndk裏面纔有,哪怕全拷過來也不管用,因爲人家是linux上用的,在使用ndk編譯成so的時候,就很沒這個問題了。

插播一個文章,裏面介紹了jniHelper的各種使用姿勢。

http://blog.csdn.net/ku726999/article/details/38553889

然後就可以使用jniHelper的方法了,看到這個代碼,把上面調試用的內容刪了,寫進正經要用的:

void  LogToAndroid::WriteLog(string str)
{
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
	//    typedef struct JniMethodInfo_
	//    {
	//        JNIEnv *    env;
	//        jclass      classID;
	//        jmethodID   methodID;
	//    } JniMethodInfo;

	JniMethodInfo info;
	bool ret = JniHelper::getStaticMethodInfo(info, "org/cocos2dx/lua/WriteLog", "WriteToLogFile", "(Ljava/lang/String;)Ljava/lang/String;");
	if (ret)
	{
		log("call function succeed");
		//傳入類ID和方法ID,小心方法名寫錯,第一個字母是大寫
		const char *p=str.data();
		jobject para = info.env->NewStringUTF(p);
		jstring jstr = (jstring)info.env->CallStaticObjectMethod(info.classID, info.methodID, para);
		std::string text = JniHelper::jstring2string(jstr);
		log("%s", text.c_str());//嘗試打印返回值

	}
#else 
	
#endif

}

我們使用的是傳入傳出都是String的Static方法,所以使用getStaticMethodInfo這個方法獲得java方法。裏面三個參數是要設置的,

"org/cocos2dx/lua/WriteLog", "WriteToLogFile", "(Ljava/lang/String;)Ljava/lang/String;"

第一個是java方法的類所在路徑,第二個是方法名,第三個是傳入傳出的對象

然後執行完getStaticMethodInfo這個後,info裏面就有java方法的信息了,如果找到方法成功,則執行方法,用的是

		jstring jstr = (jstring)info.env->CallStaticObjectMethod(info.classID, info.methodID, para);

這句,其他都是在轉換參數。

結束之後,我們編譯執行,順利的話就可以看到調用成功的信息和返回來的信息,如果在java那邊加一句打印啥的,就可以看到c++寫到java的信息了。


三、java對文本進行處理

1、在項目的java代碼中,編寫方法,被步驟二中的java方法調用(或原方法繼續處理),將錯誤信息打印到文件同時創建對話框顯示錯誤信息。

這個時候,信息已經到達java的方法了,之後想怎麼處理它,姿勢就很多了。

這裏,我們根據調試需要,選擇了輸出到文件以及彈框兩種處理,彈框後可以對遊戲進行截圖同時停止遊戲,而寫入文件則可以查找錯誤信息並和圖片對應查看。

處理方法對寫應用的人來說應該很簡單了,下面直接貼代碼

public class WriteLog {
	static String fileName;
	static String dirName;
    <span style="white-space:pre">	</span>static FileWriter fw = null;  
    <span style="white-space:pre">	</span>static BufferedWriter bw = null;  
    <span style="white-space:pre">	</span>static AppActivity mainDlg = null;
    <span style="white-space:pre">	</span>static AlertDialog.Builder builder= null;
    <span style="white-space:pre">	</span>static boolean ignoreDlg = false;
    <span style="white-space:pre">	</span>static Handler mHandler = null;
    public static void init(Handler handler)
    {
        WriteLog.mHandler = handler;
    }
	public static String CreateFile(AppActivity dlg) throws IOException
	{
		mainDlg = dlg;
		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH_mm_ss");//設置日期格式
		fileName = "LOT_"+df.format(new Date())+".txt";// new Date()爲獲取當前系統時間
		SimpleDateFormat date = new SimpleDateFormat("yyyy-MM-dd");//設置日期格式
		String dateName = date.format(new Date());		
		dirName = "mnt/sdcard/Log_of_towerDefence/"+dateName+"/";
		File file = new File(dirName); 
		
		if (!file.exists()) {  
		    try {  
		        //按照指定的路徑創建文件夾  
		        file.mkdirs();  
		    } catch (Exception e) {  
		        // TODO: handle exception  
		    }  
		}  
		File dir = new File(dirName+fileName);  
        if (!dir.exists()) {  
              try {  
                  //在指定的文件夾中創建文件  
                  dir.createNewFile();  
            } catch (Exception e) {  
            }  
        }  

        fw = new FileWriter(dirName+fileName, true);//  
        // 創建FileWriter對象,用來寫入字符流  
        bw = new BufferedWriter(fw); // 將緩衝對文件的輸出  
        bw.write(fileName+"\n"); // 寫入文件  
        bw.newLine();  
        bw.flush(); // 刷新該流的緩衝  
        bw.close();  
        fw.close();  
		return dirName+fileName;

	}
	
    public static String WriteToLogFile(String str) throws IOException
    {
    	
        String rstr = "i received your message";
        SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設置日期格式
		String datetime = dt.format(new Date());// new Date()爲獲取當前系統時間
		String myreadline = str;
		myreadline = datetime + "\n" + str;        
        fw = new FileWriter(dirName+fileName, true);//  
        // 創建FileWriter對象,用來寫入字符流  
        bw = new BufferedWriter(fw); // 將緩衝對文件的輸出  
        bw.write(myreadline + "\n"); // 寫入文件  
        bw.newLine();  
        bw.flush(); // 刷新該流的緩衝  
        bw.close();  
        fw.close();  
        showTipDialog("腳本錯誤",str);
        return rstr;
    }
    
    
    private static void showTipDialog(final String title, final String text)
    {
        Message msg = mHandler.obtainMessage();
        msg.what = 1;
        DialogMessage dm = new DialogMessage(title, text);
        dm.titile = title;
        dm.message = text;
        msg.obj = dm;
        msg.sendToTarget();
    }
}




這裏被c++調的方法是WriteToLogFile,裏面調了彈窗的方法showTipDialog。

爲了保證每次運行一個log文件,又寫了CreateFile方法,留着被項目初始化文件調。

還有一個init,也是留着被初始化文件調,是爲了獲得handler傳遞消息。

最後,彈窗的函數其實是一個發送消息的函數,真正的彈窗在初始化文件中。

最後看一下初始化文件做的事情,

在類定義

public class AppActivity extends Cocos2dxActivity{
後面加入一個處理彈窗的handler

protected Handler mHandler = new Handler(){
		@Override
       public void handleMessage(Message msg) {
           switch(msg.what)
           {
           case 1:
               DialogMessage dm = (DialogMessage)msg.obj;
               new AlertDialog.Builder(AppActivity.this)
               .setTitle(dm.titile)
               .setMessage(dm.message).setNegativeButton("返回畫面", new DialogInterface.OnClickListener() {

                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                       dialog.dismiss();
                   }
               })
               .setPositiveButton("終止程序",
                       new DialogInterface.OnClickListener() {

                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                       dialog.dismiss();
                       System.exit(0);
                   }
               })
               .create().show();
               break;
           }
       }
然後在初始化方法

protected void onCreate(Bundle savedInstanceState) {

中添加兩行

WriteLog.init(mHandler);

        try {
        	WriteLog.CreateFile(this);
        }catch (Exception e)
        {
     	   e.printStackTrace();
        }
        

於是大功告成,可以在安卓的SD卡中找到運行的錯誤log,同時錯誤的時候會出現彈窗。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章