做⼀個⾼德地圖的 iOS / Android MAUI 控制元件(上)

微軟技術棧發表於2022-07-16

Microsoft Build 2022 ⼤會上正式釋出了 .NET MAUI , 對於 .NET 開發者可以⽤ C# 完成跨平臺的前端應⽤開發。對⽐起 MAUI 的前身 Xamarin , MAUI 除了可以⽤傳統的原⽣開發模式外,還⽀持了 Blazor 的混合式開發。這也讓更多⽅向的開發⼈員能進⼊到跨平臺的應⽤開發中來。有⼈會提出雲原⽣時代,前端開發還重要嗎 ?實際上,多端應⽤相容是雲原⽣不可缺少的⻔⾯。互聯⽹時代,有很多出⾊的應⽤,併發布了針對第三⽅應⽤的 SDK,開發者可以結合這些 SDK 做相關的解決⽅案。通過 MAUI 能調⽤這些 SDK 嗎?我會通過系列⽂章去和⼤家介紹。

為何要繫結原⽣ SDK

我們知道⼀個應⽤可以融⼊不同的場景,例如⼀個打⻋應⽤就需要地圖,例如⼀個拍照應⽤就需要社交,例如⼀個如果你是傳統的物聯⽹應⽤你需要⼀個藍⽛的通訊協議。拿來主義就是⼀個節省的⽅式,可以結合第三⽅提供的 SDK 來完成應⽤的開發。對於 .NET 開發⼈員會是⼀個難點,因為習慣性地去調⽤ DLL ,但在iOS / Android 原⽣開發上,實際上是有不同的庫調⽤機制。在 Xamarin 時代,就有不少開發者去⽤ C# 繫結第三⽅的庫,例如在中國市場就有⽀付寶,微信,⾼德地圖等。到了 MAUI 有什麼不⼀樣呢?在⼤致上是和Xamarin 繫結⽅式⼀樣。但由於 MAUI 融⼊到了 .NET 6,實際上就是⼀個項⽬⽂件格式的改變。現階段你可以通過命令⾏的⽅式快速構建 iOS / Android 的繫結項⽬。

▌MAUI iOS 庫的繫結

dotnet new iosbinding -o iOS.AMapSDK.Binding

