轉自: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分析(源碼參考https://github.com/qt/qtbase/releases/tag/v5.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
目錄下的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
依賴。
總結
- 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更合適),然後調用它的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應用一樣。
讓我們梳理下。
總結
- 主要的啓動流程是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) {
}
}
}
加了註釋應該比較容易理解作者的意圖了。這裏交叉對比了QtActivity
和QtAcitivtyDelegate
的方法,然後構建了一個“代理方法表”,以方便在全局範圍內,通過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)
感興趣的讀者可以分析看看。