记得我刚接触Android开发的时候,那种兴奋劲儿到现在还记得。那时候屏幕上只有一行”Hello World”,但我感觉自己像是掌握了某种魔法。时光流转,现在我在用Mono(其实是.NET/Mono在移动端的分支)来写Android应用,这种跨平台的体验既让人惊喜,也偶尔让人抓狂。今天,我想跟你聊聊这段旅程——从最基础的”Hello World”开始,一路走到真正的跨平台应用开发,顺便把那些坑都填平。
初识Mono:你的新玩伴
首先,你得明白Mono是什么。简单来说,Mono是一个开源的.NET框架实现,它可以让你的C#代码在各种平台上运行,包括Android。当你用Mono开发Android应用时,你实际上是在用C#和Xamarin(现在叫.NET MAUI,但核心还是Mono运行时)来编写原生Android应用。
// 一个简单的Mono Android Hello World
using Android.App;
using Android.Widget;
using Android.OS;
namespace MonoAndroidApp
{
[Activity(Label = "我的第一个Mono应用", MainLauncher = true)]
public class MainActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// 创建一个简单的文本视图
var textView = new TextView(this);
textView.Text = "Hello, Android via Mono!";
textView.TextSize = 24f;
// 设置布局
SetContentView(textView);
}
}
}
这段代码看起来是不是很熟悉?如果你写过Java的Android应用,你会发现结构几乎一样。这就是Mono的魅力——它保留了Android的原生API,但让你用更熟悉的C#来写。
从Hello World到真实应用
现在,让我们把那个简单的Hello World升级成一个真正有用的应用。假设我们要做一个简单的待办事项应用,这个应用需要:
- 显示待办事项列表
- 添加新事项
- 标记完成
- 删除事项
项目结构搭建
在Visual Studio中创建一个新的Android应用项目,选择”空白应用”模板。你会看到这样的项目结构:
MonoTodoApp/
├── MonoTodoApp.Droid/ # Android特定代码
│ ├── MainActivity.cs
│ ├── Resources/
│ │ ├── layout/
│ │ └── values/
│ └── Assets/
├── MonoTodoApp/ # 共享代码(PCL或.NET Standard)
│ ├── Models/
│ ├── ViewModels/
│ └── App.xaml
└── MonoTodoApp.Tests/ # 单元测试
数据模型设计
在共享项目中,我们先定义数据模型:
using System;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
namespace MonoTodoApp.Models
{
public class TodoItem
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("isCompleted")]
public bool IsCompleted { get; set; }
[JsonProperty("createdAt")]
public DateTime CreatedAt { get; set; }
public TodoItem()
{
Id = Guid.NewGuid().ToString();
CreatedAt = DateTime.UtcNow;
}
}
public class TodoItemList : ObservableCollection<TodoItem>
{
// 自定义集合,方便后续添加过滤逻辑
public TodoItemList() : base() { }
public TodoItemList(IEnumerable<TodoItem> collection) : base(collection) { }
}
}
简单的本地存储
在Android上,我们可以使用SQLite来存储数据。先添加SQLite-net NuGet包:
using System.Collections.Generic;
using System.Linq;
using SQLite;
using MonoTodoApp.Models;
namespace MonoTodoApp.Data
{
public class TodoDatabase
{
private readonly SQLiteAsyncConnection _database;
public TodoDatabase(string dbPath)
{
_database = new SQLiteAsyncConnection(dbPath);
InitializeAsync();
}
private async Task InitializeAsync()
{
// 创建表(如果不存在)
await _database.CreateTableAsync<TodoItem>();
}
public Task<List<TodoItem>> GetItemsAsync()
{
return _database.Table<TodoItem>().ToListAsync();
}
public Task<TodoItem> GetItemAsync(string id)
{
return _database.Table<TodoItem>()
.Where(i => i.Id == id)
.FirstOrDefaultAsync();
}
public Task<int> SaveItemAsync(TodoItem item)
{
if (item.Id != null)
{
return _database.UpdateAsync(item);
}
else
{
return _database.InsertAsync(item);
}
}
public Task<int> DeleteItemAsync(TodoItem item)
{
return _database.DeleteAsync(item);
}
}
}
Android端的数据路径处理
这是Mono Android开发中一个关键的实战点:你需要正确处理Android的文件路径。
using Android.Content;
using Android.Database.Sqlite;
using Android.Provider;
using MonoTodoApp.Data;
namespace MonoTodoApp.Droid
{
public class TodoDatabaseFactory
{
public static readonly string DatabaseName = "Todo.db";
public static TodoDatabase GetDatabase(Context context)
{
// Android的数据路径处理
string dbPath = System.IO.Path.Combine(
Android.OS.Environment.ExternalStorageDirectory.ToString(),
"Todo.db");
// 或者使用内部存储(更推荐,更安全)
string internalDbPath = System.IO.Path.Combine(
context.FilesDir.ToString(),
DatabaseName);
return new TodoDatabase(internalDbPath);
}
}
}
注意:这里有个常见的陷阱。很多开发者会直接使用Environment.ExternalStorageDirectory,但在Android 10及更高版本中,访问外部存储需要特殊的权限和处理。建议使用应用内部存储路径,这样更简单也更安全。
用户界面:让应用活起来
现在我们来创建用户界面。在Android的布局文件中:
<!-- res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我的待办事项"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<ListView
android:id="@+id/todolistListView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="@+id/addButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="添加事项"
android:background="#4CAF50"
android:textColor="#FFFFFF"/>
</LinearLayout>
对应的C#代码:
using Android.App;
using Android.Widget;
using Android.OS;
using System.Collections.Generic;
using System.Linq;
using MonoTodoApp.Models;
using MonoTodoApp.Data;
namespace MonoTodoApp.Droid
{
[Activity(Label = "Todo App", MainLauncher = true)]
public class MainActivity : Activity
{
private ListView _todoListView;
private Button _addButton;
private TodoDatabase _database;
private TodoAdapter _adapter;
private List<TodoItem> _todoItems;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.activity_main);
// 初始化数据库
_database = TodoDatabaseFactory.GetDatabase(this);
// 获取UI元素
_todoListView = FindViewById<ListView>(Resource.Id.todolistListView);
_addButton = FindViewById<Button>(Resource.Id.addButton);
// 设置适配器
_todoItems = new List<TodoItem>();
_adapter = new TodoAdapter(this, _todoItems);
_todoListView.Adapter = _adapter;
// 加载数据
LoadTodoItems();
// 添加按钮点击事件
_addButton.Click += async (sender, e) =>
{
await AddNewTodoItem();
};
// 列表项点击事件
_todoListView.ItemClick += async (sender, e) =>
{
await ToggleTodoItem(e.Id);
};
}
private async void LoadTodoItems()
{
var items = await _database.GetItemsAsync();
_todoItems.Clear();
foreach (var item in items)
{
_todoItems.Add(item);
}
_adapter.NotifyDataSetChanged();
}
private async Task AddNewTodoItem()
{
var newTodo = new TodoItem
{
Title = $"待办事项 {_todoItems.Count + 1}",
Description = "这是一个新的待办事项"
};
await _database.SaveItemAsync(newTodo);
_todoItems.Add(newTodo);
_adapter.NotifyItemInserted(_todoItems.Count - 1);
}
private async Task ToggleTodoItem(int position)
{
var item = _todoItems[position];
item.IsCompleted = !item.IsCompleted;
await _database.SaveItemAsync(item);
_adapter.NotifyItemChanged(position);
}
}
}
适配器:让列表显示数据
这是Mono Android开发中一个关键的组件:
using Android.App;
using Android.Content;
using Android.Views;
using Android.Widget;
using MonoTodoApp.Models;
using System.Collections.Generic;
namespace MonoTodoApp.Droid
{
public class TodoAdapter : BaseAdapter<TodoItem>
{
private List<TodoItem> _items;
private Activity _context;
public override int Count => _items.Count;
public override TodoItem this[int position] => _items[position];
public TodoAdapter(Activity context, List<TodoItem> items)
{
_context = context;
_items = items;
}
public override long GetItemId(int position)
{
return position;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = _items[position];
var view = convertView ?? _context.LayoutInflater.Inflate(Resource.Layout.todo_item, parent, false);
var titleTextView = view.FindViewById<TextView>(Resource.Id.titleTextView);
var statusImageView = view.FindViewById<ImageView>(Resource.Id.statusImageView);
titleTextView.Text = item.Title;
// 根据完成状态设置图标
if (item.IsCompleted)
{
statusImageView.SetImageResource(Resource.Drawable.check_mark);
}
else
{
statusImageView.SetImageResource(Resource.Drawable.unchecked);
}
return view;
}
}
}
常见问题排查:那些让人头疼的时刻
好了,现在让我来分享一些在Mono Android开发中经常遇到的问题,以及如何解决它们。
问题1:Xamarin.Forms兼容性问题
很多开发者会从Xamarin.Forms开始,但可能会遇到兼容性问题:
// 在PCL或.NET Standard项目中,注意引用正确的包
// 不要混用不同版本的Xamarin.Forms包
// 正确的NuGet包引用
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
// 在Android项目中,确保MainActivity继承正确的类
[Activity(Label = "MyApp", Icon = "@mipmap/icon", Theme = "@style/MainTheme",
MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new App());
}
}
问题2:内存泄漏
Mono GC和Android的GC有时候会打架,特别是当你持有Activity引用时:
// 错误示例:在Activity中持有静态引用
public static Activity CurrentActivity;
// 正确做法:使用弱引用或确保在适当的时候释放引用
public class ActivityTracker
{
private static WeakReference<Activity> _currentActivity;
public static void SetCurrentActivity(Activity activity)
{
_currentActivity = new WeakReference<Activity>(activity);
}
public static Activity GetCurrentActivity()
{
Activity activity;
if (_currentActivity != null && _currentActivity.TryGetTarget(out activity))
{
return activity;
}
return null;
}
}
问题3:启动速度缓慢
Mono应用启动比原生Java/Kotlin应用慢,这是常见的问题:
// 优化启动速度的技巧:
// 1. 使用AOT(Ahead-of-Time)编译
// 在android项目属性中启用
// 项目属性 -> Android选项 -> 高级 -> 代码生成
// 选择"使用本地代码增强"
// 2. 预编译XAML
// 在AndroidManifest.xml中添加
// <application android:extractNativeLibs="true" ...>
// 3. 延迟加载非关键组件
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// 先显示启动画面
SetContentView(Resource.Layout.SplashScreen);
// 在后台线程加载主要数据
Task.Run(async () =>
{
await LoadInitialData();
// 切换到主界面
RunOnUiThread(() =>
{
LoadApplication(new App());
});
});
}
问题4:第三方库兼容性
有些Android库可能不兼容Mono:
// 检查库的兼容性
// 1. 查看库的GitHub页面,确认是否有Mono支持
// 2. 测试在Android模拟器上运行
// 3. 如果遇到问题,考虑使用替代库或原生封装
// 例如,某些图像处理库可能需要原生代码
// 解决方案:创建JNI桥接层
// NativeImageProcessor.cs
using Android.Runtime;
using Java.Interop;
namespace MonoApp.Native
{
[Register("com/myapp/NativeImageProcessor")]
public class NativeImageProcessor : Java.Lang.Object
{
[Register(".ctor", "()V", "")]
public NativeImageProcessor() : base()
{
}
[Register("processImage", "(Ljava/lang/String;)Ljava/lang/String;", "")]
public string ProcessImage(string imagePath)
{
// 调用原生代码处理图片
return CallNativeMethod("processImage", imagePath);
}
private string CallNativeMethod(string methodName, params object[] args)
{
// 实际的JNI调用
return "";
}
}
}
调试技巧:让你的生活更轻松
调试Mono Android应用有一些特殊的技巧:
// 1. 使用LogCat过滤特定标签
// adb logcat -s MonoTodoApp
// 2. 启用详细日志
Android.Util.Log.Debug("MonoTodoApp", "Debug message: " + someVariable);
// 3. 远程调试
// 在Visual Studio中:
// 调试 -> 附加到进程 -> 选择Android设备 -> 选择Mono运行时
// 4. 性能分析
// 使用Xamarin Profiler分析内存和CPU使用
从Hello World到生产级应用
回头看,从那个简单的”Hello World”到现在这个功能完整的待办事项应用,我们走了很长一段路。Mono在Android开发中的优势很明显:代码重用、类型安全、丰富的生态系统。但挑战也不容忽视,特别是性能优化和兼容性问题。
记住,每个问题都是学习的机会。当你遇到一个令人抓狂的bug时,深呼吸,查阅文档,或者在社区中寻求帮助。Stack Overflow和Xamarin论坛是你最好的朋友。
希望这篇文章能帮助你更好地理解Mono在Android开发中的应用。如果你有任何问题或想要分享你的经验,随时在评论区留言。开发愉快!