我眼中的Qt for Android
引子
前幾天,我分享了一下qt for android,從大家的反應和回饋,我看到兩種極端的狀態。一個是:“太好了!想做Android開發但是不想轉java,這下不用了!” 另一個是:“不要在Qt上浪費時間了,它頂多在Android上跑個Hello world,別的什麼也跑不了。”
我先說說我對Qt for Android的客觀認識。首先,從現有階段看,不得不承認TA並不是一個成熟的技術(工具)。在大型項目中,還是不建議使用qt for android開發的,因為資料太少,我們無法快速深入的在大腦建立起qt for android網路,在遇到問題的時候,解決起來就很棘手。但是,絕不是說僅僅就能跑個Hello world,如果真的這麼一無是處,TA就沒有存在的意義,也就不會吸引大批開發人員深入研究和最佳化了。要知道,世上最簡單的事情就是批評和怒斥。我再次強調一下,我只是分享我所看到的知道的,不帶任何嚮導性。對於技術本身,仁者見仁,智者見智。
品味與探究
當我看到這麼一個技術工具,我的好奇心驅使我探究一下(1)TA到底是如何?的,(2)程式在Android上執行效率和效能怎麼樣,(3)較常規的Android java開發和jni c++開發而言,兩者之間有什麼可以相互借鑒, (4)倘若Google真的開放純c++開發,那麼java和qt for android又是怎樣的一番光景?
一個開發人員分享他某一個程式的設計思路:在Qt下通過jni得到java Env,從而使用GPS等android API,並且已經實現:
JNIEnv *currEnv;currEnv = 0;if (currVM->AttachCurrentThread((void **)&currEnv, NULL)<0){ emit error("Cannot attach the current thread to the VM");}
也許因為我對Qt訊號和槽的情有獨鐘,看到emit就感到很親切,並且被深深的吸引了。那麼從Qt for android 的qt工程源碼看,到底是如何在android上成功啟動並啟動並執行呢?
啟動流程分析
用qt-creator建立的每個應用程式中,src下的檔案都是基本相同。因為啟動程式,建立介面,連結庫,這些操作是每個應用程式所必需的,最初的qt程式被編譯成了lib**.so的動態庫,當調用JNI介面startQtApp函數時真正啟動了qt程式。
執行程式的入口在src/eu/licentia/necessitas/industrius/QtActivity.java中,onCreate調用startapp檢查必要的庫檔案,擴充包,外掛程式是否存在,並載入.連結ministro服務,得知ministro服務現在的狀態,如果缺少qt庫則需要藉助ministro服務下載。
private void startApp(final boolean firstStart)
{
try
{
ActivityInfo ai=getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
if (!ai.metaData.containsKey("android.app.qt_libs_resource_id"))
{
// No required qt libs ?
// Probably this application was compiled using static qt libs
// or all qt libs are prebundled into the package
m_ministroCallback.libs(null, null, null, 0, null);
return;
}
int resourceId = ai.metaData.getInt("android.app.qt_libs_resource_id");
m_qtLibs=getResources().getStringArray(resourceId);
m_ministroCallback.libs(libs,"QT_IMPORT_PATH=/data/local/qt/imports\tQT_PLUGIN_PATH=/data/local/qt/plugins",
"-platform\tandroid", 0,null);
return;
}
try {
if(!bindService(new Intent(eu.licentia.necessitas.ministro.IMinistro.class.getCanonicalName()),
m_ministroConnection, Context.BIND_AUTO_CREATE))
throw new SecurityException("");
} catch (SecurityException e) { }
}
應用程式需要的庫由AndroidManifest.xml中的qt_libs_resource_id 項指定,這一項來自於res/values/libs.xml中的qt_libs項。
<?xmlversion='1.0' encoding='utf-8'?><resources> <array name="qt_libs"> <item>QtCore</item> <item>QtGui</item> </array> <arrayname="bundled_libs"/></resources>
qt應用程式的啟動也是藉助ministro服務,需要ministro提供相應的庫支援,當得到相應的庫後建立線程,啟動startApplication。
private IMinistroCallback m_ministroCallback = new IMinistroCallback.Stub(){
@Override
public void libs(finalString[] libs, final String evnVars, final String params,
int errorCode, StringerrorMessage) throws RemoteException {
runOnUiThread(new Runnable() {
@Override
public void run() {
startApplication(libs,evnVars, params);
}
});
}
};
在src/eu/licentia/necessitas/industriusQtApplication.java中 startApplication函數,先啟動android的Plugin然後調用一個JNI介面startQtApp啟動qt程式。
QtActivity.java中的startApplication函數會調用QtApplication.java中的 startApplication。
public static void startApplication(String params, String environment)
{
if (params == null)
params ="-platform\tandroid";
synchronized (m_mainActivityMutex)
{
startQtAndroidPlugin();
setDisplayMetrics(m_displayMetricsScreenWidthPixels,
m_displayMetricsScreenHeightPixels,
m_displayMetricsDesktopWidthPixels,
m_displayMetricsDesktopHeightPixels,
m_displayMetricsXDpi,
m_displayMetricsYDpi);
if (params.length()>0)
params="\t"+params;
startQtApp("QtApp"+params,environment);
m_started=true;
}
}
startQtApp時會啟動線程startMainMethod,在startMainMethod線程中會將lib**.so中main函數入口以庫函數介面的形式再次執行,恢複了qt可執行程式的本來面目,在後台執行
這部分源碼在android-lighthouse的源碼中。
JNI的部分代碼
extern "C" int main(int, char **); //use the standard mainmethod to start the application
staticvoid * startMainMethod(void * /*data*/)
{
char ** params;
params=(char**)malloc(sizeof(char*)*m_applicationParams.length());
for (inti=0;i<m_applicationParams.size();i++)
params[i]=(char*)m_applicationParams[i].constData();
int ret = main(m_applicationParams.length(),params);
......
}
重寫onKeydown
通過上述調用過程可以知道,android程式是怎樣通過JNI來調用qt庫中的函數。其他相關android的的JNI介面在,plugins/platforms/android/下,比如 keydown。
在src/eu/licentia/necessitas/industriusQtActivity.java重寫了onKeydown函數,調用JNI函數keyDown實現。
keyDown的實現在 plugins/platforms/android/mw/androidjnimain.cpp 中
static void keyDown(JNIEnv */*env*/, jobject /*thiz*/,jint key, jint unicode, jint modifier)
{
……
int mappedKey=mapAndroidKey(key);
if (mappedKey==Qt::Key_Close)
{
qDebug()<<"handleCloseEvent"<<mLastTLW;
QWindowSystemInterface::handleCloseEvent(mLastTLW);
}
else
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, mappedKey,modifiers, QChar(unicode),true);
//通過JNI的協助,轉化成了Qt的實現
}
結束語
先寫到這吧,不管怎麼說,接觸qt for android ,讓我收穫了很多很多,絕對不僅限於qt 和 android領域。