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分析(源码参考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 += " QT_ANDROID_THEME=" + QT_ANDROID_DEFAULT_THEME
+ "/ QT_ANDROID_THEME_DISPLAY_DPI=" + m_displayDensity + " ";
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 ";
} else {
ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=1 ";
}
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 ";
}
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);