要做 iOS / macOS的繫結你除了建立繫結項⽬外,你還需要安裝 Shapie ⼯具 (https://aka.ms/objectivesharpie)做對應轉換, 可以通過命令⾏去針對 iOS 的動態庫和靜態庫做對應轉換。這⾥補充⼀點你的 Xcode環境是必須要安裝的。下⾯是⼀個簡單的轉換語句,更多具體⼤家可以關注我的該系列的 iOS 庫⽂件繫結⽂章。

sharpie bind -framework /your path/AMapFoundationKit.framework -sdk
iphoneos15.5

▌MAUI Android 庫的繫結

dotnet new android-bindinglib -o Droid.AMapSDK.Binding

Android 的繫結和 iOS 不⼀樣,直接把第三⽅庫 Android SDK 的 jar 或者 aar 包放進去編譯即可。

如果你希望瞭解更多可以關注本系列 Android 庫繫結的系列⽂章。

控制元件定製

在 Xamarin.Forms 中,通過渲染器機制對跨平臺各⾃控制元件的引⽤,並且依賴於 INotifyPropertyChanged 。.NET MAUI 沒取消了渲染器機制,⽽是引⼊了⼀種稱為 Handler 的模式。有了 Handlers 更靈活 ,⽽且在需要時更容易擴充套件或覆蓋。

這是 MAUI 全新的 Handler 模式

我們通過 Handler 機制可以構建好⾼德地圖的 MAUI 控制元件

你可以通過 https://github.com/kinfey/AMa... 使⽤體驗 MAUI 的⾼德 Android / iOS 控制元件


介紹了⼀些做⾼德地圖的 iOS / Android MAUI 控制元件的主要知識之後,接下來將重點介紹 iOS 原⽣庫繫結的知識, 並告訴⼤家在繫結原⽣庫過程的⼀些技巧,希望給到⼩夥伴⼀些啟發。

認識 iOS 動態庫和靜態庫

在繫結之前,我們需要學習⼀下 iOS 的動態庫和靜態庫。最簡單理解的⽅式是在 iOS 中靜態庫是以 .a 字尾結尾,動態庫是以 .dylib 字尾結尾。⽆論靜態庫和動態庫都可以打包成 Framework 。

▌靜態庫和動態庫的區別

  1. 靜態庫的特點是編譯時會把庫⽂件直接拷⻉⼀份到⽬標應⽤程式,⽽這個拷⻉是駐留在⽬標應⽤程式⾥⾯的,所以編譯完成後,靜態庫的⽂件就沒有⽤了。但有個缺點就是,因為需要拷⻉,所以⽣成的應⽤程式的容量會較⼤。
  2. 動態庫和靜態庫剛好是相反,編譯的時候是不會拷⻉到⽬標應⽤程式⾥⾯的,所以⽣成應⽤程式的體積較⼩,⽽且⼀個動態庫可以共享給多個應⽤程式使⽤。但⽣成應⽤程式是依賴於動態庫,這也導致經常會出現動態庫找不到的情況。

我們來拆解⼀下⾼德地圖基礎的 SDK - AMapFoundationKit.framework

這⾥就包含了對應的頭⽂件資訊,模組資訊,以及靜態庫。你可以清晰看到⾼德地圖打包成 Framrwork 的實現。這也是我們對庫概念的認識,編譯好的⼆進位制程式碼,向外暴露頭⽂件給第三⽅開發者使⽤。

通過 Sharpie ⼯具⽣成 C# 調⽤的接⼝

Shapie 是⼀個⾮常好⽤的轉換⼯具,它⽀持在 macOS 下對 Objective-C 的庫的轉。通過 Sharpie 可以對庫⽂件給出的頭⽂件進⾏轉換完成 C# 的繫結。在 MAUI 前身 Shapie ⼯具就已經存在 , 我經常就利⽤這個⼯具做轉換。

因為這次⾼德地圖的功能我⽤到 3D ,所以我會對⾼德的 AMapFoundationKit.Framework 和MAMapKit.framework 兩個 Framework 進⾏繫結轉換。

▌轉換 AMapFoundationKit.Framework

sharpie bind -framework AMapFoundationKit.framework -sdk iphoneos15.5

▌轉換 MAMapKit.framework

sharpie bind -framework MAMapKit.framework -sdk iphoneos15.5

補充:MAMapKit.framework 依賴於 AMapFoundationKit.framework ,所以要放在⼀個相同的⽬錄下。
這⾥⾯要注意,你需要安裝好 Xcode ,建議安裝到最新 ,並對應最新的 iOS SDK , 當然你也可以根據需要繫結不同版本的 iOS SDK , 你可以通過⼀次是命令檢視環境

sharpie xcode -sdks

通過命令⾏繫結⽣成的是兩個⽂件是 StructsAndEnums.cs 和 ApiDefinitions.cs ,StructsAndEnums.cs 對應的是⼀些常量和列舉型別,ApiDefinitions.cs 對應的是⼀些接⼝和⽅法 。

建立 MAUI 的 iOS 繫結項⽬

這⾥建立需要注意,現在 Visual Studio 2022 的模版都沒有完成,現在⼤家⽤命令⾏建立,因為我們有兩個項⽬,需要建立兩個 Binding 的項⽬分別是針對於 AMapFoundationKit.Framework 的項⽬構建

dotnet new iosbinding -o iOS.AMap.Foundation

針對於 MAMapKit.framework 的項⽬構建
`
dotnet new iosbinding -o iOS.AMap.3D
`
⽣成好後,需要把 AMapFoundationKit.framework 放到 iOS.AMap.Foundation 的⽬錄下,MAMapKit.framework 放到 iOS.AMap.3D ⽬錄下。並把⽣成的 StructsAndEnums.cs 和 ApiDefinitions.cs 放到對應⽬錄。

項⽬設定調整

1. 在 Sharpie ⽣成的⽬錄下 StructsAndEnum.cs ,⽽在構建的 Binding ⽬錄下是 ApiDefinition.cs , 要把它替換掉。所以要對 .csproj 項⽬進⾏修改

<ItemGroup>
 <ObjcBindingApiDefinition Include="ApiDefinitions.cs" />
 <ObjcBindingCoreSource Include="StructsAndEnums.cs" />
</ItemGroup>

2. 對 iOS.AMap.Foundation 進⾏編譯

▌在 AMapFoundationKit.framework.csproj 增加對 Framework 的引⽤

<ItemGroup>
 <NativeReference Include="AMapFoundationKit.framework">
 <Kind>Framework</Kind>
 <ForceLoad>True</ForceLoad>
 <SmartLink>False</SmartLink>
 </NativeReference>
 </ItemGroup>

Kind :原⽣繫結型別可以是 Framwork 也可以是 StaticLibary

ForceLoad :強載入,選擇 True

SmartLink :智慧連結

完成的項⽬.csproj 設定為

<Project Sdk="Microsoft.NET.Sdk">
 <PropertyGroup>
 <TargetFramework>net6.0-ios</TargetFramework>
 <Nullable>enable</Nullable>
 <ImplicitUsings>true</ImplicitUsings>
 <IsBindingProject>true</IsBindingProject>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoBindingEmbedding>false</NoBindingEmbedding>
 </PropertyGroup>
 <ItemGroup>
 <ObjcBindingApiDefinition Include="ApiDefinitions.cs" />
 <ObjcBindingCoreSource Include="StructsAndEnums.cs" />
 </ItemGroup>
 <ItemGroup>
 <NativeReference Include="AMapFoundationKit.framework">
 <Kind>Framework</Kind>
 <ForceLoad>True</ForceLoad>
 <SmartLink>False</SmartLink>
 </NativeReference>
 </ItemGroup>
</Project>

編譯 iOS.AMap.Foundation , 你會覺得奔潰,因為⾮常多的出錯資訊。這是因為 Shapie 做轉換時,⼀些轉換沒做好導致的,這個時候你就需要⼀個⼀個進⾏調整

▌歸類⼀下出錯資訊

  • The type or namespace name 'VerifyAttribute' could not be found

這類資訊時因為轉換時候沒有確認好屬性,所以會增加 VerifyAttribute 欄位,這個⼀般情況下把這個欄位註釋掉就可以了,如

static class CFunctions
{
// NSString * AMapEmptyStringIfNil (NSString *s);
[DllImport ("__Internal")]
// [Verify (PlatformInvoke)]
static extern NSString AMapEmptyStringIfNil (NSString s);
// extern CLLocationCoordinate2D AMapCoordinateConvert
(CLLocationCoordinate2D coordinate, AMapCoordinateType type);
[DllImport ("__Internal")]
// [Verify (PlatformInvoke)]
static extern CLLocationCoordinate2D AMapCoordinateConvert
(CLLocationCoordinate2D coordinate, AMapCoordinateType type);
// extern BOOL AMapDataAvailableForCoordinate (CLLocationCoordinate2D
coordinate);
[DllImport ("__Internal")]
// [Verify (PlatformInvoke)]
static extern bool AMapDataAvailableForCoordinate
(CLLocationCoordinate2D coordinate);
}
The type or namespace name 'AMapFoundationKit'

