12-16号华为发布鸿蒙2.0手机beta版,以下的问题和分析以及截图等均来自于华为官方的DevEco Studio(版本号2.0.12.201)以及远程提供的P40手机运行环境(除部分可能涉及个人账号的信息会被擦除外,均无任何人为修改)。
任何人(多少有点开发经验)都可以按照文中的步骤重现所有过程。文中的所有的问题及分析,均基于计算机开发的常识(也不排除有错误的可能,可以讨论,但请言之有物)。
步骤一 注册用户及配置开发环境
要开发鸿蒙应用,必须去官网注册开发者身份,批准后即可下载对应的DevEco Studio,并获得使用远程虚拟手机的权限。整个过程没什么必要详细介绍的,有兴趣的同学自行百度即可。
但有几点需要吐槽一下:
- 每次使用远程虚拟机,都得通过浏览器登录,能不能直接提供账号密码的登录方式,实在不行,你使用内置浏览器也好办点啊。
- 不提供本地虚拟机实在是太慢了,断点调试时,需要从远程虚拟手机上取得堆栈调用信息,我恍如看动画一样看着那个堆栈树在刷新(附一句,我用的是1G光纤加5G 300M Wifi)。
- DevEco Studio不知道怎么想的,虚拟手机的交互界面上部占用了极大的空间显示着P40和剩余使用时间(单次只能一个小时),把这些信息放在工具栏上面不好吗?实在不行,你合成一行,或者放在侧面,也节省点空间啊(如果是MediaPad,倒是可以放上面)。
- DevEco Studio的界面预览功能,应该是用NodeJs来支持,但非常之慢,我还没有成功过呢,反正界面简单,实在懒得折腾了。
- 虚拟手机的调试功能不稳定,时不时就出现无法使用,只能重新启动。
- 虚拟手机的WebView有问题,我无法使用浏览器,甚至无法登录QQ等,除了影响使用外,我连程序生成的文件都不方便拿到本地,对于调试非常不友好。
- Idea太吃内存了,我8G的笔记本,用它总是内存满满的,还出现几次内存不足退出。
安装完DevEco Studio后,还需要在Setting中下载HarmonyOS SDK后才能使用,这点和Android Studio是一样的,熟悉的同学应该很容易上手,不熟悉的自己百度吧。
对于分析APK等程序,我使用的是jadx1.2,可以直接去下面地址自行下载。
Version releases/v1.2.0 - skylotbintray.com/skylot/jadx/releases/v1.2.0#files
步骤二 直接开发并远程部署一个Hap应用
启动DevEco Studio后,创建一个手机应用,并选择Studio提供的Business Card模板(也就是一个名片列表应用),然后一路点下去,直接使用默认的MyApplication这个名字,然后就很顺利地生成整个项目了。
接下来,需要在菜单里打开 Tools-> HVD Manager,中间可能需要登录到华为的开发账户,或者可以提前在 Tools->DevEco Login->Login进行登录操作。
登录成功后,启动HVD Manager,就有一系列设备可选,直接选择P40即可(也只有这一个手机设备可以选择),然后点击蓝色的三角启动按钮即可使用远程的虚拟机,大概几秒到几十秒(启动比较快,我本机只需要5秒左右即可使用了),就可以看到远程可用的虚拟机。
虚拟机启动后,即可通过上面的Run/Debug远程部署自己的应用了。
实在无法不吐槽这个产品经理的设计,上面的两行字占了差不多四分之一的空间
部署时出现的是以下内容:
12/17 11:01:08: Launching com.example.myapplication
$ hdc shell am force-stop com.example.myapplication
$ hdc shell bm uninstall com.example.myapplication
$ hdc file send C:/Users/***/DevEcoStudioProjects/MyApplication/entry/build/outputs/hap/debug/entry-debug-unsigned.hap /sdcard/670e402d7ec643aab51bb93c72ca6c77/entry-debug-unsigned.hap
$ hdc shell bm install -p /sdcard/670e402d7ec643aab51bb93c72ca6c77/
$ hdc shell rm -rf /sdcard/670e402d7ec643aab51bb93c72ca6c77
$ hdc shell am start -n "com.example.myapplication/com.example.myapplication.MainAbilityShellActivity" -D
Waiting for application to come online: com.example.myapplication
Connecting to com.example.myapplication
和Android的adb基本类似,然后这个应用就跑起来了。
步骤三 对Hap应用的调试-反编译及运行时分析
本地找到生成的hap文件,是一个zip格式的压缩包,解开后,可以看到以下内容。
使用Jadx把entry_ signed_entry.apk和classes.dex解开,分别得到以下内容:
其中apk中所带的AndroidManifest.xml文件如下:
而hap文件里带的classes.dex也是目前Android的编译后运行文件格式,也正是项目里的Java代码生成的目标文件。
从上面来看,APK还是一个遵守Android标准的App包,然后我打开里面的ShellMyApplication,看里面的代码:
package com.example.myapplication;
import ohos.abilityshell.HarmonyApplication;
public class ShellMyApplication extends HarmonyApplication {
public void onCreate() {
ShellMyApplication.super.onCreate();
}
}
具体的工作应该是由ohos.abilityshell.HarmonyApplication来完成的,于是我在DevEco Studio中找到这个类,这个类继承自android.app.Application。
我把代码详细的过了一遍,大概理解了整个运行的过程。
首先是这个apk会按照正常的Android生命周期加载,启动ShellMyApplication,然后在它的父类HarmonyApplication里有一个setBundleInfo(BundleInfo bundleInfo)方法,由外部注入了配置信息,这个信息我断点查看,发现和config.json是一致的。而config.json则是对myapplication应用做了定义。然后在createUserApplication方法里,再调用setUserApplication等方法,通过当前Application实例的ClassLoader加载了用户编写的类,即MyApplication这个类,再将接下来的工作转给MyApplication等用户编写的功能类。因为Ability是放的stub信息,所以没有办法深入分析,但估计也差不多。
也就是说这个apk起的是一个引导和加载功能,下面代码来自ohos.abilityshell.HarmonyApplication,即ShellMyApplication的父类,明显的展示了是通过反射加载hap包里的classes.dex:
public void loadClass(String moduleName) {
if (this.bundleInfo == null) {
AppLog.d(SHELL_LABEL, "bundleInfo is null", new Object[0]);
} else {
HapModuleInfo hapModuleInfo = this.bundleInfo.getHapModuleInfo(moduleName);
if (hapModuleInfo != null && hapModuleInfo.getName() != null) {
String name = hapModuleInfo.getName();
if (loadedHapMap.containsKey(moduleName)) {
AppLog.d(SHELL_LABEL, "this module %{public}s has been load", new Object[]{name});
} else {
try {
Class> clazz = this.getClassLoader().loadClass(name);
Object obj = clazz.newInstance();
if (obj instanceof AbilityPackage) {
loadedHapMap.put(moduleName, (AbilityPackage)obj);
this.attachHapModuleContext((AbilityPackage)obj, hapModuleInfo);
currentModuleName = moduleName;
this.application.setHarmonyosAbilityPackage(moduleName, obj);
((AbilityPackage)obj).onInitialize();
}
} catch (ClassNotFoundException var6) {
loadedHapMap.put(moduleName, new AbilityPackage());
AppLog.d(SHELL_LABEL, "HarmonyApplication::setApplicationEnv class not found exception %{public}s", new Object[]{name});
} catch (IllegalAccessException | InstantiationException var7) {
AppLog.d(SHELL_LABEL, "HarmonyApplication::setApplicationEnv newInstance failed", new Object[0]);
}
}
} else {
AppLog.d(SHELL_LABEL, "hap moduleInfo is null", new Object[0]);
}
}
}
private void setUserApplication(String moduleName) {
if (this.bundleInfo == null) {
AppLog.d(SHELL_LABEL, "bundleInfo is null", new Object[0]);
} else {
HapModuleInfo hapModuleInfo = this.bundleInfo.getHapModuleInfo(moduleName);
if (hapModuleInfo != null && hapModuleInfo.getName() != null) {
String name = hapModuleInfo.getName();
try {
Class> clazz = this.getClassLoader().loadClass(name);
Object obj = clazz.newInstance();
if (obj instanceof HarmonyosApplication) {
userApplication = (HarmonyosApplication)obj;
this.attachHapModuleContext(userApplication, (HapModuleInfo)null);
this.application.setHarmonyosApplication(obj);
loadedHapMap.put(moduleName, new AbilityPackage());
this.application.setHarmonyosAbilityPackage(moduleName, new AbilityPackage());
} else if (obj instanceof AbilityPackage) {
loadedHapMap.put(moduleName, (AbilityPackage)obj);
this.attachHapModuleContext((AbilityPackage)obj, hapModuleInfo);
currentModuleName = moduleName;
this.application.setHarmonyosAbilityPackage(moduleName, obj);
this.application.setHarmonyosApplication(new HarmonyosApplication());
}
} catch (ClassNotFoundException var6) {
this.application.setHarmonyosApplication(new HarmonyosApplication());
loadedHapMap.put(moduleName, new AbilityPackage());
this.application.setHarmonyosAbilityPackage(moduleName, new AbilityPackage());
AppLog.d(SHELL_LABEL, "HarmonyApplication::setApplicationEnv class not found exception %{public}s", new Object[]{name});
} catch (IllegalAccessException | InstantiationException var7) {
AppLog.d(SHELL_LABEL, "HarmonyApplication::setApplicationEnv newInstance failed", new Object[0]);
}
} else {
AppLog.d(SHELL_LABEL, "entry hap moduleInfo is null", new Object[0]);
}
}
}
下面的代码则是ohos.abilityshell.HarmonyApplication引入的包和继承关系,明显是继承了android.app.Application这个类:
package ohos.abilityshell;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.Handler.Callback;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.AbilityPackage;
import ohos.aafwk.ability.HarmonyosApplication;
import ohos.abilityshell.delegation.AbilityDelegator;
import ohos.app.AbilityContext;
import ohos.app.ContextDeal;
import ohos.app.DumpHelper;
import ohos.app.ProcessInfo;
import ohos.app.dispatcher.threading.AndroidTaskLooper;
import ohos.appexecfwk.utils.AppLog;
import ohos.bundle.AbilityInfo;
import ohos.bundle.BundleInfo;
import ohos.bundle.HapModuleInfo;
import ohos.eventhandler.EventRunner;
import ohos.global.resource.ResourceUtils;
import ohos.hiviewdfx.HiLogLabel;
import ohos.idn.BasicInfo;
import ohos.idn.DeviceManager;
import ohos.security.keystore.provider.HarmonyKeyStoreProvider;
import ohos.tools.Bytrace;
public class HarmonyApplication extends Application
下图则是鸿蒙SDK自带的库,明显是对应着Android的Activey/ContentProvider/Service组件,但少了一个BroadCastRecevicer,进去看代码,也是继承自Android的组件类。
还有ohos.abilityshell.IAbilityShell这个类的设计其实是有点问题的,看代码:
package ohos.abilityshell;
import android.content.Context;
import android.view.View;
public interface IAbilityShell {
void setSystemView(View var1);
Context getSystemContext();
ClassLoader getSystemClassLoader();
}
正常情况下,感觉Ability这个东西应该是与UI没有绑定的(我不讨论绑定UI是不是好),而华为的宣传也是,但在IAbilityShell设计里竟然出现了android.view.View这种UI的类,从设计上讲肯定是有问题的,正常设计应该是有一个子类AbstractAbilityShell,由它提供这个方法,在内部调用时可以先判断类型,而不应该在顶层接口直接暴露View。
晚上终于通过邮箱的方式,把远程虚拟手机上的DevEco Service和DRServer对应的APK下载下来,分别反编译后得到的是两个标准的Android对应的APK文件,并没有使用鸿蒙的HAP格式,图放在下面。
AndroidManifest.xml文件太长,随便复制了一段在下面,并没有像HAP格式使用AbilityShellActivity这种进行引导。
晚上没事干,顺手改了下代码,想确认一下类的加载是否如我猜测,在代码里拿到当前ClassLoader和父ClassLoader。不得不吐槽一把,这个鸿蒙的远程调试简直是太....,不是加不了断点,就是看调用堆栈时感觉幻灯片,关键是就为了看看这些ClassLoader,真机上3分钟搞定的事,我前后折腾一个小时。现在直接上图。
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.myapplication-_0F6pSKs6nF8pHchlq4gbg==/base.apk", zip file "/data/app/com.example.myapplication-_0F6pSKs6nF8pHchlq4gbg==/feature_entry-debug-unsigned.hap"],nativeLibraryDirectories=[/data/app/com.example.myapplication-_0F6pSKs6nF8pHchlq4gbg==/lib/arm64, /system/lib64, /hw_product/lib64, /system/product/lib64]]]
以上代码,大家就懂了,还是有一整套Android Runtime在支持App运行的。
继续补充干货,今晚用了两个小时来看一整个UI界面的创建等情况,把里面涉及的类及变量还有流程用断点查看变量的方式,过了一遍(当然还有一些特殊的逆向手段)。最吐血的是,这两个小时,我有一个半小时甚至更久在等远程加载变量和堆栈。目前华为提供的虚拟机根本不具有真正意义上的可用性。
先上张图,再次证明HAP仍然是一个非标准的Android程序,运行在Android Runtime里。
这也和我前面的猜测一致。顺便又安装了一个使用QT开发的APK和一个硬件检测APK,看看底层是不是Linux(虽然通过adb shell已经接近实锤了)。不多说了。继续上图。
下面要实锤一下华为的鸿蒙2.0里到底有什么,根据我在断点里看到的东西,鸿蒙2.0确实不能简单的看作是安卓换壳,虽然太多的人都这样说,但这样简单的断言对华为说还是有点不公平的,我翻了Button/Image/Component...等类(想多翻一点,但太慢了),虽然它们可能多少都用得了一些Android的东西,但还是可以算作独立设计的一个东西,看完这些类,我大概也明白了鸿蒙2.0到底在做什么?非常类似于Qt了,好比原来Windows上提供了win32API,而Qt则提供了新的API(尽管在windows平台上运行时,底层还是要调用win32 api),这样就为未来鸿蒙迁移到别的平台上提供了可能性,这和我另一篇文章的内容也是一致的。
所以直接批评鸿蒙2.0是安卓换壳,是有一点过头了,毕竟还是做了一些事情的。但很可惜,我并不是特别看好这条路。
- 可以看得出来这套API是极大的模仿了Android的代码风格,比如类里的变量都是用m打头,它离成熟还早呢,因为看不到源代码,我不好简单评价是什么水平,很有可能在正式版本发布的时候也就是一个Android4水平,虽然不能小看Android4,那是Android走向成熟的一个标志,但那也是9年前的东西,离现在的Android11差距还是很大的。(额外说一句,我不太相信这套API是多年前就开始研发的,很可能是近一两年设计和赶工的)
- 未来这套API就算独立出来,可以在非Android Runtime或者说别的环境里运行时,原有的Android第三方控件应该都无法使用了(虽然可能有些方法绕过,但现在官方是没有说能用第三方控件的),对于很多公司来讲,会严重的压制为鸿蒙开发App的动力,就算华为做到所谓的分布式(我也持极其怀疑的态度),又有多少App用得上,似乎动力不足。
- 只要兼容Android,普通公司是绝对没有动力迁移系统的。
- 这套API的Android痕迹很重,虽然说AOSP不存在版权的问题,但也仍然可能是隐藏的雷。
- 这套东西就算未来不使用Android Runtime了,也还是Java语言,恐怕海外市场也仍然没有机会(参见Oracle起诉google赔88亿刀)。
步骤四 对鸿蒙的部分解析
我把能看到的代码大致过一遍,也基本理解了鸿蒙2.0在开发这块的思路了(当然,只是个人看法)。
- 鸿蒙2.0提供的SDK,对外的接口完全屏蔽了Android(至少我翻的上百个类对外接口都不存在暴露Android类的内容),这样如果有一天完全脱离Android框架,理论上讲是有可能的(虽然难度极大)。但由于还是使用Java代码开发,所以理论上讲会和Google一样,会碰到Oracle的版权问题,而且对java库的依赖,从长远看,应该也没办法脱离Java。
- Apk用来引导,然后使用ClassLoader加载用户程序,优点是可以屏蔽Android接口,缺点是在某些环境上可能会出不兼容问题,用过Shell方式封装Activity的同学应该知道我说的是什么,但华为从底层通过config.json可能容易解决这个问题。
- 至少在虚拟手机层面,是有一个Android运行时存在的,至于这个Android Runtime是不是华为开发的,还是直接来自AOSP,我就不猜了,也不评论了。
- 根据Log看到的信息,虚拟手机层面,应该还是有一个Linux Runtime存在的,因为里面的各种运行环境和参数都展示了有linux各种库参与。至于有没有鸿蒙微内核的存在,我没法证明,也没法证伪。
- 如果鸿蒙微内核也存在,和Linux Runtime一起存在,这种双内核结构,只能说我的见识没法猜想两者如何协作,比如由谁负责资源中断,如果哪位有双内核资源调配的方案或者论文,可以补充一下。