首页 > 架构设计 > 操作系统 > 深入解析鸿蒙2.0手机Beta版的开发和设计
12-16号华为发布鸿蒙2.0手机beta版,以下的问题和分析以及截图等均来自于华为官方的DevEco Studio(版本号2.0.12.201)以及远程提供的P40手机运行环境(除部分可能涉及个人账号的信息会被擦除外,均无任何人为修改)。
任何人(多少有点开发经验)都可以按照文中的步骤重现所有过程。文中的所有的问题及分析,均基于计算机开发的常识(也不排除有错误的可能,可以讨论,但请言之有物)。
步骤一 注册用户及配置开发环境
要开发鸿蒙应用,必须去官网注册开发者身份,批准后即可下载对应的DevEco Studio,并获得使用远程虚拟手机的权限。整个过程没什么必要详细介绍的,有兴趣的同学自行百度即可。
但有几点需要吐槽一下:
安装完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文件如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="29" android:compileSdkVersionCodename="10" package="com.example.myapplication" platformBuildVersionCode="29" platformBuildVersionName="10">
<uses-feature android:name="zidane.software.ability" android:required="false"/>
<uses-sdk android:minSdkVersion="26" android:targetSdkVersion="29"/>
<application android:label="MyApplication" android:icon="@drawable/icon" android:name="com.example.myapplication.ShellMyApplication" android:persistent="false" android:debuggable="true" android:process="com.example.myapplication" android:allowBackup="false" android:usesCleartextTraffic="false" android:resizeableActivity="false" android:directBootAware="false">
<meta-data android:name="permZA" android:value="true"/>
<activity android:label="MyApplication" android:icon="@drawable/icon" android:name="com.example.myapplication.MainAbilityShellActivity" android:process="com.example.myapplication" android:launchMode="standard" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
而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这种进行引导。
<application android:theme="@style/AppTheme" android:label="@string/app_name" android:icon="@mipmap/icon_launcher" android:name="com.huawei.deveco.automator.base.BaseApplication" android:persistent="true" android:allowBackup="false" android:supportsRtl="true" android:usesCleartextTraffic="true" android:roundIcon="@mipmap/icon_launcher" android:appComponentFactory="android.support.v4.app.CoreComponentFactory">
<activity android:name="com.huawei.deveco.automator.ui.MainActivity" android:excludeFromRecents="true" android:launchMode="singleInstance"/>
<activity android:name="com.huawei.deveco.automator.ui.LockScreenActivity" android:excludeFromRecents="true" android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service android:name="com.huawei.deveco.automator.service.MonitorService" android:permission="" android:enabled="true" android:exported="true" android:foregroundServiceType="20">
<intent-filter android:priority="10000">
<action android:name="com.huawei.deveco.automator.ACTION_START"/>
<action android:name="com.huawei.deveco.automator.ACTION_STOP"/>
<action android:name="com.huawei.deveco.automator.AUDIO_RECORD_START"/>
<action android:name="com.huawei.deveco.automator.AUDIO_RECORD_STOP"/>
</intent-filter>
</service>
晚上没事干,顺手改了下代码,想确认一下类的加载是否如我猜测,在代码里拿到当前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是安卓换壳,是有一点过头了,毕竟还是做了一些事情的。但很可惜,我并不是特别看好这条路。
步骤四 对鸿蒙的部分解析
我把能看到的代码大致过一遍,也基本理解了鸿蒙2.0在开发这块的思路了(当然,只是个人看法)。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31562318/viewspace-3107379/ ,如需转载,请注明出处,否则将追究法律责任。