名稱空間問題,這個你需要為 StructsAndEnums.cs 和 ApiDefinitions.cs 增加命名控制元件就可以了,你可以直接⽤ AMapFoundationKit ,也可以⾃⼰修改喜歡的名字 ,我這⾥⽤ iOS.AMap.Foundation 名字和項⽬對應

  • Duplicate 'Static' attribute

這個是因為 ApiDefinitions.cs 的 Constants 重複定義了,這個就需要重新整理歸併為⼀個就可以了

  • Unsupported type for Fields: bool for 'iOS.AMap.Foundation.Constants _amapLocationOverseas'.e

型別不對應導致編譯不通過,這個時候我修改為

[Field ("_amapLocationOverseas", "__Internal")]
IntPtr _amapLocationOverseas { get; }

這樣你就可以編譯通過 iOS.AMap.Foundation

3. 對 iOS.AMap.3D 進⾏編譯

▌新增對 iOS.AMap.Foundation的引⽤

因為 MAMapKit.framework 依賴於 AMapFoundationKit.framework , 所以 iOS.AMap.3D 是依賴於iOS.AMap.Foundation

<ItemGroup>
 <ProjectReference
Include="..\iOS.Amap.Foundation\iOS.Amap.Foundation.csproj" />
 </ItemGroup>

▌引⼊ MAMapKit.framework

<ItemGroup>
 <NativeReference Include="MAMapKit.framework">
 <Kind>Framework</Kind>
 <ForceLoad>True</ForceLoad>
 <SmartLink>True</SmartLink>
 <Frameworks>GLKit OpenGLES UIKit Foundation CoreGraphics QuartzCore
CoreLocation CoreTelephony SystemConfiguration Security AdSupport
JavaScriptCore</Frameworks>
 <LinkerFlags>-lz -lstdc++ -lc++</LinkerFlags>
 </NativeReference>
 </ItemGroup>

這個和 AMapFoundationKit.framework 不⼀樣的, 需要新增 Framework 編譯時需要依賴的項, 以及⽤到的編譯⽅式 ,這個和你繫結的 framework 有關, 我這⾥選擇⾼德地圖,所以按照它們的⽂檔要求做了相關設定。
完成的項⽬.csproj 設定為

<Project Sdk="Microsoft.NET.Sdk">
 <PropertyGroup>
 <TargetFramework>net6.0-ios</TargetFramework>
 <RootNamespace>iOS.Amap._3D</RootNamespace>
 <Nullable>enable</Nullable>
 <ImplicitUsings>true</ImplicitUsings>
 <IsBindingProject>true</IsBindingProject>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoBindingEmbedding>false</NoBindingEmbedding>
 </PropertyGroup>
 <ItemGroup>
 <ObjcBindingApiDefinition Include="ApiDefinitions.cs" />
 <ObjcBindingCoreSource Include="StructsAndEnums.cs" />
 </ItemGroup>
 <ItemGroup>
 <NativeReference Include="MAMapKit.framework">
 <Kind>Framework</Kind>
 <ForceLoad>True</ForceLoad>
 <SmartLink>True</SmartLink>
 <Frameworks>GLKit OpenGLES UIKit Foundation CoreGraphics QuartzCore
