笔记 | Xamarin

引言 文件读写 参考: 使用 Xamarin.Android 对外部存储进行的文件访问 - Xamarin | Microsoft Docs Xamarin 中的文件系统访问 - Xamarin | Microsoft Docs 关于xamarin.forms Android创建文件与写文件 (ftp) - 懒猫口米 - 博客园

外部读写

应用可以在外部存储上保留两种不同类型的文件: 专用 文件 – 专用文件是特定于应用程序的文件(但仍然全局可读且全局可写)。 Android 期望专用文件存储在外部存储上的特定目录中。 尽管这些文件称为“专用”,但它们仍然可见,并且可由设备上的其他应用访问,Android 并没有对它们提供任何特殊保护。
1
2
3
// 专用外部存储目录
// /storage/emulated/0/Android/data/com.companyname.app/files/
Android.Content.Context.GetExternalFilesDir(string type)
公共 文件 – 这些文件不被视为特定于应用程序,可自由共享。
1
2
3
// 主外部存储目录
// /storage/emulated/0/
Android.OS.Environment.ExternalStorageDirectory
Android 将外部存储视为危险权限,这通常要求用户授予其访问资源的权限。 用户可以随时撤销此权限。 这意味着在进行任何文件访问之前都应执行运行时权限请求。 应用会被自动授予读取和写入其自己的专用文件的权限。 在用户授予了权限之后,应用可以读取和写入属于其他应用的专用文件。
1
2
3
4
5
6
//global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath :得到安卓的根目录
//Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)::得到安卓data目录
var path = global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

// 创建文件
System.IO.Directory.CreateDirectory(path);

外部存储权限

所有 Android 应用都必须在 AndroidManifest.xml 中为外部存储声明两个权限之一。 若要标识权限,必须将以下两个 uses-permission 元素之一添加到 AndroidManifest.xml:
AndroidManifest.xml
1
2
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
注意:下面有误 如上,在安卓项目里有个Properties的文件下有个AndroidManifest.xml的文件。在
1
<application android:label="cardionNet2.Android"></application> 
下加 这个目测有误,直接加进去就是了,不需要放在这个;里
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.demoapp" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="DemoApp.Android" android:theme="@style/MainTheme"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>
写入外部存储之前的第一步是检查它是可读或可写。 Android.OS.Environment.ExternalStorageState 属性保存标识外部存储状态的字符串。 此属性会返回表示状态的字符串。
1
2
bool isReadonly = Environment.MediaMountedReadOnly.Equals(Environment.ExternalStorageState);
bool isWriteable = Environment.MediaMounted.Equals(Environment.ExternalStorageState);
完全限定
1
2
bool isReadonly = Android.OS.Environment.MediaMountedReadOnly.Equals(Android.OS.Environment.ExternalStorageState);
bool isWriteable = Android.OS.Environment.MediaMounted.Equals(Android.OS.Environment.ExternalStorageState);
应用生命周期 参考: Xamarin.Forms 应用生命周期 - Xamarin | Microsoft Docs Application 基类提供下列功能: 生命周期方法 OnStart、OnSleep 和 OnResume。 页导航事件 PageAppearingPageDisappearing模式导航事件 ModalPushing、ModalPushed、ModalPopping 和 ModalPopped。

生命周期方法

Application 类包含三个虚拟方法,可以替代以响应生命周期更改: OnStart - 在启动应用程序时调用它。 OnSleep - 每当应用程序转入后台时调用它。 OnResume - 应用程序发送到后台后恢复时调用。 布局 参考: 搞懂Xamarin.Forms布局,看这篇应该就够了吧 - 何旭 - 博客园 JoesWeek/Cnblogs: 博客园第三方Android客户端,Xamarin App,Material Design风格 jsuarezruiz/awesome-xamarin-forms: A curated list of awesome Xamarin.Forms libraries and resources Xamarin.Forms UI Snippets JarBinding Bugly Xamarin.Forms Shell 参考: 创建 Xamarin.Forms Shell 应用程序 - Xamarin | Microsoft Docs Xamarin.Forms 第28局:Shell - 简书 Xamarin.FormsShell基础教程(9)Shell相关类体系 - 大学霸 - 博客园

视觉层次结构

Shell -> FlyoutItem / TabBar -> Tab -> ShellContent -> ContentPage FloutItem: 浮出控件 TabBar: 底部选项卡栏 Tab: 分组内容 当 Tab 中存在多个 ShellContent,时,会在内部再次分布, 若 Tab 父级是 TabBar,则会在那个页面显示 顶部导航选项卡,以对应多个 ShellContent, 若 Tab 父级是 FlyoutItem,则会在对应条下显示多个子条 (ShellContent) 若在 FloutItem / TabBar 中直接写 ShellContent,则会将每个ShellContent 隐式包裹在一个 Tab 中 补充: 和 TabBar 类是 ShellItem 类的别名,而 Tab 类是 ShellSection 类的别名。 因此,也可以 Shell -> FlyoutItem / ShellItem -> ShellSection -> ShellContent -> ContentPage 因此,在为 FlyoutItem 对象创建自定义呈现器时应重写 CreateShellItemRenderer 方法,在为 Tab 对象创建自定义呈现器时应重写 CreateShellSectionRenderer 方法。
1
2
3
<!-- When the Flyout is visible this will be a menu item you can tie a click behavior to  -->
<MenuItem Text="Logout" StyleClass="MenuItemLayoutStyle" Clicked="OnMenuItemClicked">
</MenuItem>
侧边浮出注销按钮 当 侧边 (Flyout) 浮出显示 时,MenItem 就会显示 MenuItem: 浮出控件的菜单项

浮出控件

参考: Xamarin.Forms Shell 浮出控件 - Xamarin | Microsoft Docs 可以通过图标或从屏幕的一侧轻扫来访问它。 浮出控件由可选标头、浮出控件项、可选菜单项和可选页脚组成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 
When the Flyout is visible this defines the content to display in the flyout.
FlyoutDisplayOptions="AsMultipleItems" will create a separate flyout item for each child element
https://docs.microsoft.com/dotnet/api/xamarin.forms.shellgroupitem.flyoutdisplayoptions?view=xamarin-forms
-->
<FlyoutItem Title="首页" Icon="icon_about.png">
<ShellContent Route="HomePage" ContentTemplate="{DataTemplate local:HomePage}" />
</FlyoutItem>
<FlyoutItem Title="列表" Icon="icon_feed.png">
<ShellContent Route="ItemsPage" ContentTemplate="{DataTemplate local:ItemsPage}" />
</FlyoutItem>
<FlyoutItem Title="设置" Icon="icon_setting.png">
<ShellContent Route="SettingPage" ContentTemplate="{DataTemplate local:SettingPage}" />
</FlyoutItem>
隐式转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Xaminals.Controls"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<FlyoutItem Title="Cats"
Icon="cat.png">
<Tab>
<ShellContent ContentTemplate="{DataTemplate views:CatsPage}" />
</Tab>
</FlyoutItem>
<FlyoutItem Title="Dogs"
Icon="dog.png">
<Tab>
<ShellContent ContentTemplate="{DataTemplate views:DogsPage}" />
</Tab>
</FlyoutItem>
</Shell>
等同于下方:
1
2
3
4
5
6
7
8
9
10
11
12
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Xaminals.Controls"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<ShellContent Title="Cats"
Icon="cat.png"
ContentTemplate="{DataTemplate views:CatsPage}" />
<ShellContent Title="Dogs"
Icon="dog.png"
ContentTemplate="{DataTemplate views:DogsPage}" />
</Shell>
此隐式转换自动将每个 ShellContent 对象包装在 Tab 对象中,而 Tab 则包装在 FlyoutItem 对象中。 即 Shell 中默认 FlyoutItem, FlyoutItem / TabBar 中默认 Tab 备注 子类化的 Shell 对象中的所有 FlyoutItem 对象都会自动添加到 Shell.FlyoutItems 集合, 该集合定义将在浮出控件中显示的项的列表。

定义 FlyoutItem 外观

通过将 Shell.ItemTemplate 附加属性设置为 DataTemplate 可自定义每个 FlyoutItem 的外观:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Shell ...>
...
<Shell.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="0.2*,0.8*">
<Image Source="{Binding FlyoutIcon}"
Margin="5"
HeightRequest="45" />
<Label Grid.Column="1"
Text="{Binding Title}"
FontAttributes="Italic"
VerticalTextAlignment="Center" />
</Grid>
</DataTemplate>
</Shell.ItemTemplate>
</Shell>
FontAttributes="Italic" 此示例以斜体显示每个 FlyoutItem 对象的标题: Shell.ItemTemplate 是一个附加属性,因此可将不同的模板附加到特定的 FlyoutItem 对象。

替换浮出控件内容

浮出项表示浮出控件内容,可以选择将其替换为你自己的内容,方法是将 Shell.FlyoutContent 可绑定属性设置为 object:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Shell ...
x:Name="shell">
...
<Shell.FlyoutContent>
<CollectionView BindingContext="{x:Reference shell}"
IsGrouped="True"
ItemsSource="{Binding FlyoutItems}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Title}"
TextColor="White"
FontSize="Large" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Shell.FlyoutContent>
</Shell>
在此示例中,将浮出控件内容替换为 CollectionView,它显示了 FlyoutItems 集合中每个项的标题。 此外,可以通过将 Shell.FlyoutContentTemplate 可绑定属性设置为 DataTemplate 来定义浮出控件内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Shell ...
x:Name="shell">
...
<Shell.FlyoutContentTemplate>
<DataTemplate>
<CollectionView BindingContext="{x:Reference shell}"
IsGrouped="True"
ItemsSource="{Binding FlyoutItems}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Title}"
TextColor="White"
FontSize="Large" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</DataTemplate>
</Shell.FlyoutContentTemplate>
</Shell>

FlyoutItem 选择

场景: 有时候,默认并不需要显示第一个 首次运行使用浮出控件的 Shell 应用程序时,Shell.CurrentItem 属性将设置为子类化的 Shell 对象中的第一个 FlyoutItem 对象。 但是,此属性可以设置为另一个 FlyoutItem,如以下示例所示:
1
2
3
4
5
6
7
8
9
10
<Shell ...
CurrentItem="{x:Reference aboutItem}">
<FlyoutItem FlyoutDisplayOptions="AsMultipleItems">
...
</FlyoutItem>
<ShellContent x:Name="aboutItem"
Title="About"
Icon="info.png"
ContentTemplate="{DataTemplate views:AboutPage}" />
</Shell>
此示例将 CurrentItem 属性设置为名为 aboutItem 的 ShellContent 对象,这将导致选中并显示该对象。 在此示例中,隐式转换用于将 ShellContent 对象包装在 Tab 对象中,后者包装在 FlyoutItem 对象中。 假设有一个名为 aboutItem 的 ShellContent 对象,则等效的 C# 代码为:
1
CurrentItem = aboutItem;
在此示例中,CurrentItem 属性是在子类化的 Shell 类中设置的。 或者,可通过 Shell.Current 静态属性在任何类中设置 CurrentItem 属性:
1
Shell.Current.CurrentItem = aboutItem;

FlyoutItem 可见性

浮出项在浮出控件中默认可见。 但是,可以使用 FlyoutItemIsVisible 属性将项隐藏在浮出控件中,并使用 IsVisible 属性将其从浮出控件中删除: 类型为 bool 的 FlyoutItemIsVisible 指示项是否已隐藏在浮出控件中但仍可以通过 GoToAsync 导航方法进行访问。 此属性的默认值为 true。 类型为 bool 的 IsVisible 指示是否应从可视化树中移除项,从而不在浮出控件中显示。 它的默认值为 true。 备注 还有一个 Shell.FlyoutItemIsVisible 附加属性,可在 FlyoutItemMenuItemTabShellContent 对象上设置该属性。

以编程方式打开和关闭浮出控件

1
2
3
<Shell ...
FlyoutIsPresented="{Binding IsFlyoutOpen}">
</Shell>
1
Shell.Current.FlyoutIsPresented = false;

底部导航栏

参考: Xamarin.Forms Shell 选项卡 - Xamarin | Microsoft Docs
1
2
3
4
<TabBar>
<ShellContent Title="About" Icon="icon_about.png" Route="AboutPage" ContentTemplate="{DataTemplate local:AboutPage}" />
<ShellContent Title="Browse" Icon="icon_feed.png" ContentTemplate="{DataTemplate local:ItemsPage}" />
</TabBar>
目测,不加 Title, Icon 就会隐藏起来,那么这个时候就只能通过代码导航到这里了。
1
2
3
4
5
6
7
<!--
If you would like to navigate to this content you can do so by calling
await Shell.Current.GoToAsync("//LoginPage");
-->
<TabBar>
<ShellContent Route="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}" />
</TabBar>

单页

TabBar 中只有 一个 ShellContent,就不会显示底部选项卡导航栏
1
2
3
4
5
6
7
8
9
10
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<TabBar>
<Tab>
<ShellContent ContentTemplate="{DataTemplate views:CatsPage}" />
</Tab>
</TabBar>
</Shell>

底部选项卡

倘若单个 TabBar 对象中有多个 Tab 对象,则 Tab 对象呈现为底部选项卡: 类型为 string 的 Title 属性,可定义选项卡标题。 类型为 ImageSourceIcon 属性,可定义选项卡图标: 如果 TabBar 上有五个以上的选项卡,则显示“更多”选项卡,可用于访问其他选项卡:

底部和顶部选项卡

如果一个 Tab 对象中存在多个 ShellContent 对象时,则将在底部选项卡中添加一个顶部选项卡栏,通过该选项卡栏可以导航 ContentPage 对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<TabBar>
<Tab Title="Domestic"
Icon="paw.png">
<ShellContent Title="Cats"
ContentTemplate="{DataTemplate views:CatsPage}" />
<ShellContent Title="Dogs"
ContentTemplate="{DataTemplate views:DogsPage}" />
</Tab>
<Tab Title="Monkeys"
Icon="monkey.png">
<ShellContent ContentTemplate="{DataTemplate views:MonkeysPage}" />
</Tab>
</TabBar>
</Shell>

选项卡选择

首次运行使用选项卡栏的 Shell 应用程序时,Shell.CurrentItem 属性将设置为子类化的 Shell 对象中的第一个 Tab 对象。 但是,此属性可以设置为另一个 Tab,如以下示例所示:
1
2
3
4
5
6
7
8
9
10
11
12
<Shell ...
CurrentItem="{x:Reference dogsItem}">
<TabBar>
<ShellContent Title="Cats"
Icon="cat.png"
ContentTemplate="{DataTemplate views:CatsPage}" />
<ShellContent x:Name="dogsItem"
Title="Dogs"
Icon="dog.png"
ContentTemplate="{DataTemplate views:DogsPage}" />
</TabBar>
</Shell>

补充

同时显示 浮出、底部导航栏

参考: Xamarin Form Shell:弹出型v/s TabBar - 我爱学习网 app shell - 在 Xamarin.Forms 中同时使用 TabBar 和 Flyout - 爱编程的大狗 没办法直接在Shell中,同时显式定义 FlyoutItem 和 TabBar 只能通过 FlyoutItem 隐式达到效果 注意: 并没有在 FlyoutItem 上使用 FlyoutDisplayOptions="AsMultipleItems", 这会导致 首页、游戏、频道、动态也显示在侧边浮出栏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- 显示在底部导航栏 -->
<FlyoutItem Title="首页" Icon="icon_about.png">
<Tab Title="首页" Icon="icon_about.png">
<ShellContent x:Name="HotPageItem"
Title="热门"
ContentTemplate="{DataTemplate local:HotPage}" />
<ShellContent x:Name="RecomPageItem"
Title="推荐"
ContentTemplate="{DataTemplate local:RecomPage}" />
<ShellContent x:Name="LastPageItem"
Title="最新"
ContentTemplate="{DataTemplate local:LastPage}" />
</Tab>
<Tab Title="游戏" Icon="icon_feed.png">
<ShellContent ContentTemplate="{DataTemplate local:ItemsPage}" />
</Tab>
<Tab Title="频道" Icon="icon_feed.png">
<ShellContent ContentTemplate="{DataTemplate local:ItemsPage}" />
</Tab>
<Tab Title="动态" Icon="icon_feed.png">
<ShellContent ContentTemplate="{DataTemplate local:ItemsPage}" />
</Tab>
</FlyoutItem>
<!-- 显示在侧边浮出栏 -->
<FlyoutItem Title="关于" Icon="icon_about.png">
<ShellContent ContentTemplate="{DataTemplate local:HomePage}" />
</FlyoutItem>
<FlyoutItem Title="设置" Icon="icon_setting.png">
<ShellContent ContentTemplate="{DataTemplate local:SettingPage}" />
</FlyoutItem>
补充 让首页默认选中 第二个 推荐,在 首页 项使用 CurrentItem
1
2
3
4
5
6
7
8
9
10
11
<Tab Title="首页" Icon="icon_about.png" CurrentItem="{x:Reference RecomPageItem}">
<ShellContent x:Name="HotPageItem"
Title="热门"
ContentTemplate="{DataTemplate local:HotPage}" />
<ShellContent x:Name="RecomPageItem"
Title="推荐"
ContentTemplate="{DataTemplate local:RecomPage}" />
<ShellContent x:Name="LastPageItem"
Title="最新"
ContentTemplate="{DataTemplate local:LastPage}" />
</Tab>

Shell 添加手势、滑动

参考: c# - 在 Xamarin Shell 中的 tabbar 页面之间滑动_c++_帮酷编程问答 [Feature] Swipe left/right to navigate between upper/bottom tabs of Shell · Issue #12435 · xamarin/Xamarin.Forms 官方没有实现 底部选项导航栏(包括子项顶部导航栏) 滑动动画切换页面 见 [Feature] Swipe left/right to navigate between upper/bottom tabs of Shell · Issue #12435 · xamarin/Xamarin.Forms Xamarin.Forms 滑动、手势 参考: softlion/XamarinFormsGesture: Xamarin Form Gesture Effects Xamarin.Forms ScrollView - Xamarin | Microsoft Docs Xamarin.Forms CarouselView 滚动 - Xamarin | Microsoft Docs 滚动视图 ScrollView 在Xamarin.Forms中,滚动视图ScrollView用来实现长内容的滚动显示。虽然ScrollView的Content属性只能设置一个值,即ScrollView只能包含一个子元素,但它实际是一个布局控件,一个特殊的布局元素。 在使用的时候,ScrollView要求父容器给它分配固定的大小,同时子元素并且有固定的大小。这样,ScrollView才能根据各自大小计算滚动量。ScrollView不仅提供了当前滚动量ScrollX和ScrollY,还提供内容总量ContentSize。这样,开发者就可以计算滚动进度,显示给用户。同时,利用ScrollView提供的滚动结束事件Scrolled,可以提示用户,或者加载新的内容。 Q&A 补充

解析 Markdown

参考: NumerousTechnology/MdView: The flexible Markdown control for Xamarin.Forms. dotnet-ad/MarkdownView: Native markdown rendering on top of Xamarin.Forms & Markdig. SuavePirate/MarkdownTextView: A Xamarin.Forms component to display markdown text in a TextView

VPN

参考: c# - Vpn client in xamarin - Stack Overflow vpnhood/VpnHood: Undetectable Fast Portable VPN

内网穿透

参考: FastTunnel/FastTunnel: 开源免费跨平台的内网穿透工具 远程内网计算机 域名访问内网站点 反向代理内网服务 花生壳 端口转发 http代理 微信 小程序 like ngrok and frp. NAT ssh proxy tunnel reverse-proxy

自动升级

参考: C# Xamarin For Android自动升级项目实战 - 跟着阿笨一起玩.NET - 博客园 xamarin.forms 版本自动更新(针对android) - sxsean - 博客园 XamarinAndroid获取当前版本号-Android-CSDN问答 C#使用Xamarin开发可移植移动应用终章(11.获取设备信息与常用组件,开源一个可开发模版.) - GuZhenYin - 博客园 Android开发之自带下载器DownloadManager的使用示例代码_Android_脚本之家 android 8.0以上无法唤起apk安装界面问题_vulgar_rabbit的博客-CSDN博客 Android7.0、8.0安装apk以及安装apk弹出“选择打开方式”的解决方案_Lone_Star斌 的博客-CSDN博客 系统自带DownloadManager详解_Demi的博客-CSDN博客 WVector/AppUpdate: 🚀 Android 版本更新 🚀 a library for android version update 🚀 azhon/AppUpdate: 一个简单、轻量、可随意定制的Android版本更新库;A simple lightweight and customizable Android version update library 安卓app自动更新功能完美实现_白云天的博客-CSDN博客_android 自动更新 关于Android7.0 Intent调起安装页面,自动安装apk_我靠_叫我大当家的的博客-CSDN博客 android代码调用安装apk(兼容7.0)_魑魅魍魉9527-CSDN博客_android 调用安装 android file provider 完全解析 - 天使漫步IT工作室 angelinn/Xam.Plugin.AutoUpdate: Xamarin Forms plugin that auto updates your Android or UWP sideloaded application. edsnider/latestversionplugin: LatestVersion Plugin for Xamarin and Windows apps teprinciple/UpdateAppUtils: 一行代码快速实现app版本更新 Xamarin.Android获取当前版本号 Android
1
2
3
4
5
6
7
public string GetVersion()
{
// https://stackoverflow.com/questions/47353986/xamarin-forms-forms-context-is-obsolete
var context = Android.App.Application.Context;

return context.PackageManager.GetPackageInfo(context.PackageName, 0).VersionName;
}
下方错误,Activity 继承 Context,这样 拿到的 activity=null
1
2
3
4
5
public string GetVersion()
{
var activity=Xamarin.Forms.Forms.Context as Activity;
return activity.PackageManager.GetPackageInfo(activity.PackageName, 0).VersionName;
}
iOS
1
2
3
4
public string GetVersion()
{
return NSBundle.MainBundle.InfoDictionary["CFBundleShortVersionString"].ToString();
}
安装本地apk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void InstallAPK(string filePath)
{
try
{
Java.IO.File apkFile = new Java.IO.File(filePath);

Intent intent = new Intent(Intent.ActionView);
intent.SetFlags(ActivityFlags.NewTask);

// 注意: 直接从文件中 安装apk 和 从下载管理器中安装 不一样
// 获取下载文件的Uri
if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.N)
{
// Android 7.0+
Android.Net.Uri apkFileUri = Android.Support.V4.Content.FileProvider.GetUriForFile(_mContext, "github.yiyungent.onetree.fileprovider", apkFile);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
intent.SetDataAndType(apkFileUri, "application/vnd.android.package-archive");
}
else
{
Android.Net.Uri apkFileUri = Android.Net.Uri.FromFile(apkFile);
intent.SetDataAndType(apkFileUri, "application/vnd.android.package-archive");
}
_mContext.StartActivity(intent);
}
catch (Exception ex)
{

}
}

> AndroidManifest.xml
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="0.4.2" package="github.yiyungent.onetree" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="OneTree" android:theme="@style/MainTheme">
<provider android:name="android.support.v4.content.FileProvider" android:authorities="github.yiyungent.onetree.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider>
</application>
<!-- 访问网络状态权限 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 读写外部存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 访问网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<!-- 允许安装未知来源安装包 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>
Resources/xml/file_paths.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--Context.getFilesDir() 位于/data/data/安装目录-->
<files-path name="internalPath" path="file" />
<!--Context.getCacheDir()-->
<cache-path name="cachePath" path="file" />
<!--Environment.getExternalStorageDirectory()-->
<external-path name="externalPath" path="file" />
<!--Context.getExternalFilesDir(null)-->
<external-files-path name="externalFPath" path="file" />
<root-path
name="root-path"
path="." />
</paths>
参考: https://stackoverflow.com/questions/54421242/how-to-make-xamarin-forms-app-auto-updated https://stackoverflow.com/questions/58409164/xamarin-display-notification-in-notification-bar-once-the-file-is-downloaded android8.0以上权限变更,若apk内下载安装包后安装,首先需要确认是否有安装未知来源应用程序的权限。 首先,需要在清单文件内加入以下权限:
1
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
安卓8以上,代码Intent调用打开apk,未唤起安装界面 解决:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//下载到本地后执行安装
private void InstallAPK()
{
// 获取下载文件的Uri
Android.Net.Uri downloadFileUri = _downloadManager.GetUriForDownloadedFile(_downloadId);
if (downloadFileUri != null)
{
Intent intent = new Intent(Intent.ActionView);
intent.SetDataAndType(downloadFileUri, "application/vnd.android.package-archive");
//intent.AddFlags(ActivityFlags.NewTask);

intent.SetFlags(ActivityFlags.NewTask);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);

_mContext.StartActivity(intent);
}
}
注意:下方两句都要有
1
2
intent.SetFlags(ActivityFlags.NewTask);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
权限注意:
1
2
<!-- 允许安装未知来源安装包 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
同时,记得在代码中请求此权限

常用包

参考: xamarin/XamarinComponents: Plugins for Xamarin shinyorg/shiny: A Xamarin Framework for Backgrounding & Device Hardware Services (iOS, Android, UWP, Tizen, tvOS, watchOS, & more coming soon) architchie/httptransfertasks:跨平台HTTP传输下载和上传(支持后台操作) HTTP Transfers - Progress notification · Issue #475 · shinyorg/shiny HTTP Transfers - Alternative Android Download Manager · Issue #485 · shinyorg/shiny Xamarin开发笔记—设备类&第三方弹窗的使用和注意事项 - Java中文社群 - 博客园 rotorgames/Rg.Plugins.Popup: Xamarin Forms popup plugin Rg.Plugins.Popup基本使用_代码整理_聚享阁 Android 开发一般都使用什么框架? - 知乎 Android 开发时你遇到过什么相见恨晚的工具或网站? - 知乎 android-best-practices/README.cn.md at master · futurice/android-best-practices chsword/xamarin-bugly: A bugly SDK for Xamarin Android Bindings

Firebase

参考: Firebase的优势有哪些? - 知乎 parse-community/Parse-SDK-dotNET: Parse SDK for .NET, Xamarin, Unity.

界面设计

参考: google/material-design-icons: Material Design icons by Google jossef/material-design-icons-iconfont: Material Design icons + Development Experience Material Design Icons DX

设置 Button IsEnabled="False",将会使 Button 变回默认样式

参考: c# - Change TextColor & Use Command on IsEnabled in Button in Xamarin - Stack Overflow

Android 11 访问 Android/data 目录

参考: android11 文件读写 访问 android/data 目录_u010963053的博客-CSDN博客 Android11 无Root 访问data目录实现、Android11访问data目录、Android11解除data目录限制、Android11 data空白解决_子君的博客-CSDN博客_安卓11data访问限制如何解决

JarBinding 极光推送

参考: Xamarin学习系列之极光消息推送(示例代码)_136.la 极光推送 - Android SDK 集成指南 - 极光文档 Xamarin 使用极光推送 详细教程 - 尚码园 绑定 .JAR - Xamarin | Microsoft Docs xamarin使用极光推送JPush (安卓)_初级打字员的博客-CSDN博客 Xamarin.Forms Android真机使用极光推送JPush - 知乎

Android 初始屏幕 Splash

参考: 初始屏幕 - Xamarin | Microsoft Docs monodroid-samples/SplashActivity.cs at master · xamarin/monodroid-samples App启动图下载 | Illustrations | unDraw 超赞!15个不翻墙免费可商用矢量素材下载网站推荐! - 知乎 注意: splash_screen.xml 文件默认为 TransformFile,这样会导致 Rebuild 找不到文件 解决: 改为: AndroidResource 即,OneTree.Android.csproj,其中如下:
OneTree.Android.csproj
1
2
3
<ItemGroup>                                                          
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
</ItemGroup>

选择文件

参考: xamarin 实现选择文件功能 - lishidefengchen - 博客园 Xamarin.Essentials: File Picker - Xamarin | Microsoft Docs

上传文件

参考: C# HttpClient设置cookies的两种办法 - 小猪39505 - 博客园 如何在C# 项目中利用HttpClient实现一个文件上传功能 - 开发技术 - 亿速云 C#-HttpClient模拟登录Cookie问题_index的博客-CSDN博客 C# HttpClient设置cookies - 小恋的专栏 - TNBLOG

Android 9.0 必须使用 HTTPS

参考: android9.0适配HTTPS:not permitted by network security policy' - Sharley - 博客园 Android 9.0强制使用https,会阻塞http请求,如果app使用的第三方sdk有http,将全部被阻塞 - 简书 解决android 9.0之后 必须用 https_峰枫风少年的博客-CSDN博客 解决android 9上无法使用http协议 - 红鱼儿 - 博客园 AndroidManifest.xml
1
2
3
4
5
6
<application 
android:label="@string/ApplicationName"
android:theme="@style/MainTheme"
android:icon="@mipmap/icon"
android:networkSecurityConfig="@xml/network_security_config">
</application>
xml/network_security_config.xml
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Android 9.0+ 必须使用HTTPS -->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

下拉刷新,上拉加载

参考: Xamarin Android 打造属于自己的博客园APP(3) - IT胡小帅 - 博客园 Xamarin.Forms菜鸟笔记--3.ListView上拉加载更多_半块菠萝的博客-CSDN博客 自定义 ListView - Xamarin | Microsoft Docs Xamarin.Forms - ListView With Pull To Refresh 在 Xamarin.Forms 中的 ListView 末尾加载更多项目 - James Montemagno c# - Xamarin.Forms ListView Load More - Stack Overflow InfinitescrollInCollectionView/ItemsPage.xaml at 29c59033f1f4013cab774433e233cdf50bf552ad · coolc0ders/InfinitescrollInCollectionView

自定义控件

参考: Xamarin Forms 建立可以绑定属性的方法 ~ 少爷的博客 | 大专栏

HTTP Listener

参考: 1iveowl/Simple-Http-Listener-PCL:Xamarin Forms 的简单 Http 服务器 PCL 1iveowl/SimpleHttpListener.Rx HttpListener Class (System.Net) | Microsoft Docs Writing a WebSocket server in C# - Web APIs | MDN .net - Httplistener with HTTPS support - Stack Overflow C# HttpListener建立Http服务器并使用Silverlight来发送POST请求的疑问 | Code Bye c#-带有SSL证书的HttpListener挂在GetContext()上 - CocoaChina_一站式开发者成长社区 C#通过HttpListener实现HTTP监听 - 走看看

UI

参考: praeclarum/Ooui: A small cross-platform UI library that brings the simplicity of native UI development to the web

分割线

参考: Separators in Xamarin.Forms - Stack Overflow App.xaml
App.xaml
1
2
3
4
5
6
7
<Style x:Key="Separator" TargetType="BoxView">
<Setter Property="HeightRequest" Value="1" />
<Setter Property="HorizontalOptions" Value="FillAndExpand" />
<Setter Property="Color" Value="Gray" />
<Setter Property="Margin" Value="0, 5, 0, 5" />
<Setter Property="Opacity" Value="0.5" />
</Style>
1
<BoxView Style="{StaticResource Separator}" />

发布到酷安

