
简介这是一套C#开发Android应用实战资料包面向有C#基础、希望借助Xamarin或Mono for Android进入移动开发的程序员也适合已熟悉Java/Kotlin、想拓展跨平台技能的开发者。压缩包共93个文件、约417MB以cs源码、xml布局/配置、dll库及sln工程文件为主并含mdb数据文件、keystore签名文件及多个txt说明文档整体工程结构完整可直接打开编译运行。资源内含三个可运行的示例模块FreshMeat2演示动态数据的获取与列表展示MonoForAndroidPreferences讲解SharedPreferences用户配置的存储与读取QuickEdit实现文本编辑器并涉及触摸事件与多线程处理可帮助理解Activity、Intent、API交互、界面适配等核心知识点。这套资料也覆盖从环境搭建、C#语法到数据存储、网络通信与发布打包的完整开发链条尤其适合想用C#快速开发Android平板应用的学习者。目前已有570人学习下载适合希望通过完整源码动手实践的开发者。 上个月陪一个做自动化设备的朋友吃饭他吐槽团队里全是C#老玩家WinForm上位机写得飞起结果客户突然提了个需求——搞一个Android平板端的现场工单App。团队里没人写过Kotlin和Java外包又怕源码烂尾问我有没有办法用C#直接干。这问题我太熟了因为我这半年刚把一个真实上线的Pad端巡检项目跑通技术栈就是C# Android项目源码连同踩坑记录给我整理成了一个完整的实战包也就是你看到的这个“C#开发android应用实战 pad源码.rar”。这篇我就把这个项目的核心设计、技术选型、关键实现和排坑过程全部铺开讲。适合谁看主要是两类人一类是长期写C#上位机、WPF、WinForm想把手里的活儿延伸到移动端但不想重学Java的另一类是刚接Android项目被各种环境问题、碎片化适配和存储权限折腾得想摔键盘的。我会把从环境搭建、Pad布局适配、文件存储权限到打包发布的完整链路都讲清楚所有代码都是实测跑通的。1. 项目整体设计与技术选型思路1.1 核心需求解析为什么要用C#开发Android应用先把需求摆出来这是一个Pad端现场巡检工单系统核心功能包括任务下发、现场拍照上传、离线数据缓存、工单签收和表单填报。设备是10寸左右的Android工业平板要求能适配不同分辨率和屏幕方向还要处理摄像头拍照、本地SQLite存储、HTTP同步这些常见动作。如果这个项目放在常规团队99%的人会直接开Android Studio Kotlin。但这里有个现实的团队背景问题开发人员全是.NET背景对C#非常熟对Java和Kotlin基本零经验。如果硬切Kotlin光是学习语法的成本加上项目排期就够喝一壶。更重要的是公司内部有一个现成的C#业务逻辑库包含工单状态机、表单校验规则和协议解析模块这些代码直接用.NET标准库就能和Android端共享。这个“代码复用”是最关键的因素。我用C#做Android选的是Xamarin.Android方案。你别一上来就纠结Xamarin是不是过时了、.NET MAUI是不是更新——技术选型永远是看场景的。Xamarin.Android这个方案在Android端的原理本质上是通过绑定把Android的Java API映射为C# API底层跑的是Mono运行时可以调用几乎所有的原生Android接口。这个项目要的是稳定和业务代码复用不是追新Xamarin.Android足够成熟踩坑资料也全。1.2 与原生开发的对比C#方案的真实优劣势我做一个直白的对比大家在面对“C#写Android到底行不行”的时候可以参考维度Kotlin/Java原生开发C# (Xamarin.Android / .NET MAUI)团队学习成本需掌握Kotlin语法、Android生态只需学Android机制语言层零门槛业务代码复用难以复用现有.NET/C#库可直接引用.NET Standard类库UI开发体验XML/Compose声明式代码布局XAML接近WPF习惯性能损耗直接编译为字节码多一层Mono运行时有一定开销社区与资料极其丰富相对少但足够覆盖主流场景打包与发布Android Studio全流程VS集成需注意签名与AAB配置这个表格不是说C#方案全面碾压原生而是说在“团队已有C#技能储备、业务复杂度和排期压力都比较大”的特定项目里C#方案是性价比更高的选择。性能方面Mono运行时在Android上的开销主要体现在应用启动阶段和JIT编译实测一个业务类App在稳定运行后性能差距并不大操作数据库和网络请求这种IO密集场景基本没有体感差异。1.3 项目架构与源码包内容规划整个实战包的架构我做了三层拆分参考了常见的MVVM思想但去掉了重量级框架避免给新手增加负担表现层Android Activity Fragment负责界面展示和用户交互。业务层C#业务逻辑类库处理工单状态流转、字段校验、照片压缩等。数据层SQLite存储、文件缓存、HttpClient网络同步。源码包里面包含完整的项目文件、AndroidManifest配置、资源文件、打包脚本和一份环境部署说明。整个设计原则就一条能用一段代码说清楚的事绝不给读者整出三个继承层次。所以你会看到我在源码里大量使用原生Intent、ContentResolver这些Android基础组件而不是套一堆看不懂的抽象。2. 开发环境搭建与工程配置要点2.1 从零搭建Xamarin.Android开发环境先说环境我用的是Windows 10 Visual Studio 2022。安装VS时一定要勾选“使用.NET的移动开发”工作负载这个负载里包含.NET Android也就是Xamarin.Android所需的全部组件。这里有个经常被忽略的点Android SDK的路径和JDK版本必须匹配。我建议直接用Visual Studio自带的Android SDK管理器装SDK不要自己从网上下Android Studio。版本上我用的JDK是OpenJDK 11Android SDK平台版本用的是API 33Android 13编译目标设为API 33最低支持设为API 26Android 8.0可以覆盖市面上绝大部分平板设备。环境变量这一块容易踩坑很多人装完VS发现创建不了Android项目一查是ANDROID_HOME和JAVA_HOME没设置或者路径指向了不存在的位置。如果你遇到类似问题直接在系统环境变量里把ANDROID_HOME指向SDK路径把JAVA_HOME指向VS自带的OpenJDK路径就行。2.2 工程配置的几个关键文件工程创建好之后第一件事就是打开.csproj和AndroidManifest.xml确认配置。Xamarin.Android项目的.csproj里这几个属性我建议直接ctrlF找到改掉PropertyGroup TargetFrameworknet8.0-android/TargetFramework SupportedOSPlatformVersion26/SupportedOSPlatformVersion RuntimeIdentifiersandroid-arm;android-arm64;android-x86/RuntimeIdentifiers /PropertyGroupRuntimeIdentifiers这一段特别重要默认配置可能只打arm架构。现在市面上的Android平板有arm有x86尤其是模拟器和部分国产盒子是x86的如果你只编arm64拿x86设备一装就提示“无法安装”或者运行崩溃。我把三种架构都加上虽然apk体积会大一些但兼容性明显更好。AndroidManifest.xml里面用于生产的权限我控制在最小范围相机、存储读写、网络、定位。这里给新手一个提醒Android 6.0以上系统危险权限需要在代码中动态申请光在Manifest里声明不够。如果你漏了动态申请运行时会直接闪退或者功能静默失效这个问题在后面的常见问题里我再详细说。2.3 使用真机调试还是模拟器模拟器我强烈建议用真机尤其是涉及摄像头、GPS、存储分区的功能模拟器的行为跟真机差异很大。我调试用的是两台设备一台是华为的10.8寸平板另一台是杂牌8寸国产平板一台高分辨率一台低分辨率用来专门验证Pad适配。开启开发者模式和USB调试的流程跟Android原生开发一模一样连上之后在VS的调试目标里选择设备即可。如果VS识别不到设备先检查是否安装了USB驱动再检查设备端的“USB调试”授权弹窗有没有点允许。我在实际项目中遇到过好几次设备连上没反应最后发现是USB线只支持充电不支持数据传输所以别急着怀疑SDK问题先看线材。3. 核心功能实现与Pad适配实战3.1 平板双栏布局用C#实现Master-Detail界面这个项目的Pad端界面我用的是双栏结构左侧是工单列表右侧是详情和操作区这种布局在10寸平板上体验很好比手机端的逐页跳转高效得多。实现方式上我用的是Fragment动态替换左侧列表Fragment和右侧详情Fragment在一个Activity里共存。当屏幕宽度大于600dp时左侧列表和右侧详情同时显示当屏幕宽度小于600dp时只显示列表Fragment点击后再单独显示详情Fragment。这个判断C#代码可以这样写var metrics Resources.DisplayMetrics; if (metrics.WidthPixels / metrics.Density 600) { // 双栏模式 FragmentTransaction tx FragmentManager.BeginTransaction(); tx.Add(Resource.Id.left_container, new ListFragment()); tx.Add(Resource.Id.right_container, new DetailFragment()); tx.Commit(); } else { // 单栏模式 FragmentTransaction tx FragmentManager.BeginTransaction(); tx.Add(Resource.Id.main_container, new ListFragment()); tx.Commit(); }为什么用600dp作为断点因为Android官方Material Design对“最小宽度”的限定是600dp凡是超过600dp的设备基本就是平板或大屏折叠设备。如果你用的是sw600dp资源限定符原理相同。我这里动态判断是为了在代码里统一控件的显示状态比如双栏模式下隐藏详情页的返回按钮。3.2 列表加载与图片上传C#中如何调用Android原生组件工单列表使用的是RecyclerViewXamarin.Android里对应的类就叫AndroidX.RecyclerView.Widget.RecyclerView。它的适配器要继承RecyclerView.Adapter这个跟Java写法是一一对应的。我封装了一个通用的ViewHolder模式避免每次创建列表都重复写模板代码。拍照上传这个功能我选的是调用系统相机Intent的方式这样不用自己写Camera2的复杂逻辑Intent intent new Intent(MediaStore.ActionImageCapture); var file new Java.IO.File(GetExternalFilesDir(null), $photo_{DateTime.Now:yyyyMMdd_HHmmss}.jpg); var uri FileProvider.GetUriForFile(this, PackageName .fileprovider, file); intent.PutExtra(MediaStore.ExtraOutput, uri); StartActivityForResult(intent, REQUEST_IMAGE_CAPTURE);这里有个关键的坑Android 7.0以上不能直接用file:// Uri来传图片给相机应用否则会扔出FileUriExposedException。解决办法就是代码里的FileProvider.GetUriForFile。FileProvider需要在Manifest里注册一个provider节点同时配置一个file_paths.xml来声明可访问的路径范围这一步我会在下面的常见问题里展开。3.3 本地存储与离线缓存策略现场环境网络不稳定工单数据必须支持离线缓存。我选择了SQLiteXamarin.Android里可以用Mono.Data.Sqlite或者SQLite.Net-PCL这个NuGet包。我推荐SQLite.Net-PCL它封装得比较顺手支持Linq查询跟C#开发者的习惯非常匹配。考虑到数据安全我在写入的时候做了两个操作一是写业务核心数据用事务二是把照片文件先缓存到应用私有目录等网络恢复后再上传。为什么不用公共目录因为Android 10以后的分区存储机制对公共目录的访问限制越来越严把文件放到自己应用私有目录是兼容性最好、也最不容易出权限问题的方法。同步逻辑我用了一个简单的队列新增或修改的工单先更新SQLite然后往同步队列里塞一条记录。网络状态变化时触发同步任务依次取出队列中的记录上传照片和表单数据成功后删除队列记录。如果网络断了任务自动重试失败原因写入日志表。3.4 后台线程、异步与UI更新的正确姿势C#写Android很容易让人顺手就把异步玩脱了最大的坑就是在子线程里直接更新UI控件。举个例子从服务器拉取工单列表后你用Task.Run去做IO然后在任务里设置RecyclerView的Adapter并NotifyDataSetChanged这时候一定会抛异常CalledFromWrongThreadException。正确的做法是异步任务完成后回到UI线程再更新控件。C#这里其实比Java方便因为async/await帮你处理了大部分上下文切换private async Task LoadOrdersAsync() { var list await Task.Run(() _orderService.GetOrdersFromApi()); _adapter.Update(list); _adapter.NotifyDataSetChanged(); }上面的代码中await回来后默认会捕获同步上下文直接回到UI线程执行后面的代码。但如果你在某些库中关闭了PostAsync/ConfigureAwait(false)那就需要注意上下文丢失。对于Android开发我的经验是不要在UI层代码里用ConfigureAwait(false)老老实实回来更新UI就行。4. 常见问题与排查技巧实录4.1 FileProvider配置不当导致的“文件无法访问”有一个问题在我实机测试各个Android版本时反复出现拍照功能在Android 6和7上正常在Android 11上点击拍照就直接闪退。这里有两个原因要叠在一起看。第一Android 7.0以上的FileUriExposedException不少开发者已经知道要加FileProvider但file_paths.xml配置的路径范围和实际保存路径不一致导致FileProvider返回的Uri是null相机拿到一个空Uri直接崩。我的解决方案是在file_paths.xml中同时声明files-path和external-files-pathpaths external-files-path nameapp_storage path. / files-path nameinternal_storage path. / /paths然后在代码里统一使用GetExternalFilesDir(null)作为照片目录既保证了相机有权限写文件也给后续上传文件提供了清晰路径。第二Android 11开始强制执行分区存储应用读写自己私有目录不需要额外权限但如果你想让系统相册也能看到这张照片你得用MediaStore去搞一条记录。我在项目里就不做这个操作了因为照片本来就不是给用户相册看的是上传到服务端的没必要增加复杂度。4.2 Thread.Abort已经被废弃你还在用吗搜“C#查询线程并中止线程”的人很多我在项目里也遇到过类似需求一个同步任务执行时间太长用户点击取消按钮要求把这个线程杀掉。这个思路最大的问题在于Thread.Abort()在.NET Core/.NET 5上调用会直接抛出PlatformNotSupportedException就算在老版本能跑也在任意一个地方强行中断线程可能导致数据状态不完整、SQLite文件被写坏。正确做法是使用CancellationToken。网络请求、数据库写入、循环遍历这些操作都应该在适当位置检查令牌状态var cts new CancellationTokenSource(); try { await Task.Run(() SyncData(cts.Token), cts.Token); } catch (OperationCanceledException) { Log.Info(Sync, 用户取消了同步); }在SyncData方法内部循环每处理一条记录就检查一次cts.Token.ThrowIfCancellationRequested()。这样取消是协作式的不会在文件写入中途强行打断数据完整性有保障。4.3 处理content:// Uri访问数据目录报错项目调试时我频繁遇到一个问题使用系统文件选择器返回一个content://开头的Uri代码里却用文件路径去读自然报错。比如热词里看到的content://com.baidu.searchbox.fileprovider这种这是其他应用通过FileProvider暴露出来的Uri除了使用ContentResolver读取外正则路径或者是拼路径的方式统统是死路。我的排查步骤很简单先判断Uri的Scheme如果是content就用ContentResolver打开输入流如果是file才能用File类操作if (uri.Scheme content) { var stream ContentResolver.OpenInputStream(uri); // 读取流并使用 } else if (uri.Scheme file) { var file new Java.IO.File(uri.Path); // 直接操作文件 }这个判断套路非常实用尤其是面对第三方App分享文件、拍照返回数据、文件选择器返回结果这些场景几乎可以解决90%的Uri访问问题。4.4 P/Invoke调用C Dll报AccessViolationException虽然项目主体是C#写的但处理某些硬件设备时还是绕不开调用底层C/C动态库。过程中我遇到了System.AccessViolationException提示Attempted to read or write protected memory。排查下来基本都是两个原因一个是结构体布局不对另一个是调用约定写错了。C#默认的调用约定是Winapi但Android上编译的C库通常使用Cdecl如果DllImport里没声明CallingConvention CallingConvention.Cdecl就会导致栈平衡错误。另外结构体字段的排列、字节对齐、字符串编码都需要和C那头严格对应。我建议在结构体上加上StructLayout(LayoutKind.Sequential, Pack 1)但前提是C侧也定义好了对齐方式否则同样会踩坑。这个坑的位置很隐蔽一次进程崩溃往往排查半天才发现是字符串编码问题。C那头是char*对应C#要用string时记得在MarshalAs里标注UnmanagedType.LPStr默认的LPWStr在Android上并不适用。4.5 C#语言层的小坑截取字符串、引用类型参数热度词里“C#语言怎样截取字符串”“C#引用类型参数”被反复搜说明这是C#新手最容易犯错的点。截取字符串这块Substring的IndexOf和LastIndexOf一定要搞清楚是从字符串开头还是从末尾开始搜索的。我项目里截取文件名时的写法是var fileName path.Substring(path.LastIndexOf(/) 1);用LastIndexOf从右侧找最后一个斜杠再截取后面的部分。如果对这个API不熟用Split(/)再取数组最后一项也可以但要注意空字符串的情况Split之后要过滤。引用类型参数这个点很多已经写过一阵C#的人也会犯错方法内修改引用类型对象的字段会直接影响到外部对象因为传进去的是对象的引用不是对象的副本。如果你想让外部对象不被修改就得显式克隆或者使用不可变类型。这个理解在写业务代码的时候极其重要尤其是传实体对象给服务层再落库的流程一个不留神就把没校验的数据写进去了。4.6 打包安装包从APK到AAB的发布流程项目做完要交付给甲方我用的是Visual Studio的Archive功能生成Release包。这里提醒大家Xamarin.Android项目在Release模式下默认启用共享运行时移除、AOT编译和捆绑程序集这些选项都要保持开启编译出来的包性能更好体积也更小。第一次发布时我直接在VS里生成了APK给客户装结果客户装不上。排查下来发现是签名问题Android要求应用必须使用正式签名才能安装到正常设备调试签名只能跑调试版本。解决方案很简单在VS的项目属性里配置Android Package Signing创建一个新的keystore填好别名、密码Release发布强制使用这个签名文件。如果你要上Google Play现在必须使用Android App BundleAAB格式。Xamarin.Android的构建目标直接选择“Bundle”即可生成。国内应用商店不一定认AAB所以国内直接发APK海外发AAB。WinForm制作安装包的经验在这里基本用不上Android这里的安装包就是一个签名好的APK/AAB没有类似于InstallShield的安装引导界面。别把PC端打包的经验直接搬过来这是两个完全不同的生态。5. 常用类库、性能调优与顺手工具5.1 我用到的几个高质量NuGet包项目里我用的包数量不多坚持“够用就好”的原则NuGet包用途理由SQLite-net-pcl本地数据库存储支持Linq、API简洁、文档丰富Newtonsoft.JsonJSON序列化/反序列化生态最成熟几乎无兼容性问题Xamarin.Essentials设备信息、网络状态、文件系统微软官方组件跨平台API统一Hjg.Pngcs图片处理辅助批量压缩时比系统API更可控有一个经验不要什么功能都往项目里塞NuGet包。我在开发初期装了整整20多个包结果编译时间暴涨启动速度下降后来顺手精简掉了其中一多半。对于业务型App核心框架类库加两三个工具包就够了。5.2 启动速度优化与内存管理Pad设备配置一般不如旗舰手机启动优化非常关键。我的优化手段主要是这几种第一开启Xamarin.Android的AOT编译把IL直接编译成目标机器码虽然会增加APK体积但启动速度提升明显。实测在国产平板上AOT编译后的启动Token量减少了一半以上。第二减少启动时初始化的全局服务。不要在Application.OnCreate里面做复杂IO把网络连接、数据库迁移这些任务改为延迟初始化等界面出现后再异步触发。第三避免内存泄漏。重点检查两处一是有静态引用指向Activity或View二是事件订阅了却没有取消订阅。C#这里特别提醒一下委托和事件是托管的但一旦涉及原生Java对象绑定GC的回收时机可能比你预想的晚。在Fragment的OnDestroy里取消订阅是最保险的习惯。5.3 调试真机日志抓取技巧在实机调试时Android日志是唯一能看清崩溃真相的朋友。VS自带的Logcat面板可以直接看设备日志筛选Tag如果太杂就按进程名过滤。我最常用的过滤方式是adb logcat -s AndroidRuntime:E XamarinException:E崩溃分析的时候先看AndroidRuntime异常再看是否有Managed exception两者分别代表原生系统异常和C#托管异常。如果日志里出现SIGSEGV大概率就是P/Invoke调用本地库出了问题十有八九是内存访问越界可以直接检查DllImport的签名。5.4 顺手推荐的两个高效辅助工具这个项目开发中我有两个每天必用的工具。第一个是Fiddler或Charles用来抓包的。开发期调试接口对接时电脑上开Fiddler然后Android设备的HTTP代理指向电脑IP:8888所有请求和响应一目了然比断点调试外网请求高效得多。注意在Android 7以上应用默认不信任用户证书需要把证书加入系统证书或使用NetworkSecurityConfig声明信任项目里的网络请求是http和https混合的所以我把某些域名的cleartextTrafficPermitted设为了true不然连本地测试环境都连不上。第二个是Scrcpy一个可以把Android屏幕投到电脑并反向控制鼠标键盘的工具。做演示、录操作视频、真机调试时这东西太稳了比我直接用Vysor或者手机自带的投屏软件流畅得多。6. 源码包的模块对照与后续扩展建议6.1 源码包内容导览我把整个项目的源码整理成了可以直接打开的解决方案结构如下目录/文件说明Tms.Android/主项目包含Activity、Fragment、AdapterTms.Core/业务逻辑层与平台无关的C#代码Tms.Data/SQLite访问、网络同步、文件存储docs/环境部署.md从零搭建环境的完整步骤docs/常见问题.md打包了本文描述的坑和排查方案build.sh / build.bat一键构建并签名脚本我建议拿到源码先按docs/环境部署.md把开发环境过一遍然后再打开Tms.Android项目编译。官方文档的步骤顺序是经过验证的不要跳步骤尤其是SDK、JDK版本和.NET Android工作负载这三项错了就会在编译阶段卡住。6.2 哪些业务场景适合直接套用这套方案这套架构适合的业务类型很清晰数据采集、工单流转、现场拍照、离线数据同步、扫码操作。后来有读者拿着这套代码去做了“设备巡检记录App”还有人改成了“装修现场验收工具”基本都能塞进这个骨架里。如果你需要接蓝牙打印机、串口设备或USB外设C#也能做但要用到Android Native接口绑定或使用Xamarin的绑定库工作量会明显增加。如果只是简单调用硬件厂商提供的SDK且厂商SDK只有Java版本那就通过Xamarin.Android绑定Java库的方式来做方式也比较成熟。6.3 关于后续迁移到.NET MAUI如果你还没正式启动项目可以再等一等再评估.NET MAUI。MAUI是Xamarin.Forms的继任者理论上支持一次编写多端运行但在Android平台上的第三方组件生态还在完善中稳定性和资料数量都不如Xamarin.Android。项目既定任务很紧的话就不要在MAUI上冒险了。从源码包的演进方向上目前Xamarin.Android项目的核心类库可以通过小幅改动迁移到.NET MAUI因为业务逻辑层和数据层本来就是跨平台的.NET Standard库界面层需要重写一半左右。我给客户交付时在文档里写清楚了这条演进路径方便后续接手的人心里有数。结语说点实际的这套方案的最后一块拼图是解决“维护的人会不会用C#写Android”这个现实问题。项目交付了后续接手的开发仍然是公司的.NET团队他们不用重新学一门语言只需要补一补Android生命周期和四大组件的概念就能在前人的代码上做修改这对企业的长期维护成本来说至关重要。如果你也是C#背景、突然被要求做一个Android端项目我希望这套源码和这篇实战总结能减轻你从零摸路的痛苦。最后再啰嗦一句拿到源码先改包名、先换keystore、先跑通一遍测试流程再开始在自己业务的基础上动刀你会少踩一半的坑。本文还有配套的精品资源点击获取