CoreLocation CoreTelephony SystemConfiguration Security AdSupport
JavaScriptCore</Frameworks>
 <LinkerFlags>-lz -lstdc++ -lc++</LinkerFlags>
 </NativeReference>
 </ItemGroup>
 <ItemGroup>
 <ProjectReference
Include="..\iOS.Amap.Foundation\iOS.Amap.Foundation.csproj" />
 </ItemGroup>
</Project>

編譯 iOS.AMap.3D,你會⽐之前更奔潰,這個時候你需要有⾜夠的耐⼼, 除了和之前差不多的出錯資訊外,還有⼀些新的狀況,我這⾥列舉⼀下

  • Type 'MAMapViewDelegate' already defines a member called 'MapView' with the same parameter types

造成這個原因是因為⽅法重名了,這也是 Objective-C 宣告式語法和傳統語法不⼀樣的地⽅,所以你要針對這個做重新命名

如這個

// @optional -(void)mapView:(MAMapView *)mapView didAnnotationViewTapped:
(MAAnnotationView *)view;
[Export ("mapView:didAnnotationViewTapped:")]
void MapView (MAMapView mapView, MAAnnotationView view);

修改為

// @optional -(void)mapView:(MAMapView *)mapView didAnnotationViewTapped:
(MAAnnotationView *)view;
[Export ("mapView:didAnnotationViewTapped:")]
void MapViewDidAnnotationViewTapped (MAMapView mapView, MAAnnotationView
view);
  • The type or namespace name 'IMAOverlay' could not be found
    這個是命名出錯,在 ApiDefinitions.cs ⽂件中你可以找到 MAOverlay

    [Protocol]
    interface MAOverlay : IMAAnnotation
    {
    // @required -(CLLocationCoordinate2D)coordinate;
    [Abstract]
    [Export ("coordinate")]
    // [Verify (MethodToProperty)]
    CLLocationCoordinate2D Coordinate { get; }
    // @required -(MAMapRect)boundingMapRect;
    [Abstract]
    [Export ("boundingMapRect")]
    // [Verify (MethodToProperty)]
    MAMapRect BoundingMapRect { get; }
    }

    所以把所有 IMAOverlay 替換為 MAOverlay 即可。

  • The type or namespace name 'AutoGeneratedName' could not be found

把 AutoGeneratedName 取消

  • Constant value '-1' cannot be converted to a 'ulong'

指定型別錯誤 AllCorners = ~0x0 改為 AllCorners = 0x0
Do not know how to make a signature for CoreLocation.CLLocationCoordinate2D in parameter`coordinates'
C# 是沒有指標的,在 Sharpie 轉換時出錯了

  • 'MAMapView_UserLocation.HeadingFilter': cannot declare instance members in a static class
// @property (nonatomic) CLLocationDegrees headingFilter;
[Export ("headingFilter")]
double HeadingFilter( { get; set; })

這個定義要換成

// @property (nonatomic) CLLocationDegrees headingFilter;
[Export ("headingFilter")]
double HeadingFilter();
  • Cannot convert type 'Foundation.NSObject' to 'nint'
  • // @property (nonatomic, weak) id<MAOverlayRenderDelegate>
  • rendererDelegate;
  • [NullAllowed, Export ("rendererDelegate", ArgumentSemantic.Weak)]
  • NSObject WeakRendererDelegate { get; set; }
    修改為
// @property (nonatomic, weak) id<MAOverlayRenderDelegate>
rendererDelegate;
[NullAllowed, Export ("rendererDelegate", ArgumentSemantic.Weak)]
IntPtr WeakRendererDelegate { get; set; }

或者排除是⼀個漫⻓的過程,但編譯成功⼀刻你會⾮常興奮,這樣我們就把 AMapFoundationKit.framework和 MAMapKit.framework 繫結成功了。

嘗試建立⼀個 .NET for iOS 項⽬驗證⼀下

小結

原⽣庫繫結雖然⽐較多繁瑣的事情,但是實際上也是⼗分治癒的,當你看到編譯通過的那⼀刻,你就會明⽩箇中的快樂。還有⼀點,很多⼈認為跨平臺移動開發不需要平臺的基礎知識了,實際還是需要。特別在這種原⽣庫的繫結上,就需要你既會 C# ⼜會 Objective-C 。希望該例⼦能給各位有所啟發。請⼤家期待下⼀篇 Android 原生庫繫結。

相關資源


長按識別二維碼
關注微軟中國MSDN

點選瞭解MAUI

相關文章