参考: 酷安开发者签名认证步骤_xiayiye5的博客-CSDN博客_酷安开发者认证 java - Jarsigner: certificate chain not found for - Stack Overflow Visual Stuido 2019 找签名文件 yiyun.keystore 1.右键进入 查看归档 如果之前没有生成过 apk(Archive),请先执行一次 Archive archive.xml
archive.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<Archive>
<Name>OneTree</Name>
<PackageName>github.yiyungent.onetree</PackageName>
<PackageVersionCode>1</PackageVersionCode>
<PackageVersionName>0.4.0</PackageVersionName>
<PackageFormat>apk</PackageFormat>
<CreationDate>637619313170861804</CreationDate>
<SolutionName>OneTree.App</SolutionName>
<SolutionPath>F:\Com\me\Repos\OneTree.App\OneTree.App.sln</SolutionPath>
<Status></Status>
<Configuration>
<DebugMode>false</DebugMode>
</Configuration>
<Comment />
<LastUsedKeystore>C:\Users\yiyun\AppData\Local\Xamarin\Mono for Android\Keystore\yiyun\yiyun.keystore</LastUsedKeystore>
<TimeStampingAuthority />
<LastInsightsUploadDate>0</LastInsightsUploadDate>
</Archive>
其中 LastUsedKeystore 即为签名文件路径 使用此签名文件,对酷安给的未签名apk ( CoolApkDevVerify_no_sign.apk )签名,生成 签名的 signed.apk
1
jarsigner -verbose -keystore yiyun.keystore -signedjar signed.apk CoolApkDevVerify_no_sign.apk yiyun
1
jarsigner -verbose -keystore [Your signature storage path] -signedjar [signed filename] [unsigned filename] [Your alias key]
补充: 查看 alias key,其实就是你当时创建秘钥时的用户名
1
keytool -keystore yiyun.keystore -list  -v
1
keytool -keystore [your key store] -list -v
yiyun.keystore:代表你的项目签名文件 signed.apk:代表你apk的签名包 CoolApkDevVerify_no_sign.apk:代表酷安提供给你的未签名包 输入上面的命令后你桌面要上传到酷安的apk会变成已签名(并且和酷安提供的未签名安装包差不多大) 其实就是将 酷安给你的 CoolApkDevVerify_no_sign.apk ,用你给你自己的apk签名的秘钥,再给这个验证apk 签名一下 其实就是下面这个,我没设置,所以没有

WebView

参考: Xamarin中WebView问题记录_liangyj66的博客-CSDN博客 Xamarin WebView App (Hybrid) with Xamarin Essential - Stack Overflow Customizing a WebView - Xamarin | Microsoft Docs Building HTML views using Razor Templates - Xamarin | Microsoft Docs Android清除WebView缓存_Fantasy-CSDN博客 Xamarin Forms WebView在Android中加载混合内容_三行代码 WebView.SetMixedContentMode Methode (Xamarin.Forms.PlatformConfiguration.AndroidSpecific) | Microsoft Docs WebView中Settings积累 - 简书 WebView的使用详解 - 知乎 使用Chrome DevTools调试WebView_秦川小将-CSDN博客 Android WebView顶部进度条 - 简书 WebView添加进度条_wuqingsen1的博客-CSDN博客_webview进度条

WebView 与 JavaScript 交互

参考: Hybrid WebView 技术总结 | 大专栏 Android Hybrid 和 WebView 解析_u012014301的博客-CSDN博客 WebView中支持加载http和https混合模式_冰雪世界-CSDN博客 WebView加载http、https细节,交互及注意事项 - 简书 android WebView详解,常见漏洞详解和安全源码(下)_Shawn_Dut的专栏-CSDN博客_webview漏洞

1.js -> WebView: console.log

1
2
// Javascript 代码
console.log('{"width": "750"}');
1
2
3
4
5
6
7
8
9
10
11
// C#
public class TorchWebChromeClient : Android.Webkit.WebChromeClient
{

public override void OnConsoleMessage(string message, int lineNumber, string sourceID)
{
// message 即为 JS 传过来的消息,判断消息来决定调用方法
base.OnConsoleMessage(message, lineNumber, sourceID);
}

}

2.js -> WebView: JSBridge

最普遍方法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题 通过 addJavascriptInterface 方法进行添加对象映射 这种方法实际是向 js 环境上下文 ( Window ) 注入,以供 js 调用 实际上,下面向 window 中注入了 jsBridge.invokeAction 和 invokeCSharpAction,后者是前者的封装,实际上你也可以直接使用 jsBridge.invokeAction,不过一定要保证在 OnPageFinished 后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JSBridge : Java.Lang.Object
{
readonly WeakReference<HybridWebViewRenderer> hybridWebViewRenderer;

public JSBridge(HybridWebViewRenderer hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewRenderer>(hybridRenderer);
}

// 暴露方法名: invokeAction
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
HybridWebViewRenderer hybridRenderer;

if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
// 调用 WebView
((HybridWebView)hybridRenderer.Element).InvokeAction(data);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace TorchView4Droid.Components
{
public class HybridWebViewRenderer : WebViewRenderer
{
const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";

protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);

if (e.OldElement != null)
{
// Unsubscribe from event handlers

Control.RemoveJavascriptInterface("jsBridge");
((HybridWebView)Element).Cleanup();
}
if (e.NewElement != null)
{
// 1.WebViewClient
var webViewClient = new JavascriptWebViewClient(this, {{hbeSeoContent}}quot;javascript: {JavascriptFunction}");
Control.SetWebViewClient(webViewClient);
// 暴露在 jsBridge 对象上
// 于是最终: jsBridge.invokeAction
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JavascriptWebViewClient : FormsWebViewClient
{
string _javascript;

public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
{
_javascript = javascript;
}

public override void OnPageFinished(WebView view, string url)
{
base.OnPageFinished(view, url);
// 封装的函数被在此处 保证执行了一次,以注入
view.EvaluateJavascript(_javascript, null);
}
}
js 调用C#
1
2
3
4
5
6
7
8
function invokeCSCode(data) {
try {
log("Sending Data:" + data);
invokeCSharpAction(data);
} catch (err) {
log(err);
}
}

3. js -> WebView: WebViewClient 拦截 url

缺点: 协议的约束需要记录一个规范的文档,并且 js 无法立即获取 C# 的返回值,需要 C# 再次主动调用 js 来传递返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JavascriptWebViewClient : FormsWebViewClient
{
public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request)
{
// 拦截url, 检查 Url.Scheme 是否为你为js调用定义的 Scheme
if (request.Url != null && request.Url.Scheme != null && request.Url.Scheme.ToLower() == "js")
{
// 调用 C#
// 可以从 Query 中解析传过来的数据
var queryPars = request.Url.QueryParameterNames;

// 举例: 打开本地页面
// url = "js://openActivity?arg1=111&arg2=222"
/ ...
}

return base.ShouldOverrideUrlLoading(view, request);
}
}
1
2
3
4
// JavaScript
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}
缺点: 不能拿到 C# 的返回值, 若 js 想拿到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 js 方法把返回值传递回去,相关的代码如下:
1
webView.LoadUrl("javascript:returnResult(" + result + ")");
1
2
3
4
// JavaScript
function returnResult(result){
alert("result is" + result);
}

4.js -> WebView: WebChromeClient 三方法拦截消息

prompt 对话框方法可以返回字符串类型的返回值, 缺点: 协议的制定比较麻烦,需要记录详细的文档,但是不会存在漏洞问题 拦截 js 中的几个提示方法,也就是几种样式的对话框,在 js 中有三个常用的对话框方法: OnJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入; OnJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消; OnJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
1
2
3
4
5
6
// JavaScript
function clickPrompt(){
// 优点: 可以拿到 C# 方法返回值
var result = prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TorchWebChromeClient : Android.Webkit.WebChromeClient
{
#region js 三对话框

public override bool OnJsAlert(WebView view, string url, string message, JsResult result)
{
return base.OnJsAlert(view, url, message, result);
}

public override bool OnJsConfirm(WebView view, string url, string message, JsResult result)
{
return base.OnJsConfirm(view, url, message, result);
}

public override bool OnJsPrompt(WebView view, string url, string message, string defaultValue, JsPromptResult result)
{
// 注意: js 传过来的数据在 message
// message = "js://openActivity?arg1=111&arg2=222"
Android.Net.Uri uri = Android.Net.Uri.Parse(message);
string scheme = uri.Scheme;
if (scheme != null && scheme.ToLower() == "js")
{
IList<string> queryPars = uri.QueryParameterNames?.ToList();


// 代表应用内部处理完成
result.Confirm("success");

return true;
}

return base.OnJsPrompt(view, url, message, defaultValue, result);
}

#endregion
}
只有 OnJsPrompt 方法可以返回字符串类型的值,放在 result (JsPromptResult) 中,所以选择拦截它

5.WebView -> js: webView.LoadUrl

缺点: C# 调用 js ,无法立即获取 js的返回值,只能通过 js再次调用 C# 来传入返回值, loadUrl 的执行会造成页面刷新一次
1
2
// C#
mWebView.LoadUrl("javascript:show(" + result + ")");
1
2
3
4
5
// JavaScript
function show(result){
alert("result"=result);
return "success";
}
注意 方法名字对应, 还有,js 的调用一定要在 WebViewClient.OnPageFinished 函数回调之后才能调用,要不然也会失败。

6. WebView -> js: webView.EvaluateJavascript

Google 在 Android4.4 为我们新增加了一个新方法,这个方法比 loadUrl 方法更加方便简洁,而且比 loadUrl 效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以我们使用的时候需要添加版本的判断
1
2
3
4
5
6
7
8
9
10
string jsFuncStr = "";
if ((int)Build.VERSION.SdkInt < 18)
{
webView.LoadUrl(jsFuncStr);
}
else
{
var jsCallback = new JsFuncValueCallback();
webView.EvaluateJavascript(jsFuncStr, jsCallback);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#region JsFuncValueCallback
public class JsFuncValueCallback : Android.Webkit.IValueCallback
{
public void OnReceiveValue(Object? value)
{
// value 为 js 返回的结果

// 转换为 string 写法来自:Xamarin.Forms.Platform.Android.JavascriptResult
string data = ((Java.Lang.String)value)?.ToString();

// TODO: js 返回值处理
}

// ...
}
#endregion
一般最常使用的就是第一种方法,但是第一种方法获取返回的值比较麻烦,而第二种方法由于是在 4.4 版本引入的,所以局限性比较大。

WebView 加载本地 html

方案1:
1
file://xxxx/index.html
强烈不推荐 方案2: 在本地启动一个 WebServer,监听某个端口,url使用 http://localhost:12531 方案3: 参考: ShouldInterceptRequest - vue.js - VueJS in Android WebView (Xamarin) - Stack Overflow vue.js - VueJS in Android WebView (Xamarin) - Stack Overflow 自定义url前缀,或是 HTTP Url.Scheme, Url.Host,再通过 override WebViewClient ,拦截url请求
1
2
public override WebResourceResponse ShouldInterceptRequest( WebView view, IWebResourceRequest request ) {
}

Failed to decode downloaded font: <URL>

参考: Failed to decode downloaded font问题解决 - 程序员大本营 http Content-type对照表 - 一只特立独行的猫 - 博客园 HTTP Content-Type对照表_mime类型大全-90度查询网 Nginx Gzip模块启用和配置指令详解 - 灰信网(软件开发博客聚合) 一个fopen函数中未使用二进制模式(b)引发的血案_大道至简,持之以恒-CSDN博客_fopen 二进制 c#-直接将字节抽入Response.OutputStream-如何处理字节数? 文本方式读写二进制文件,可能导致损坏内容 二进制方式很简单,读文件时,会原封不动的读出文件的全部內容,写的時候,也是把內存缓冲区的內容原封不动的写到文件中。   而文本方式就不一样了,在写文件时,会将换行符号CRLF(0x0D 0x0A)全部转换成单个的0x0A,并且当遇到结束符CTRLZ(0x1A)时,就认为文件已经结束。相应的,写文件时,会将所有的0x0A换成0x0D0x0A。 所以,若使用文本方式打开二进制文件时,就很容易出现文件读不完整,或內容不对的错误。即使是用文本方式打开文本文件,也要谨慎使用,比如复制文件,就不应该使用文本方式。

Resource interpreted as Stylesheet but transferred with MIME type text/plain: "<URL>".

Java.Lang.IllegalStateException: AssetInputStream is closed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{Java.Lang.IllegalStateException: AssetInputStream is closed
at Java.Interop.JniEnvironment+InstanceMethods.CallNonvirtualIntMethod (Java.Interop.JniObjectReference instance, Java.Interop.JniObjectReference type, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x0008e] in <8b3b636835d84984ba4604c1f57b1983>:0
at Java.Interop.JniPeerMembers+JniInstanceMethods.InvokeNonvirtualInt32Method (System.String encodedMember, Java.Interop.IJavaPeerable self, Java.Interop.JniArgumentValue* parameters) [0x0001f] in <8b3b636835d84984ba4604c1f57b1983>:0
at Android.Content.Res.AssetManager+AssetInputStream.Read (System.Byte[] b, System.Int32 off, System.Int32 len) [0x00052] in <44e54a86dea24313a2bdb807df77c27a>:0
at Android.Runtime.InputStreamInvoker.Read (System.Byte[] buffer, System.Int32 offset, System.Int32 count) [0x00006] in <44e54a86dea24313a2bdb807df77c27a>:0
at System.IO.Stream.CopyTo (System.IO.Stream destination, System.Int32 bufferSize) [0x0001f] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corert/src/System.Private.CoreLib/shared/System/IO/Stream.cs:179
at System.IO.Stream.CopyTo (System.IO.Stream destination) [0x00007] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corert/src/System.Private.CoreLib/shared/System/IO/Stream.cs:168
at (wrapper remoting-invoke-with-check) System.IO.Stream.CopyTo(System.IO.Stream)
at TorchView.WebServer.TaskProc (System.Object obj) [0x000ac] in F:\Com\me\Repos\TorchView\src\TorchView\WebServer.cs:144
--- End of managed Java.Lang.IllegalStateException stack trace ---
java.lang.IllegalStateException: AssetInputStream is closed
at android.content.res.AssetManager$AssetInputStream.ensureOpen(AssetManager.java:1364)
at android.content.res.AssetManager$AssetInputStream.read(AssetManager.java:1303)
}

打包 apk

参考: 开发App到上架应用市场需要经历什么? - popfisher - 博客园 做好应用程序发布准备 - Xamarin | Microsoft Docs Xamarin.Android release-Apk 瘦身策略_Vir_czz的博客-CSDN博客 ProGuard - Xamarin | Microsoft Docs

指定图标

ProGuard

自定义 ProGuard

或者,可添加自定义 ProGuard 配置文件,实现对 ProGuard 工具的更多掌控。 例如,你可能想就要保留的类显式通知 ProGuard。 为此,请新建 .cfg 文件,并在 解决方案资源管理器 的“属性”窗格中应用 ProGuardConfiguration 生成操作: 例如,使用了 腾讯 Bugly,则 请避免混淆Bugly,在Proguard混淆文件中增加以下配置:
Proguard.cfg
1
2
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
对于大多数 Xamarin.Android 应用,Xamarin.Android 提供的默认 ProGuard 配置文件足以删除所有(仅)未使用的代码。 若要查看默认 ProGuard 配置,请打开 **obj_xamarin.cfg** 处的文件。 请记住,该配置文件不会替换 Xamarin.Android proguard_xamarin.cfg 文件,因为 ProGuard 将使用这两者。 对应 OneTree.Android.csproj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>true</BundleAssemblies>
<AndroidLinkTool>proguard</AndroidLinkTool>
</PropertyGroup>
1
2
3
<ItemGroup>
<ProguardConfiguration Include="ProGuard.cfg" />
</ItemGroup>

保护应用程序

禁用调试

在 Android 应用程序开发期间,将使用 Java 调试线路协议 (JDWP) 执行调试。 这是一种技术,它允许 adb 等工具出于调试目的与 JVM 通信。 默认对 Xamarin.Android 应用程序的调试版本启用 JDWP。 虽然 JDWP 在开发过程中很重要,但它会对已发布的应用程序造成安全问题。 重要 请始终禁用已发布应用程序中的调试状态,因为如果不禁用此状态,则可能(通过 JDWP)获得 Java 进程的完全访问权限并在应用程序的上下文中执行任意代码。 Android 清单包含 android:debuggable 属性,该属性控制是否可以调试应用程序。 将 android:debuggable 属性设置为 false 被视为一种很好的做法。 执行此操作最简单的方法是在 AssemblyInfo.cs 中添加条件编译语句:
1
2
3
4
5
#if DEBUG
[assembly: Application(Debuggable=true)]
#else
[assembly: Application(Debuggable=false)]
#endif

将程序集捆绑到本机代码

此选项启用时,程序集会捆绑到本机共享库中。 这样便可以对程序集进行压缩,减小 .apk 文件的大小。 程序集压缩还提供最小形式的模糊处理;此类模糊处理不应作为依据。 此选项需要 Enterprise 许可证,仅当“使用快速部署”禁用时才可用。 “将程序集捆绑到本机代码”在默认情况下处于禁用状态。 请注意,“捆绑到本机代码”选项执行不意味着程序集会编译到本机代码中。 无法使用 AOT 编译将程序集编译为本机代码。 对应 OneTree.Android.csproj
1
2
3
<PropertyGroup>
<BundleAssemblies>true</BundleAssemblies>
</PropertyGroup>
注意: 本人试用后,apk体积从 14MB 增加到 25MB,不知道为何,并没有缩小,反而增大 注意: 发现,同一套代码,同一个打包配置, Visual Studio 2019 Professional 打包体积 13.5 MB Visual Studio 2019 Enterprise 打包体积 19.2 MB, 居然企业版打包体积还要大些,而只有企业版有 into Native Code

配置链接器

报错: Using ProGuard with the D8 DEX compiler is no longer supported

1
Using ProGuard with the D8 DEX compiler is no longer supported. Please set the code shrinker to 'r8' in the Visual Studio project property pages or edit the project file in a text editor and set the 'AndroidLinkTool' MSBuild property to 'r8'.
解决: ProGuard 不能与 d8 一起使用,要么 使用 ProGuard,就只能换 d8 为 dx, 或者不用 ProGuard,而是 使用 r8 与 d8

keystore 密码修改

参考: android 签名 别名,修改Android签名证书keystore的密码、别名alias以及别名密码_彭碧康的博客-CSDN博客

Visual Studio AppCenter

参考: Get Started with Xamarin - Visual Studio App Center | Microsoft Docs Visual Studio App Center | iOS, Android, Xamarin & React Native App Center Distribute for Xamarin - Visual Studio App Center | Microsoft Docs Understanding App Center Events Metrics - Visual Studio App Center | Microsoft Docs App Center Analytics for Xamarin - Visual Studio App Center | Microsoft Docs

Xamarin 结合 GitHub Actions

参考: saamerm/Xamarin-GitHubActions-DevOps: This repo contains the result of my work combining GitHub Actions with Xamarin Forms for iOS & Android TBertuzzi/ExemploXamarinGithubActions: Exemplo de Configuração de Github Actions para Xamarin e Xamarin.Forms shell - MSB4019 Run Xamarin Android Unit Tests using Github Actions - Stack Overflow How to build a xamarin android application using github actions? - Stack Overflow maxim-lobanov/setup-xamarin: Set up your GitHub Actions workflow with a specific version of Xamarin & Mono 使用GitHub Actions实现Android自动打包apk_无bug不人生-CSDN博客_github 打包apk 参考 感谢帮助! 用xamarin开发App的体验如何? - 知乎 XamSome/awesome-xamarin: A collection of interesting libraries/tools for Xamarin mobile projects o1298098/Xamarin-CloudMusic: Xamarin.Forms goodlooking UI sample JoesWeek/XamCnblogs: 博客园第三方客户端,Xamarin.Forms App,支持Android,IOS xamarin/monodroid-samples: A collection of Xamarin.Android sample projects. Android & iOS Apps with Xamarin | .NET C#使用Xamarin开发可移植移动应用进阶篇(8.打包生成安卓APK并精简大小),附源码 - 程序员大本营