Qt android 淺析


Qt android 淺析

來源 https://zhuanlan.zhihu.com/p/36798160

 

Qt5支持編寫Android應用。

典型main

int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); } 

這會在Android設備上顯示一個空白窗口。

但是:

  • 問題1, main函數“沖突”。我們知道Android進程源於zygote的fork,作為進程入口的函數main早就執行過了,那么上述代碼這中Qt的入口函數main又何時怎么被執行的呢?
  • 問題2,Android activity的生命周期是如何轉化為qt的生命周期的?
  • 問題3,qt主線程和Android主線程的關系是什么樣的?

以下基於Qt 5.10.1分析(源碼參考)。

helloworld工程分析

在qtcreator新建一個helloworld的工程,編譯后,在qt的build目錄下,可以看到目錄結構:

- android_build/
- libhelloworld.so
- main.o
- mainwindow.o

.o文件顯然對應各個cpp文件,so文件是.o文件的“集合”。android-build的目錄經過簡單的查看,可以知道是一個gradle組織的android工程。

所以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插件的默認源碼、資源路徑。

主要引入了qt5AndroidDir目錄下的srcaidlres

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依賴。

總結

  • qt編寫的代碼會生成為一個動態庫,以工程名命名
  • qt生成Android應用的策略是借助了一個模板工程
  • 不難猜測,qt應用的運行模式是,通過模板工程生成的Android應用,以native調用方式執行qt代碼

 

啟動流程

上節分析可知,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更合適),然后調用它的loadApplicationstartApplication.

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)); …… }

QtActivityDelegatestartApplication調用了自己的onCreateonCreate中主要是創建一個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應用一樣。

讓我們梳理下。

總結

  • 主要的啟動流程是QtActivity.create -> QtActivityLoader.startApp -> QtActivityDelegate.startApplication -> QtNative.startApplicaiton
  • qt的主線程並不是Android的主線程,二者相互獨立
  • qt的main函數並不是android應用中的入口函數,只是用作android代碼啟動qt代碼的調用入口(其實c語言中的main也有異曲同工之處)
  • AndroidManifest中留了很多“啟動”qt代碼的參數

 

退出流程

入口找到了,找下出口。

傳統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); } catch (Exception e) { e.printStackTrace(); } } } } catch (Exception e) { } } }

加了注釋應該比較容易理解作者的意圖了。這里交叉對比了QtActivityQtAcitivtyDelegate的方法,然后構建了一個“代理方法表”,以方便在全局范圍內,通過QtApplication.invokeDelegate調用。

現在,繼續之前的退出流程分析。

onStop

QtActivity.onStop被QtActivityDelegate.onStop代理:

//QtActivityDelegate.onStop public void onStop() { QtNative.setApplicationState(ApplicationSuspended);//ApplicationSuspended=0 } //QtNative.setApplicationState public static void setApplicationState(int state) { synchronized (m_mainActivityMutex) { switch (state) { case QtActivityDelegate.ApplicationActive: m_activityPaused = false; Iterator<Runnable> itr = m_lostActions.iterator(); while (itr.hasNext()) runAction(itr.next()); m_lostActions.clear(); break;  default: m_activityPaused = true; break; } } updateApplicationState(state);//我們關注下這個 } public static native void updateApplicationState(int state);

跟native代碼:

//androidjnimain.cpp /* enum ApplicationState {  ApplicationSuspended = 0x00000000,  ApplicationHidden = 0x00000001,  ApplicationInactive = 0x00000002,  ApplicationActive = 0x00000004  }; */ static void updateApplicationState(JNIEnv */*env*/, jobject /*thiz*/, jint state) { …… if (state == Qt::ApplicationActive) QtAndroidPrivate::handleResume(); else if (state == Qt::ApplicationInactive) QtAndroidPrivate::handlePause(); if (state <= Qt::ApplicationInactive) { if (QAndroidEventDispatcherStopper::instance()->stopped()) return; QAndroidEventDispatcherStopper::instance()->goingToStop(true); QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationState(state)); if (state == Qt::ApplicationSuspended)//onStop會觸發這個分支  QAndroidEventDispatcherStopper::instance()->stopAll(); } else { …… } } 

這里主要調用了QAndroidEventDispatcherStopper::instance()->stopAll();,native代碼不是重點,不深入分析QAndroidEventDispatcherStopper了。這里stopAll會導致qt main函數中QtApplicaiton.exec循環退出。

循環退出后,qt主線程:

static void *startMainMethod(void */*data*/) { …… int ret = m_main(m_applicationParams.length(), const_cast<char **>(params.data())); …… sem_post(&m_terminateSemaphore); sem_wait(&m_exitSemaphore);//在這里等待信號量  sem_destroy(&m_exitSemaphore); // We must call exit() to ensure that all global objects will be destructed  exit(ret); return 0; } 

“主函數”退出后,會發出m_terminateSemaphore信號,並等待m_exitSemaphore信號。直到m_exitSemaphore信號到來前,線程還不會退出。

onDestroy

QtActivity.onDestroy被QtActivityDelegate.onDestroy代理:

//QtActivityDelegate.onDestroy public void onDestroy() { if (m_quitApp) { QtNative.terminateQt(); QtNative.setActivity(null, null); if (m_debuggerProcess != null) m_debuggerProcess.destroy(); System.exit(0);// FIXME remove it or find a better way  } } //QtNative.terminateQt public static native void terminateQt();

跟native代碼:

//androidjnimain.cpp static void terminateQt(JNIEnv *env, jclass /*clazz*/) { // QAndroidEventDispatcherStopper is stopped when the user uses the task manager to kill the application  if (!QAndroidEventDispatcherStopper::instance()->stopped()) { sem_wait(&m_terminateSemaphore);//等待m_terminateSemaphore  sem_destroy(&m_terminateSemaphore); } …… if (!QAndroidEventDispatcherStopper::instance()->stopped()) { sem_post(&m_exitSemaphore);//發送m_exitSemaphore  pthread_join(m_qtAppThread, nullptr); } } 

先等待m_terminateSemaphore信號量,然后發送m_exitSemaphore信號量。和startMainMethod的退出相輔相成。

至此,退出流程也分析完了。

總結

  • QtActivity使用singleTop模式
  • QtAcitivity的onDestroy是退出點,但qt主線程的退出是由onStop觸發的
  • QtActivity的生命周期處理是用代理的方式,轉變為Qt應用的生命周期處理

 

其他

上面分析的啟動、退出流程只是拋磚引玉。

讀者可以借此分析更多,比如:

  • 跟蹤qt應用的debug實現
  • 分析其他生命周期的實現,比如pause/resume
  • 學習java反射的用法
  • 在啟動和退出流程中hook
  • 基於Android的模板工程,使用Android SDK添加不方便qt實現功能

Service

敏銳的讀者已經發現,除了基於Activity作為qt應用的"容器"外,還有另一條支線——用Service作為”容器“。(QtService、QtServiceLoader、QtServiceDelegate)

感興趣的讀者可以分析看看。

 

=================== End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM