Qt5支持編寫Android應用。java
典型main
:android
int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }
這會在Android設備上顯示一個空白窗口。git
可是:github
main
函數「衝突」。咱們知道Android進程源於zygote的fork,做爲進程入口的函數main
早就執行過了,那麼上述代碼這中Qt的入口函數main
又什麼時候怎麼被執行的呢?如下基於Qt 5.10.1分析(源碼參考https://github.com/qt/qtbase/releases/tag/v5.10.1)。web
在qtcreator新建一個helloworld的工程,編譯後,在qt的build目錄下,能夠看到目錄結構:app
- android_build/
- libhelloworld.so
- main.o
- mainwindow.o
.o文件顯然對應各個cpp文件,so文件是.o文件的「集合」。android-build的目錄通過簡單的查看,能夠知道是一個gradle組織的android工程。ide
因此qt的Android支持,簡單看就是將咱們寫的qt代碼生成so文件,並經過自動生成的Android模板工程來最終生成一個apk文件。函數
看下android_build中的build.gradle:佈局
…… sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] res.srcDirs = [qt5AndroidDir + '/res', 'res'] resources.srcDirs = ['src'] renderscript.srcDirs = ['src'] assets.srcDirs = ['assets'] jniLibs.srcDirs = ['libs'] } } ……
build.gradle中經過sourceSets
調整了com.android.application
插件的默認源碼、資源路徑。post
主要引入了qt5AndroidDir
目錄下的src
、aidl
和res
。
qt5AndroidDir
定義在gradle.properties
:
qt5AndroidDir=/Users/xxxx/Qt5.10.1/5.10.1/android_armv7/src/android/java
通常指向qt安裝目錄中的android_armv7/src/android/java
.
看下libs目錄:
libs
├── QtAndroid-bundled.jar
└── armeabi-v7a
├── gdbserver
├── libQt5Core.so
├── libQt5Gui.so
├── libQt5Widgets.so
├── libgdbserver.so
├── libgnustl_shared.so
├── libhelloworld.so
├── libplugins_imageformats_libqgif.so
├── libplugins_imageformats_libqicns.so
├── libplugins_imageformats_libqico.so
├── libplugins_imageformats_libqjpeg.so
├── libplugins_imageformats_libqtga.so
├── libplugins_imageformats_libqtiff.so
├── libplugins_imageformats_libqwbmp.so
├── libplugins_imageformats_libqwebp.so
├── libplugins_platforms_android_libqtforandroid.so
└── libplugins_styles_libqandroidstyle.so
qt運行所需的幾個核心so拷貝被拷貝到了libs目錄下,這樣就會被打包到最終的apk中。還引入了一個QtAndroid-bundled.jar
依賴。
總結
上節分析可知,Android代碼主導了整個進程的運行。那麼尋找Qt應用入口就從Android代碼入手。
Android應用入口通常是Application
,模板工程android-build
中,QtApplication
繼承了Applicaiton
,可是瀏覽一遍並無發現加載libhelloworld.so
的地方,先略過。
Application
加載後會會執行「主Activity」,也就是<intent-filter>
指定有category.LAUNCHER
的那個Activity
,在模板工程中是QtActivity
。
這樣,主Activity的onCreate
就不亞於第二入口:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); onCreateHook(savedInstanceState); }
onCreateHook
protected void onCreateHook(Bundle savedInstanceState) { m_loader.APPLICATION_PARAMETERS = APPLICATION_PARAMETERS; m_loader.ENVIRONMENT_VARIABLES = ENVIRONMENT_VARIABLES; m_loader.QT_ANDROID_THEMES = QT_ANDROID_THEMES; m_loader.QT_ANDROID_DEFAULT_THEME = QT_ANDROID_DEFAULT_THEME; m_loader.onCreate(savedInstanceState); }
這裏看到的m_loader
是類QtActivityLoader
Loader
QtActivityLoader.create
:
…… m_displayDensity = m_activity.getResources().getDisplayMetrics().densityDpi; ENVIRONMENT_VARIABLES += "\tQT_ANDROID_THEME=" + QT_ANDROID_DEFAULT_THEME + "/\tQT_ANDROID_THEME_DISPLAY_DPI=" + m_displayDensity + "\t"; if (null == m_activity.getLastNonConfigurationInstance()) {//代碼分析看應該老是null,多是預留的功能吧 if (m_contextInfo.metaData.containsKey("android.app.background_running") && m_contextInfo.metaData.getBoolean("android.app.background_running")) { ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=0\t"; } else { ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=1\t"; } if (m_contextInfo.metaData.containsKey("android.app.auto_screen_scale_factor") && m_contextInfo.metaData.getBoolean("android.app.auto_screen_scale_factor")) { ENVIRONMENT_VARIABLES += "QT_AUTO_SCREEN_SCALE_FACTOR=1\t"; } startApp(true);//上面大部分只是在設置ENVIRONMENT_VARIABLES,這裏是關鍵 }
找到一個親切的函數——startApp
:
//查AndroidManifest.xml,易得, <meta-data android:name="android.app.use_local_qt_libs" android:value="1"/> if (m_contextInfo.metaData.containsKey("android.app.use_local_qt_libs") && m_contextInfo.metaData.getInt("android.app.use_local_qt_libs") == 1) { ……//根據AndroidManifest和ENVIRONMENT_VARIABLES的值來配置loaderParams,此處省略一萬字 //這裏調用loaderClassName()設置LOADER_CLASS_NAME,對於QtActivityLoader是org.qtproject.qt5.android.QtActivityDelegate(loaderClassName()函數的名字取得不許確,我的更趨向於叫delegaterClassName()) loaderParams.putString(LOADER_CLASS_NAME_KEY, loaderClassName()); loadApplication(loaderParams); return; } //若是不使用本地qt庫,則綁定ministro的服務,而且在綁定後會啓動下載流程 if (!m_context.bindService(new Intent(org.kde.necessitas.ministro.IMinistro.class.getCanonicalName()), m_ministroConnection, Context.BIND_AUTO_CREATE)) { throw new SecurityException(""); }
startApp
處理了是否須要ministro
介入Qt啓動流程的事,ministro
能夠不在應用中嵌入qt庫,而是在運行的時候去下載必要庫。QtCreator
建立的工程生成的apk是自帶qt庫的,讀者能夠忽略ministro
.
最後startApp
調用了loadApplication
——加載Qt庫並運行:
…… //這裏上文分析了,加載到的是類是:org.qtproject.qt5.android.QtActivityDelegate Class<?> loaderClass = classLoader.loadClass(loaderParams.getString(LOADER_CLASS_NAME_KEY)); // load QtLoader class Object qtLoader = loaderClass.newInstance(); // create an instance //反射調用loadApplication Method prepareAppMethod = qtLoader.getClass().getMethod("loadApplication", contextClassName(), ClassLoader.class, Bundle.class); if (!(Boolean)prepareAppMethod.invoke(qtLoader, m_context, classLoader, loaderParams)) throw new Exception(""); QtApplication.setQtContextDelegate(m_delegateClass, qtLoader); //這裏會加載libhelloworld.so,以及它依賴的其餘庫,如libQt5Core.so等 if (libName != null) System.loadLibrary(libName); //反射調用startApplication Method startAppMethod=qtLoader.getClass().getMethod("startApplication"); if (!(Boolean)startAppMethod.invoke(qtLoader)) throw new Exception("");
這裏涉及qt Android封裝中一個重要的類——delegate。對於QtActivity
而言是QtActivityDelegate
.
到此爲止,接觸了QtActivity, QtActivityLoader , QtActivityDeleagate,這3個類是其Android封裝中很重要的類,注意梳理3個類間關係。
startApp
主要工做是找到loaderClass(成爲delegate更合適),而後調用它的loadApplication
和startApplication
.
Delegate
loadApplication
主要用來讀取loadParams
,並設置一些全局值,不是本文重點,不深刻分析。
startApplication
是重頭戲:
//qtbase-5.10.1/src/android/jar/src/org/qtproject/qt5/android/QtActivityDelegate.java public boolean startApplication() { …… if (null == m_surfaces) onCreate(null); …… } public void onCreate(Bundle savedInstanceState) { …… //建立一個名字是startApplication的「回調」 startApplication = new Runnable() { @Override public void run() { try { String nativeLibraryDir = QtNativeLibrariesDir.nativeLibrariesDir(m_activity); //調用QtNative.startApplication QtNative.startApplication(m_applicationParameters, m_environmentVariables, m_mainLib, nativeLibraryDir); m_started = true; } catch (Exception e) { e.printStackTrace(); m_activity.finish(); } } }; //建立一個佈局,startApplication回調將在QtLayout.onSizeChanged重載中調用 m_layout = new QtLayout(m_activity, startApplication);//QtLayout extends ViewGroup …… //設置爲ContentView m_activity.setContentView(m_layout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); …… }
QtActivityDelegate
的startApplication
調用了本身的onCreate
,onCreate
中主要是建立一個QtLayout
,做爲contentView
,並在QtLayout onSizeChanged
的時候調用局部變量startApplication
指向的回調。而後啓動任務拋給了QtNative.startApplication
.
QtNative.startApplication
:
public static boolean startApplication(String params, String environment, String mainLibrary, String nativeLibraryDir) throws Exception{ synchronized (m_mainActivityMutex) { res = startQtAndroidPlugin(); …… startQtApplication(f.getAbsolutePath() + params, environment);//native startQtApplication m_started = true; } } public static native boolean startQtAndroidPlugin(); public static native void startQtApplication(String params, String env);
調到了native方法,實如今qtbase-5.10.1/src/plugins/platforms/android/androidjnimain.cpp:
static jboolean startQtAndroidPlugin(JNIEnv* /*env*/, jobject /*object*//*, jobject applicationAssetManager*/) { m_androidPlatformIntegration = nullptr; m_androidAssetsFileEngineHandler = new AndroidAssetsFileEngineHandler(); return true; } static jboolean startQtApplication(JNIEnv *env, jobject /*object*/, jstring paramsString, jstring environmentString) { //經過dlopen+dlsym定位libhelloworld.so中的main函數,也就是qt代碼的main函數 m_mainLibraryHnd = dlopen(m_applicationParams.constFirst().data(), 0); if (Q_UNLIKELY(!m_mainLibraryHnd)) { qCritical() << "dlopen failed:" << dlerror(); return false; } m_main = (Main)dlsym(m_mainLibraryHnd, "main"); …… //建立線程調用startMainMethod jboolean res = pthread_create(&m_qtAppThread, nullptr, startMainMethod, nullptr) == 0; …… } static void *startMainMethod(void */*data*/) { …… int ret = m_main(m_applicationParams.length(), const_cast<char **>(params.data())); …… }
這裏native代碼中的startQtApplication
經過dlsym
定位了qt代碼中的main
函數,而後建立了一個線程來執行這個main
函數的代碼(因此qt主線程,並非Android的主線程)。而main
函數,咱們知道會一直執行QApplication.exec()
直到退出。
至此,qt工程生成的libhelloworld.so開始執行main函數,開始表現得就像傳統的桌面qt應用同樣。
讓咱們梳理下。
總結
main
函數並非android應用中的入口函數,只是用做android代碼啓動qt代碼的調用入口(其實c語言中的main也有殊途同歸之處)
入口找到了,找下出口。
傳統qt應用,會在主窗口關閉後,事件循環結束,而後QApplication.exec()
退出,主函數退出。
Android中qt建立的「窗口」其實是Activity中的一塊Surface。並且咱們知道Android Activity的生命週期onDestroy算是退出。但,也不盡然,由於onDestroy後,進程能夠駐留後臺等待系統喚起。
那麼,qt的Android封裝是如何對接的?
先看其QtActivity的lauchMode:android:launchMode="singleTop"
singleTop,簡而言之:若是已經在棧頂,複用,不然,從新建立。這意味着,一旦QtActivity退到其餘Activity後面,下次回到棧頂就須要從新建立。
因此onDestroy
是一個很合理的「退出點」。這和以前的onCreate
做爲啓動入口正好在Android生命週期上是對應的。
onDestroy
以前onStop
會先觸發。
@Override protected void onStop() { super.onStop(); QtApplication.invokeDelegate();//?? } @Override protected void onDestroy() { super.onDestroy(); QtApplication.invokeDelegate();//?? }
onStop和onDestropy都是一句QtApplication.invokeDelegate
搞定,挺優雅的實現,不過對於分析流程而言有點障礙,咱們先看下究竟invokeDelegate
作了什麼?
分析別人代碼,筆者喜歡猜流程。
好比這裏,以前分析啓動流程的時候QtActivity是一個殼,調用QtActivityLoader幫忙加載libhelloworld.so,而真正的又處理髮生在QtActivityDelegate.這和QtApplication.invokeDelegate應該不會只是名字上的巧合。
能夠合理猜想QtApplication.invokeDelegate的任務是「優雅」地把QtActivity要作的事委託給QtActivityDelegate。
帶着這個猜想分析看看吧。
QtApplication.invokeDelegate
private static int stackDeep=-1; public static InvokeResult invokeDelegate(Object... args) { InvokeResult result = new InvokeResult(); if (m_delegateObject == null) return result; //經過調用棧查找要被代理的函數 StackTraceElement[] elements = Thread.currentThread().getStackTrace(); if (-1 == stackDeep) { for (int it=0;it<elements.length;it++) //activityClassName在QtLoader.loadApplication中被設置爲QtActivity if (elements[it].getClassName().equals(activityClassName)) { stackDeep = it; break; } } if (-1 == stackDeep) return result; final String methodName=elements[stackDeep].getMethodName(); if (!m_delegateMethods.containsKey(methodName)) return result; //從m_delegateMethods表中查找要調用的函數,並執行 for (Method m : m_delegateMethods.get(methodName)) { if (m.getParameterTypes().length == args.length) { result.methodReturns = invokeDelegateMethod(m, args); result.invoked = true; return result; } } return result; }
invokeDelegate
根據被代理函數的名字,查表調用了代理函數。
看下m_delegateMethods
的賦值:
//QtLoader.loadApplication中調用到:QtApplication.setQtContextDelegate(m_delegateClass, qtLoader); //m_delegateClass = QtActivity.class //qtLoader = new QtActivityDelegate public static void setQtContextDelegate(Class<?> clazz, Object listener) { m_delegateObject = listener;//代理類設爲QtActivityDelegate activityClassName = clazz.getCanonicalName();//activityClassName設爲QtActivity //反射獲取QtActivityDelegate的方法 ArrayList<Method> delegateMethods = new ArrayList<Method>(); for (Method m : listener.getClass().getMethods()) { if (m.getDeclaringClass().getName().startsWith("org.qtproject.qt5.android")) delegateMethods.add(m); } //反射獲取QtApplication的字段 ArrayList<Field> applicationFields = new ArrayList<Field>(); for (Field f : QtApplication.class.getFields()) { if (f.getDeclaringClass().getName().equals(QtApplication.class.getName())) applicationFields.add(f); } //關聯代理方法 //1. 關聯到m_delegateMethods表 //2. 關聯到QtApplication的字段,如Method onKeyUp for (Method delegateMethod : delegateMethods) { try { clazz.getDeclaredMethod(delegateMethod.getName(), delegateMethod.getParameterTypes()); //1. 關聯到m_delegateMethods表 if (QtApplication.m_delegateMethods.containsKey(delegateMethod.getName())) { QtApplication.m_delegateMethods.get(delegateMethod.getName()).add(delegateMethod); } else { ArrayList<Method> delegateSet = new ArrayList<Method>(); delegateSet.add(delegateMethod); QtApplication.m_delegateMethods.put(delegateMethod.getName(), delegateSet); } //2. 關聯到QtApplication的字段,如Method onKeyUp for (Field applicationField:applicationFields) { if (applicationField.getName().equals(delegateMethod.getName())) { try { applicationField.set(null, delegateMethod); }