QT For Android 源码解析

李兴庆
2023-12-01

1,前言

本文主要用于总结之前分析QT For Android 从启动到QWidget启动的相关代码,可以通过该文大概知晓QT是如何对Android平台进行支持的。
话不多说,直接进入正文。

2,Qt For Android的启动

2.1 在Android的世界

通过Qt For Android 开发的应用说到底,其本质依旧是一个Android 的APP。而传统的Android是通过Java进行开发的,而且我们都知道绝大部分的Android App是由一系列的Activity组成的,Activity可以认为是Android App的入口。而QT 也是通过Activity来作为连接QT和Android的桥梁。

2.2 QtActivity

通过查看QT For Android 的AndroidManifest.xml(该文件在:qtbase\src\android\templates中),我们可以知道QT For Andoird应用的主Activity是QtActivity

qtbase\src\android\java\src\org\qtproject\qt5\android\bindings\QtActivity.java

	...
	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);
    }
    
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        onCreateHook(savedInstanceState);
    }
    ...

通过该Activity的onCreate函数,我们可以知道,QtActivity将onCreate的工作交给m_loader.onCreate函数,m_loader是一个QtActivityLoader对象。

2.3 QtActivity的onCreate

qtbase\src\android\java\src\org\qtproject\qt5\android\bindings\QtActivityLoader.java

public void onCreate(Bundle savedInstanceState) {
			...
            startApp(true);
            
    }

该onCreate函数没有什么特别的,其主要是做一些环境和主题的初始化,而后便是调用startApp来进行相关动态库的加载。该startApp的实现在QtActivityLoader的父类QtLoader

qtbase\src\android\java\src\org\qtproject\qt5\android\bindings\QtLoader.java

    public void startApp(final boolean firstStart)
    {
	    ...
	    loaderParams.putString(LOADER_CLASS_NAME_KEY, loaderClassName()); // (1) 获取QT Loader的类名
	    ...
	    loadApplication(loaderParams);	// (2) 加载相关库
	    ...
    }

在QtLoader的starApp中也没有做太多有意义的事情,主要是获取AndroidManifest配置文件中的相关依赖库,并且将相关库的列表放入loaderParams中,其中比较关键的代码如下:
(1) 通过loaderClassName() 函数来获取QT Loader的类名 而后调用loadApplication函数。该函数的实现如下(该函数实现在QtActivityLoader中):

qtbase\src\android\java\src\org\qtproject\qt5\android\bindings\QtActivityLoader.java

    protected String loaderClassName() {
        return "org.qtproject.qt5.android.QtActivityDelegate";
    }

(2)调用loadApplication(loaderParams) 来加载相关类,其具体实现如下:

qtbase\src\android\java\src\org\qtproject\qt5\android\bindings\QtLoader.java

    private void loadApplication(Bundle loaderParams){
			Class<?> loaderClass = classLoader.loadClass(loaderParams.getString(LOADER_CLASS_NAME_KEY)); // (3)通过java反射机制加载以LOADER_CLASS_NAME_KEY为名的类
         	Object qtLoader = loaderClass.newInstance(); //  实例化出一个对象
        	Method prepareAppMethod = qtLoader.getClass().getMethod("loadApplication", 
                    contextClassName(),
                    ClassLoader.class,
                    Bundle.class); 	// (4)获取对象的loadApplication函数
            if (!(Boolean)prepareAppMethod.invoke(qtLoader, m_context, classLoader, loaderParams))	// (5) 调用函数loadApplication函数
                throw new Exception("");
                
            Method startAppMethod=qtLoader.getClass().getMethod("startApplication"); // (6) 获取对象的startApplication函数
            if (!(Boolean)startAppMethod.invoke(qtLoader))  // (7) 调用startApplication函数
                throw new Exception("");
	}

上述loadApplication函数主要的工作如下:
(3)加载LOADER_CLASS_NAME_KEY对应的类,并调用其loadApplication函数。而我们从上文可以知道,LOADER_CLASS_NAME_KEY对应的类名为org.qtproject.qt5.android.QtActivityDelegate
(4,5)获取该类中的loadApplication函数并调用,该函数主要是负责加载相关库,本文不做深入分析。
(6,7)获取该类中的startApplication函数并调用,该函数大部分也是在做初始化相关的动作,最后变会调用一个关键的函数QtActivityDelegateonCreate函数

    public void onCreate(Bundle savedInstanceState)
    {
		/**
		* (8)创建runable
		*/
        Runnable startApplication = null;
        if (null == savedInstanceState) {
            startApplication = new Runnable() {
                @Override
                public void run() {
                    try {
                        String nativeLibraryDir = QtNativeLibrariesDir.nativeLibrariesDir(m_activity);
                        QtNative.startApplication(m_applicationParameters,
                            m_environmentVariables,
                            m_mainLib,
                            nativeLibraryDir);
                        m_started = true;
                    } catch (Exception e) {
                        e.printStackTrace();
                        m_activity.finish();
                    }
                }
            };
        }
        m_layout = new QtLayout(m_activity, startApplication);	// (9) 将创建的runable用来构建QtLayout

        try {
            ActivityInfo info = m_activity.getPackageManager().getActivityInfo(m_activity.getComponentName(), PackageManager.GET_META_DATA);
            if (info.metaData.containsKey("android.app.splash_screen_drawable")) {
                m_splashScreenSticky = info.metaData.containsKey("android.app.splash_screen_sticky") && info.metaData.getBoolean("android.app.splash_screen_sticky");
                int id = info.metaData.getInt("android.app.splash_screen_drawable");
                m_splashScreen = new ImageView(m_activity); // (10) 创建启动屏,实际上就是一个
                m_splashScreen.setImageDrawable(m_activity.getResources().getDrawable(id));
                m_splashScreen.setScaleType(ImageView.ScaleType.FIT_XY);
                m_splashScreen.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                m_layout.addView(m_splashScreen);  // (11) 添加进activity的content layout中
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
		...
        m_activity.setContentView(m_layout,
                                  new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                                             ViewGroup.LayoutParams.MATCH_PARENT));	// (12) 将创建的QtLayout设置为Activity的COntentView
 		...

    }

QtActivityDelegate的onCreate函数中:
(8,9) 首先创建了一个关键的Runable,并且将其传递给了QtLayout的构造函数。
(10,11)接着就是判断AndroidManifest.xml文件中是否有指定splash_screen,也就是启动屏,如果有就会将该启动splash_screen(实际上就是一个ImageView)添加进刚刚创建的m_layout中。于是Activity启动的时候便会先显示我们在AndroidManifest.xml文件中指定的图片。
(12)最后并且(8,9)创建的QtLayout设置为Activity的contentview。而且个刚刚创建的Runable会在QLayoutonSizeChanged事件中被调用。而这个事件是Activity显示过中发生的。

至此,Qt For Android中的QtActivity的onCreate函数便分析完成了。可是看到这里,仍然没有看到任何和Qt相关的东西,Qt的界面究竟是如何显示在Android上的呢?那就得看接下来的分析。

2.4当QtActivity显示出来后

当QtActivity显示出来后,就会调用我们在上文中说的Runable了。而这个Runable只是调用了QtNative类中的startApplication

    public static boolean startApplication(String params,
                                           String environment,
                                           String mainLibrary,
                                           String nativeLibraryDir) throws Exception
    {
        File f = new File(nativeLibraryDir + "lib" + mainLibrary + ".so");
        if (!f.exists())
            throw new Exception("Can't find main library '" + mainLibrary + "'");

        if (params == null)
            params = "-platform\tandroid";

        boolean res = false;
        synchronized (m_mainActivityMutex) {
            res = startQtAndroidPlugin();
            setDisplayMetrics(m_displayMetricsScreenWidthPixels,
                              m_displayMetricsScreenHeightPixels,
                              m_displayMetricsDesktopWidthPixels,
                              m_displayMetricsDesktopHeightPixels,
                              m_displayMetricsXDpi,
                              m_displayMetricsYDpi,
                              m_displayMetricsScaledDensity,
                              m_displayMetricsDensity);
            if (params.length() > 0 && !params.startsWith("\t"))
                params = "\t" + params;
            startQtApplication(f.getAbsolutePath() + params, environment);
            m_started = true;
        }
        return res;
    }

这个函数及其的简单,设置了相关参数后,便调用了JNI函数,startQtApplication,从此进入了C++的世界。

3,进入QT的世界

static jboolean startQtApplication(JNIEnv *env, jobject /*object*/, jstring paramsString, jstring environmentString)
{
	...
    // Go home
    QDir::setCurrent(QDir::homePath());

    //look for main()
    if (m_applicationParams.length()) {
        // Obtain a handle to the main library (the library that contains the main() function).
        // This library should already be loaded, and calling dlopen() will just return a reference to it.
        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");
    } else {
        qWarning("No main library was specified; searching entire process (this is slow!)");
        m_main = (Main)dlsym(RTLD_DEFAULT, "main");
    }
	...
    return 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())); // 执行QT的main函数
    sem_post(&m_terminateSemaphore);
    sem_wait(&m_exitSemaphore);
    sem_destroy(&m_exitSemaphore);

    exit(ret);
    return 0;
}

该函数的主要目的就是在于,加载我们用户所编写的main函数。我们知道,在android中启动一个activity会创建一个进程,而这个进程是已经拥有mian函数的,其入口在java虚拟机创建时指定。于是,QT将用户编写的main函数放在了so中,然后通过上述代码,加载SO,并且执行其main函数,于是便彻底的进入了QT的世界。

4,后续文章

后续将会分析QT是如何在android是显示QWidget的,QT的platform plugin到底是什么东西。

5,参考文章

Qt android浅析: https://zhuanlan.zhihu.com/p/36798160 写得十分好的一篇文章

 类似资料: