Feb 25

来自Facebook的骚操作,Osmeta的跨平台App开发方案窥探

Lrdcq , 2018/02/25 22:46 , 程序 , 閱讀(6719) , Via 本站原創
11月初的时候,Facebook的Socal应用更新了一波,有人发现:
點擊在新視窗中瀏覽此圖片
神奇,值得一看。经过拆解,我们首先注意到整个程序的包名,资源名,项目名都包含osmeta。Osmeta是何许人也,一搜,已经全是1314年的新闻了:Facebook收购一家神秘的移动软件公司Osmeta,可能是为了利用它的系统虚拟化技术把Facebook Home移植到更多设备上。从这个新闻之后,osmeta完全没有消息了,新闻中提到的系统虚拟化技术或者说是跨平台移动开发技术也没消息了,莫非Facebook Socal的这次更新就是osmeta团队鲤跃龙门的暴击。

本篇博客主要对Facebook Socal进行逆向和分析,窥探其到底是什么骚操作。P.S.跳票了两个月真是抱歉。

拆解分析

我们用apktool拆开整个apk,可以看到:
點擊在新視窗中瀏覽此圖片
其中包括上面截图中assets里面,那些xcode项目风格的资源文件:
點擊在新視窗中瀏覽此圖片
而lib里面有大量非常巨大的.so,可以相信程序主要逻辑都在这里面了:
點擊在新視窗中瀏覽此圖片
另外安卓资源文件夹里面确实没啥东西,看起来只有加载状态的几张资源和布局:
點擊在新視窗中瀏覽此圖片
嗯,把这些文件大概浏览一遍之后,可以了解到:

- 作为安卓项目的资源结构绝大部分都没有被使用,主要资源集中在assets中的SystemResources.zip的一个xcode项目风格的目录结构下
- 主classes.dex并不大,而对应的lib中的二进制内容非常巨大,解压后超过200mb,基本可以断定主要程序逻辑在lib中
- AndroidManifest.xml是一个非常范式的声明,包括一个主activity:com.osmeta.runtime.OMActivity和一大堆service,还有一个值得关注的OMExpansionActivity。这个结构一看就是一个runtime的框架,并没有业务实现相关的声明在里面。
- 其他:assets里面的objc_selector_table看着十分引人注目,但是并不清楚到底如何使用。里面的main_obb.sha剧透了这个app包含obb热更新。xcode资源目录里的plist就是传统意义上的ios项目的plist包名也是一个范式声明的com.osmeta.resources.system,版本是1.0.0,而版权声明Copyright 2013-present说明了这个项目的历史。

那么我们继续进行拆解。

java代码

首先逆向dex的代码,直接用jdgui进行查看:

- 首先看入口OMActivity:

        - OMActivity看起来主要是做了程序初始化和生命周期的传递工作
  protected void onCreate(Bundle paramBundle)
  {
    Date localDate = new Date();
    Log.d("osmeta", String.format("[%tF %tT.%tL] [startup profiling] OMActivity onCreate", new Object[] { localDate, localDate, localDate }));
    long l1 = System.currentTimeMillis();
    if (!sInitialized) {}
    try
    {
      sOMClassLoader = new OMClassLoader(getApplicationContext());
      sActivityLifecycleHandlerClass = sOMClassLoader.forName("com.osmeta.runtime.OMActivityLifecycleHandler");
      sActivityLifecycleHandlerOnLifecycleEvent = sActivityLifecycleHandlerClass.getMethod("onLifecycleEvent", new Class[] { Activity.class, String.class, Object[].class });
      sInitialized = true;
      this.mRequiresExpansionFileSetup = OMExpansionActivity.needsPermissionsOrObbFiles(this);
      l2 = System.currentTimeMillis();
      invoke("preOnCreate", new Object[] { getIntent(), paramBundle });
      l3 = System.currentTimeMillis();
      super.onCreate(paramBundle);
      l4 = System.currentTimeMillis();
      if (this.mRequiresExpansionFileSetup)
      {
        paramBundle = new Intent(this, OMExpansionActivity.class);
        paramBundle.putExtra("OMRelaunchActivityNameKey", getClass().getName());
        startActivity(paramBundle);
        finish();
        return;
      }
    }
    catch (Exception localException)
    {
      long l2;
      long l3;
      long l4;
      for (;;)
      {
        Log.e("osmeta", "Couldn't setup link to com.osmeta.runtime.OMActivityLifecycleHandler", localException);
      }
      invoke("onCreate", new Object[] { getIntent(), paramBundle });
      long l5 = System.currentTimeMillis();
      paramBundle = new Date();
      Log.d("osmeta", String.format("[%tF %tT.%tL] [startup profiling] OMActivity onCreate total time: %dms (init: %dms, preOnCreate: %dms, super.onCreate: %dms, onCreate: %d)", new Object[] { paramBundle, paramBundle, paramBundle, Long.valueOf(l5 - l1), Long.valueOf(l2 - l1), Long.valueOf(l3 - l2), Long.valueOf(l4 - l3), Long.valueOf(l5 - l4) }));
    }
  }

        - 核心初始化了sActivityLifecycleHandlerOnLifecycleEvent这个方法即OMActivityLifecycleHandler的onLifecycleEvent方法
        - 另外所有接受的生命周期事件,触控事件,还有些乱七八糟的都通过这个方法传递到Handler中去了
        - OMExpansionActivity在这里看起来是验权和obb相关的步骤,无关紧要

- 那么OMActivityLifecycleHandler打开看看

        - OMActivity发送过来的那么多事件,包括preOnCreate,onCreate,preOnPostCreate,onPostCreate,preOnDestroy,onDestroy,preOnResume,onResume,preOnPostResume,onPostResume,preOnPause,onPause,preOnConfigurationChanged,onConfigurationChanged,preOnWindowFocusChanged,onWindowFocusChanged,preOnBackPressed,onBackPressed,preOnLowMemory,onLowMemory,preOnTrimMemory,onTrimMemory,preOnRestart,onRestart,preOnStart,onStart,preOnStop,onStop,preOnActivityResult,onActivityResult,onRequestPermissionsResult,preOnNewIntent,onNewIntent,preOnRestoreInstanceState,onRestoreInstanceState,preOnSaveInstanceState,onSaveInstanceState,preDispatchTouchEvent,dispatchTouchEvent,preDispatchGenericMotionEvent,dispatchGenericMotionEvent,preDispatchTrackballEvent,dispatchTrackballEvent,preDispatchKeyEvent,dispatchKeyEvent,preDispatchKeyShortcutEvent,dispatchKeyShortcutEvent。虽然里面好多空实现,但是大部分关键的还是有后续操作。
        - onCreate的实现:
  private static void onCreate(Activity paramActivity, Intent paramIntent, Bundle paramBundle)
  {
    if (DEBUG) {
      Log.i(TAG, "OMActivityLifecycleHandle.onCreate");
    }
    Bundle localBundle = paramIntent.getExtras();
    int i = 0;
    if (localBundle != null)
    {
      if (localBundle.getShort("VR_PRESENTATION_REQUEST", (short)0) != 0) {
        i = 1;
      }
    }
    else
    {
      sHierarchyViewerEnabled = "1".equals(System.getenv("OSMETA_HIERARCHY_VIEWER"));
      if (sHierarchyViewerEnabled) {
        ViewServer.get(paramActivity).addWindow(paramActivity);
      }
      if (!OMApplication.isApplicationLoaded()) {
        break label270;
      }
      sNewLaunchIntent = paramIntent;
      label81:
      OMApplication.nativeHandleOnCreate(paramActivity, paramIntent, paramBundle);
      sContainerView = new FrameLayout(paramActivity)
      {
        private int mInsetBottom = 0;
        private int mInsetLeft = 0;
        private int mInsetRight = 0;
        private int mInsetTop = 0;
        
        public boolean fitSystemWindows(Rect paramAnonymousRect)
        {
          if (OMActivityLifecycleHandler.DEBUG) {
            Log.i(OMActivityLifecycleHandler.TAG, "OMActivityLifecycleHandle.ContainerView.fitSystemWindows(" + paramAnonymousRect.left + ", " + paramAnonymousRect.top + ", " + paramAnonymousRect.right + ", " + paramAnonymousRect.bottom + ")");
          }
          if ((this.mInsetLeft != paramAnonymousRect.left) || (this.mInsetTop != paramAnonymousRect.top) || (this.mInsetRight != paramAnonymousRect.right) || (this.mInsetBottom != paramAnonymousRect.bottom))
          {
            this.mInsetLeft = paramAnonymousRect.left;
            this.mInsetTop = paramAnonymousRect.top;
            this.mInsetRight = paramAnonymousRect.right;
            this.mInsetBottom = paramAnonymousRect.bottom;
            requestLayout();
          }
          return true;
        }
        
        public void onLayout(boolean paramAnonymousBoolean, int paramAnonymousInt1, int paramAnonymousInt2, int paramAnonymousInt3, int paramAnonymousInt4)
        {
          if (OMActivityLifecycleHandler.DEBUG) {
            Log.i(OMActivityLifecycleHandler.TAG, "OMActivityLifecycleHandle.ContainerView.onLayout(" + paramAnonymousBoolean + ", " + paramAnonymousInt1 + ", " + paramAnonymousInt2 + ", " + paramAnonymousInt3 + ", " + paramAnonymousInt4 + ")");
          }
          super.onLayout(paramAnonymousBoolean, paramAnonymousInt1, paramAnonymousInt2, paramAnonymousInt3, paramAnonymousInt4);
          Rect localRect = new Rect();
          OMActivityLifecycleHandler.sActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(localRect);
          if (OMActivityLifecycleHandler.DEBUG) {
            Log.i(OMActivityLifecycleHandler.TAG, "OMActivityLifecycleHandle.ContainerView.onLayout. Got windowVisibleDisplayFrame: " + localRect.left + ", " + localRect.top + ", " + localRect.right + ", " + localRect.bottom);
          }
          OMActivityLifecycleHandler.nativeOnLayout(this.mInsetLeft + paramAnonymousInt1, this.mInsetTop + paramAnonymousInt2, paramAnonymousInt3 - this.mInsetRight, localRect.bottom, paramAnonymousInt3 - paramAnonymousInt1, paramAnonymousInt4 - paramAnonymousInt2);
        }
      };
      if (i == 0) {
        break label285;
      }
      sContainerView.setSystemUiVisibility(3846);
      label111:
      sSurfaceView = new OMSurfaceView(paramActivity);
      sSurfaceView.setVisibility(4);
      sContainerView.addView(sSurfaceView, new FrameLayout.LayoutParams(-1, -1));
      if (sExploreByTouchHelperDelegate != null) {
        createAccessibilityOverlayView(sExploreByTouchHelperDelegate);
      }
      sCustomView = new OMCustomView(paramActivity);
      sContainerView.addView(sCustomView);
      paramActivity.setContentView(sContainerView);
      OMProcessServiceHelper.testOnNeed();
      if (Build.VERSION.SDK_INT < 17) {
        break label297;
      }
      sDisplayListener = new OMDisplayListener(null);
      sDisplayManager = (DisplayManager)paramActivity.getSystemService("display");
      sDisplayManager.registerDisplayListener(sDisplayListener, null);
    }
    for (;;)
    {
      if (i != 0) {
        OMVRShellServiceLifecycleHandler.onVRActivityStart(paramActivity);
      }
      sWindowHelper = new OMWindowHelper();
      sActivity.getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(sWindowHelper);
      return;
      i = 0;
      break;
      label270:
      OMApplication.loadApplication(paramActivity, paramActivity.getClass().getClassLoader(), paramIntent);
      break label81;
      label285:
      sContainerView.setSystemUiVisibility(1280);
      break label111;
      label297:
      sWindowManager = (WindowManager)paramActivity.getSystemService("window");
    }
  }

        - 调用OMApplication.nativeHandleOnCreate初始化。
        - 添加布局容器FrameLayout,包括一个OMSurfaceView和一个OMCustomView。
        - 然后还做了一些乱七八糟的就OMApplication.loadApplication启动runtime了
        - 而其他事件的实现,多半是在往native方法中传事件,所以无需细谈
        - 另外注意到OMActivity是反射调用的,而OMActivityLifecycleHandler直接持有Activity,说明java层其实OMActivityLifecycleHandler才是Activity的枢纽抽象

- OMSurfaceView没什么特别的,只是把SurfaceView用jni链接到native层

- OMCustomView内容稍复杂

        - 它是一个FrameLayout
        - 它里面有一个PseudoEditorView用于处理文本,并且把所有input事件传到native层
        - 这个看起来和微信小程序很像,即无论如何input还是一个原生控件进行处理

- 还有很多别的类,主要包括OMBluetoothHelper,OMCamera2Helper,OMChoreographerHelper,OMClipboardChangedListener,OMImagePicker,OMMediaScannerHelper,OMTelephonyHelper......这些东西看起来乱七八糟,无非都是给native提供功能,并且把接受到的事件或者callback传回native。

- 以上看起来可以获得结论:

        - 整个界面大部分情况只有一个activity和两个view,所有实际界面都是在这两个view上在native层实现的
        - 除了界面树,事件树也应该是通过native层实现的
        - java层主要代码是为native层提供各种android原生功能的事件和反馈
        - OMActivityLifecycleHandler其实起到java和native层的bridge作用,功能基本和ios的appdelegate对齐
        - 顺带注意到暴露到native的方法和事件很多都是ios命名风格和内容对齐的

native代码

分析native代码,想想可以猜到大部分业务逻辑代码铁定是用oc写的(否则跨平台的成本太高了),因此用老方法hopper就可以做一些简单的逆向了。可惜不太方便直接dump-class。

注意到有两个so,libosmeta.so-50.6 MB,libSocalAndroid.so-136.4 MB,很显然,他们一个是osmate的基础库和runtime,一个是实际app的sdk和业务逻辑部分。

我们先看libosmeta.so吧:

- c/cpp库:icu包,FreeType,bzip2,libpng之类的,当然还有各种c基础库。
- android-ndk。
- oc-runtime,虽说oc-runtime是开源的...。
- 有一套基本完整的iOS的sdk,从CoreFoundation到各种CoreText,AVFoundation,CoreMedia,QuartzCore,CoreImage,GLKit,AddressBookUI之类的,到UIKit,甚至还有HomeKit和HealthKit,简直是应有尽有。这个应该是这个框架最厉害的地方了。
- 有一套XF开头的代码和CF对齐,大概就是osmeta对CF的实际抽象和bridge了。整个程序也应该是依赖XF运行起来的。包括还有一套WS开头的,对齐了CA和界面绘制相关的东西,应该也是其中的一部分。

我们还可以简单的review一下代码中的onCreate即程序启动流程:

- 从Activity的onCreate调用到OMApplication.nativeHandleOnCreate调用到_handleActivityOnCreate函数中
- 进行窗口初始化,确认当前显示在windows中还是vr环境并且让view实际显示出来
- 通过单例oc类OMApplicationBridge初始化启动参数和生命周期到UIApplication中,最后调用startApplication的oc方法启动
- 然后就是UIApplication的实现了

可以看到:

- 整个调用链条已经打通,核心的关键节点是:OMActivity-OMActivityLifecycleHandler-OMApplicationBridge-UIApplication,这四个类是整个程序的唯一节点,由他们把安卓的各个事件传递到了实现的UIKit中
- osmeta自己实现了一套ios sdk,并把功能的实现bridge到ndk和安卓上

那么再来看看libSocalAndroid.so中的情况:

- 不出所料,里面全是oc的业务实现,类均以FB和SCL开头编写。当然,也有很多工具和功能是c/cpp写的,比如Facebook的MSQRD。
- 还出现了opencv,openal之类的高科技玩意儿,看起来是MSQRD使用的。
- 注意到reactivesocket,cpp部分应用了响应式编程。不过RAC倒是没有。
- cpp层统计部分用了glog,老冤家的统计库都敢用。

应用层并没有什么特点,看起来就是传统的ios应用。不过代码和注释里还是出现了android字眼,虽然只有几处。看起来并不是真正直接将ios应用进行的迁移,还是做了一点点代码上的兼容处理。

最后看看其他so都是些啥技术和框架:

- libbootstrap.so:CrossPortability的JNI引导入口和工具
- libc++_osmeta.so:osmeta一些cpp的基础工具类,看着主要是各种拓展类型的处理方法
- libcharset_osmeta.so:osmeta的本地化字符加载库
- libCrossPortability_osmeta.so:CrossPortability即代码中出现的XP开头的库,这一套方案显然也就是最早1314年的新闻里所说的跨平台方案了。看起来是用在跨平台的系统方法封装上
- libexif_osmeta.so:如题exif信息读取
- libFaultyLib.so:看着像是个动态库的加载器,函数前缀mozloader
- libFBAnalyticsCore_osmeta.so:facebook logger咯
- libFBAnalyticsCoreObjc_osmeta.so:对楼上的oc迁移,虽然主要是些category
- libffmpeg_osmeta.so:部分编译的ffmpeg,明显不全
- libiconv_osmeta.so:iconv常见的字符集转换工具
- libJavaScriptCore_osmeta.so:好像带js的东西有点多。这是jscore,就是苹果用那个webkit的一部分,这个
- libWebCore_osmeta.so:这就是苹果用那个webkit的另一部分,管渲染的那个
- libWebKit_osmeta.so:这个就是实际webkit的耦合,提供了WKWebview和相应的拓展
- libWebKitLegacy_osmeta.so:这个也能猜到了,提供了UIWebview和相应的拓展
- libOpenAL_osmeta.so:当然如题openAl
- libpdfium_osmeta.so:开源的pdf阅读库,这个
- libpgl_osmeta.so:chromium对opengl es的封装
- libsystem_malloc_osmeta.so:还是chromium中的/gperftools/malloc_extension.h
- libSystem_osmeta.so:感觉是GNU Mach中的工具,这个
- libunwind_osmeta.so:libunwind库的封装,这个
- libvrapi_osmeta.so:对项目中java层的vr功能的jni封装,其实是用的oculus的vr库
- libz_osmeta.so:对压缩解压库zlib的封装,这个

注意到:

- 这些lib中绝大部分都是为应用和runtime提供c/cpp级的纯软件模块基础能力,而没有使用android系统提供的基础能力
- 除了和ndk有关的东西,这些代码都是在跨平台的框架上,即CrossPortability上写成的,除了迁移到android,也能迁移到更多系统与平台

总结与短评

总的来看,osmeta到底做了什么事情呢:

- 在非iOS平台上,实现了一层套iOS框架以实现iOS应用程序代码跨平台编译/运行

莫非这帮人是果粉?我们能看到这一套跨平台架构的野心:

- 以iOS架构为基础,利用iOS开发封装完善并且能编译为二进制程序的优势,进行跨平台的迁移——而Android显然是这套框架的第一个落脚点。
- 同样,以native为runtime可以在绝大部份平台上低成本的进行迁移并高性能的实现write once run anywhere。


当然,缺陷也是很明显的:

- 这一整套runtime确实是太大了,大量的功能都通过native代码实现而没有使用系统原生功能。对于非大型应用显然得不偿失。
- 运行时级别的跨平台也就意味着会丢失掉大量的平台特性,特别是UI和交互层面的。
- 如果对以上两条进行优化,平台迁移成本也会随之提高,也会失去其相对于其他跨平台方案的优势。


说到这里,我们可以简单的和同样是facebook家的react-native这种跨平台方案做一下对比:

- 性能:显然osmeta更胜一筹,甚至在非ios平台上可能高于原生。
- 跨平台开发成本:不考虑js与oc本身的开发成本差异的话,osmeta的方案几乎没有把平台差异暴露到应用层面,显然成本更低。
- 包大小:无论如何对比,osmeta方案显然远大于rn,rn安卓就算携带了一个v8也不过10mb左右。
- 交互体验:由于去除了平台差异并且以iOS为基础为开发,osmeta方案所有平台的交互体验均与iOS一直;而rn的组件均为原生封装,保留了平台差异,因此会更贴近于各个平台原生体验。
- 开发体验:osmeta方案显然更亲近于native开发者,但是对于非iOS端的开发和调试方案是否完善还有待疑问。而rn的开发更亲近于web开发者,并且随着这两年的迭代,方案也非常完善了。

最后如何评价osmate这套跨平台方案呢?我只能说:欲知后事如何,拭目以待。

方案的产品化,特别是应用到Facebook Socal这么重量级的产品上,可见团队对这套方案给予了巨大的厚望,的这次试水必然能收集到大量的用户反馈和实际运行数据,继续做优化和改进。相信不久之后,osmate这一套跨平台方案又能给移动应用开发社区注入一股新的力量。

关键词:osmeta , facebook , 跨平台
logo