参考:
应用可以在外部存储上保留两种不同类型的文件:
// 专用外部存储目录
// /storage/emulated/0/Android/data/com.companyname.app/files/
Android.Content.Context.GetExternalFilesDir(string type)
// 主外部存储目录
// /storage/emulated/0/
Android.OS.Environment.ExternalStorageDirectory
Android 将外部存储视为危险权限,这通常要求用户授予其访问资源的权限。 用户可以随时撤销此权限。 这意味着在进行任何文件访问之前都应执行运行时权限请求。 应用会被自动授予读取和写入其自己的专用文件的权限。 在用户授予了权限之后,应用可以读取和写入属于其他应用的专用文件。
//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:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
注意:下面有误
如上,在安卓项目里有个Properties的文件下有个AndroidManifest.xml的文件。在
<application android:label="cardionNet2.Android"></application>
下加
这个目测有误,直接加进去就是了,不需要放在这个;里
<?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
属性保存标识外部存储状态的字符串。 此属性会返回表示状态的字符串。
bool isReadonly = Environment.MediaMountedReadOnly.Equals(Environment.ExternalStorageState);
bool isWriteable = Environment.MediaMounted.Equals(Environment.ExternalStorageState);
完全限定
bool isReadonly = Android.OS.Environment.MediaMountedReadOnly.Equals(Android.OS.Environment.ExternalStorageState);
bool isWriteable = Android.OS.Environment.MediaMounted.Equals(Android.OS.Environment.ExternalStorageState);
参考:
Application
基类提供下列功能:
OnStart
、OnSleep
和 OnResume
。PageAppearing
、PageDisappearing
。ModalPushing
、ModalPushed
、ModalPopping
和 ModalPopped
。生命周期方法
Application
类包含三个虚拟方法,可以替代以响应生命周期更改:
OnStart
- 在启动应用程序时调用它。OnSleep
- 每当应用程序转入后台时调用它。OnResume
- 应用程序发送到后台后恢复时调用。参考:
参考:
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
方法。
<!-- 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
: 浮出控件的菜单项
参考:
可以通过图标或从屏幕的一侧轻扫来访问它。 浮出控件由可选标头、浮出控件项、可选菜单项和可选页脚组成:
<!--
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>
隐式转换
<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>
等同于下方:
<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
集合, 该集合定义将在浮出控件中显示的项的列表。
通过将 Shell.ItemTemplate
附加属性设置为 DataTemplate
可自定义每个 FlyoutItem
的外观:
<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
:
<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
来定义浮出控件内容:
<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>
场景: 有时候,默认并不需要显示第一个
首次运行使用浮出控件的 Shell 应用程序时,Shell.CurrentItem
属性将设置为子类化的 Shell
对象中的第一个 FlyoutItem
对象。 但是,此属性可以设置为另一个 FlyoutItem
,如以下示例所示:
<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# 代码为:
CurrentItem = aboutItem;
在此示例中,CurrentItem
属性是在子类化的 Shell
类中设置的。 或者,可通过 Shell.Current
静态属性在任何类中设置 CurrentItem
属性:
Shell.Current.CurrentItem = aboutItem;
浮出项在浮出控件中默认可见。 但是,可以使用 FlyoutItemIsVisible
属性将项隐藏在浮出控件中,并使用 IsVisible
属性将其从浮出控件中删除:
bool
的 FlyoutItemIsVisible
指示项是否已隐藏在浮出控件中但仍可以通过 GoToAsync
导航方法进行访问。 此属性的默认值为 true
。bool
的 IsVisible
指示是否应从可视化树中移除项,从而不在浮出控件中显示。 它的默认值为 true
。备注 还有一个
Shell.FlyoutItemIsVisible
附加属性,可在FlyoutItem
、MenuItem
、Tab
和ShellContent
对象上设置该属性。
<Shell ...
FlyoutIsPresented="{Binding IsFlyoutOpen}">
</Shell>
Shell.Current.FlyoutIsPresented = false;
参考:
<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
就会隐藏起来,那么这个时候就只能通过代码导航到这里了。
<!--
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
,就不会显示底部选项卡导航栏
<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
属性,可定义选项卡标题。 类型为 ImageSource
的 Icon
属性,可定义选项卡图标:
如果 TabBar
上有五个以上的选项卡,则显示“更多”选项卡,可用于访问其他选项卡:
如果一个 Tab
对象中存在多个 ShellContent
对象时,则将在底部选项卡中添加一个顶部选项卡栏,通过该选项卡栏可以导航 ContentPage
对象:
<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
,如以下示例所示:
<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>
参考:
没办法直接在Shell中,同时显式定义
FlyoutItem
和TabBar
只能通过FlyoutItem
隐式达到效果注意: 并没有在
FlyoutItem
上使用FlyoutDisplayOptions="AsMultipleItems"
, 这会导致首页、游戏、频道、动态
也显示在侧边浮出栏
<!-- 显示在底部导航栏 -->
<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
<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>
参考:
官方没有实现 底部选项导航栏(包括子项顶部导航栏) 滑动动画切换页面 见 [Feature] Swipe left/right to navigate between upper/bottom tabs of Shell · Issue #12435 · xamarin/Xamarin.Forms
参考:
滚动视图 ScrollView 在Xamarin.Forms中,滚动视图ScrollView用来实现长内容的滚动显示。虽然ScrollView的Content属性只能设置一个值,即ScrollView只能包含一个子元素,但它实际是一个布局控件,一个特殊的布局元素。 在使用的时候,ScrollView要求父容器给它分配固定的大小,同时子元素并且有固定的大小。这样,ScrollView才能根据各自大小计算滚动量。ScrollView不仅提供了当前滚动量ScrollX和ScrollY,还提供内容总量ContentSize。这样,开发者就可以计算滚动进度,显示给用户。同时,利用ScrollView提供的滚动结束事件Scrolled,可以提示用户,或者加载新的内容。
参考:
V**
参考:
参考:
参考:
Xamarin.Android获取当前版本号
Android
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
public string GetVersion()
{
var activity=Xamarin.Forms.Forms.Context as Activity;
return activity.PackageManager.GetPackageInfo(activity.PackageName, 0).VersionName;
}
iOS
public string GetVersion()
{
return NSBundle.MainBundle.InfoDictionary["CFBundleShortVersionString"].ToString();
}
安装本地apk
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
<?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
<?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>
参考:
android8.0以上权限变更,若apk内下载安装包后安装,首先需要确认是否有安装未知来源应用程序的权限。
首先,需要在清单文件内加入以下权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
安卓8以上,代码Intent调用打开apk,未唤起安装界面
解决:
//下载到本地后执行安装
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);
}
}
注意:下方两句都要有
intent.SetFlags(ActivityFlags.NewTask);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
权限注意:
<!-- 允许安装未知来源安装包 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
同时,记得在代码中请求此权限
参考:
参考:
参考:
IsEnabled="False"
,将会使 Button
变回默认样式参考:
参考:
参考:
参考:
注意:
splash_screen.xml
文件默认为 TransformFile
,这样会导致 Rebuild 找不到文件
解决:
改为: AndroidResource
即,OneTree.Android.csproj
,其中如下:
<ItemGroup>
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
</ItemGroup>
参考:
参考:
参考:
AndroidManifest.xml
<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
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Android 9.0+ 必须使用HTTPS -->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
参考:
参考:
参考:
参考:
参考:
App.xaml
<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>
<BoxView Style="{StaticResource Separator}" />
参考:
Visual Stuido 2019
找签名文件 yiyun.keystore
1.右键进入 查看归档
如果之前没有生成过 apk(Archive),请先执行一次 Archive
archive.xml
<?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
jarsigner -verbose -keystore yiyun.keystore -signedjar signed.apk CoolApkDevVerify_no_sign.apk yiyun
jarsigner -verbose -keystore [Your signature storage path] -signedjar [signed filename] [unsigned filename] [Your alias key]
补充:
查看 alias key
,其实就是你当时创建秘钥时的用户名
keytool -keystore yiyun.keystore -list -v
keytool -keystore [your key store] -list -v
yiyun.keystore:代表你的项目签名文件 signed.apk:代表你apk的签名包 CoolApkDevVerify_no_sign.apk:代表酷安提供给你的未签名包 输入上面的命令后你桌面要上传到酷安的apk会变成已签名(并且和酷安提供的未签名安装包差不多大)
其实就是将 酷安给你的
CoolApkDevVerify_no_sign.apk
,用你给你自己的apk签名的秘钥,再给这个验证apk 签名一下
其实就是下面这个,我没设置,所以没有
参考:
参考:
// Javascript 代码
console.log('{"width": "750"}');
// C#
public class TorchWebChromeClient : Android.Webkit.WebChromeClient
{
public override void OnConsoleMessage(string message, int lineNumber, string sourceID)
{
// message 即为 JS 传过来的消息,判断消息来决定调用方法
base.OnConsoleMessage(message, lineNumber, sourceID);
}
}
最普遍方法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题
通过 addJavascriptInterface 方法进行添加对象映射
这种方法实际是向 js 环境上下文 ( Window ) 注入,以供 js 调用 实际上,下面向 window 中注入了
jsBridge.invokeAction
和invokeCSharpAction
,后者是前者的封装,实际上你也可以直接使用jsBridge.invokeAction
,不过一定要保证在OnPageFinished
后
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);
}
}
}
[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, $"javascript: {JavascriptFunction}");
Control.SetWebViewClient(webViewClient);
// 暴露在 jsBridge 对象上
// 于是最终: jsBridge.invokeAction
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
}
}
}
}
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#
function invokeCSCode(data) {
try {
log("Sending Data:" + data);
invokeCSharpAction(data);
} catch (err) {
log(err);
}
}
缺点: 协议的约束需要记录一个规范的文档,并且 js 无法立即获取 C# 的返回值,需要 C# 再次主动调用 js 来传递返回值
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);
}
}
// JavaScript
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}
缺点: 不能拿到 C# 的返回值,
若 js 想拿到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 js 方法把返回值传递回去,相关的代码如下:
webView.LoadUrl("javascript:returnResult(" + result + ")");
// JavaScript
function returnResult(result){
alert("result is" + result);
}
prompt 对话框方法可以返回字符串类型的返回值, 缺点: 协议的制定比较麻烦,需要记录详细的文档,但是不会存在漏洞问题
拦截 js 中的几个提示方法,也就是几种样式的对话框,在 js 中有三个常用的对话框方法:
// JavaScript
function clickPrompt(){
// 优点: 可以拿到 C# 方法返回值
var result = prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}
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)
中,所以选择拦截它
缺点: C# 调用 js ,无法立即获取 js的返回值,只能通过 js再次调用 C# 来传入返回值, loadUrl 的执行会造成页面刷新一次
// C#
mWebView.LoadUrl("javascript:show(" + result + ")");
// JavaScript
function show(result){
alert("result"=result);
return "success";
}
注意 方法名字对应, 还有,js 的调用一定要在 WebViewClient.OnPageFinished 函数回调之后才能调用,要不然也会失败。
Google 在 Android4.4 为我们新增加了一个新方法,这个方法比 loadUrl 方法更加方便简洁,而且比 loadUrl 效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以我们使用的时候需要添加版本的判断
string jsFuncStr = "";
if ((int)Build.VERSION.SdkInt < 18)
{
webView.LoadUrl(jsFuncStr);
}
else
{
var jsCallback = new JsFuncValueCallback();
webView.EvaluateJavascript(jsFuncStr, jsCallback);
}
#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 版本引入的,所以局限性比较大。
方案1:
file://xxxx/index.html
强烈不推荐
方案2: 在本地启动一个 WebServer,监听某个端口,url使用 http://localhost:12531
方案3:
参考:
自定义url前缀,或是 HTTP Url.Scheme, Url.Host,再通过 override WebViewClient ,拦截url请求
public override WebResourceResponse ShouldInterceptRequest( WebView view, IWebResourceRequest request ) {
}
<URL>
参考:
文本方式读写二进制文件,可能导致损坏内容 二进制方式很简单,读文件时,会原封不动的读出文件的全部內容,写的時候,也是把內存缓冲区的內容原封不动的写到文件中。 而文本方式就不一样了,在写文件时,会将换行符号CRLF(0x0D 0x0A)全部转换成单个的0x0A,并且当遇到结束符CTRLZ(0x1A)时,就认为文件已经结束。相应的,写文件时,会将所有的0x0A换成0x0D0x0A。 所以,若使用文本方式打开二进制文件时,就很容易出现文件读不完整,或內容不对的错误。即使是用文本方式打开文本文件,也要谨慎使用,比如复制文件,就不应该使用文本方式。
<URL>
".{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)
}
参考:
或者,可添加自定义 ProGuard 配置文件,实现对 ProGuard 工具的更多掌控。 例如,你可能想就要保留的类显式通知 ProGuard。 为此,请新建 .cfg 文件,并在 解决方案资源管理器 的“属性”窗格中应用 ProGuardConfiguration
生成操作:
例如,使用了 腾讯 Bugly,则
-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
<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>
<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 中添加条件编译语句:
#if DEBUG
[assembly: Application(Debuggable=true)]
#else
[assembly: Application(Debuggable=false)]
#endif
此选项启用时,程序集会捆绑到本机共享库中。 这样便可以对程序集进行压缩,减小 .apk
文件的大小。 程序集压缩还提供最小形式的模糊处理;此类模糊处理不应作为依据。
此选项需要 Enterprise 许可证,仅当“使用快速部署”禁用时才可用。 “将程序集捆绑到本机代码”在默认情况下处于禁用状态。
请注意,“捆绑到本机代码”选项执行不意味着程序集会编译到本机代码中。 无法使用 AOT 编译将程序集编译为本机代码。
对应 OneTree.Android.csproj
<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. 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
参考:
参考:
参考:
感谢帮助!
本文作者: yiyun
本文链接: https://moeci.com/posts/分类-dotnet/xamarin/
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!