Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端
本文同步更新地址:
阅读导航:
- 一、功能说明
- 二、代码实现
- 三、源码获取
- 四、参考资料
- 五、后面计划
一、功能说明
完整思维导图:https://github.com/dotnet9/TerminalMACS/blob/master/docs/TerminalMACS.xmind
本文介绍图中右侧画红圈处的功能,即使用Xamarin.Forms获取和展示Android和iOS的通讯录信息,下面是最终效果,由于使用的是真实手机,所以联系人姓名及电话号码打码显示。
并简单的进行了搜索功能处理,之所以说简单,是因为通讯录列表是全部读取出来了,搜索是直接从此列表进行过滤的。
下图来自:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/, 本功能是参考此文所写,所以直接引用文中的图片。
二、代码实现
1、共享库工程创建联系人实体类:Contacts.cs
namespace TerminalMACS.Clients.App.Models
{
/// <summary>
/// 通讯录
/// </summary>
public class Contact
{
/// <summary>
/// 获取或者设置名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 获取或者设置 头像
/// </summary>
public string Image { get; set; }
/// <summary>
/// 获取或者设置 邮箱地址
/// </summary>
public string[] Emails { get; set; }
/// <summary>
/// 获取或者设置 手机号码
/// </summary>
public string[] PhoneNumbers { get; set; }
}
}
2、共享库创建通讯录服务接口:IContactsService.cs
包括:
- 一个通讯录获取请求接口:RetrieveContactsAsync
- 一个读取一条通讯结果通知事件:OnContactLoaded
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
namespace TerminalMACS.Clients.App.Services
{
/// <summary>
/// 通讯录事件参数
/// </summary>
public class ContactEventArgs:EventArgs
{
public Contact Contact { get; }
public ContactEventArgs(Contact contact)
{
Contact = contact;
}
}
/// <summary>
/// 通讯录服务接口,android和iOS终端具体的通讯录获取服务需要继承此接口
/// </summary>
public interface IContactsService
{
/// <summary>
/// 读取一条数据通知
/// </summary>
event EventHandler<ContactEventArgs> OnContactLoaded;
/// <summary>
/// 是否正在加载
/// </summary>
bool IsLoading { get; }
/// <summary>
/// 尝试获取所有通讯录
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? token = null);
}
}
3、iOS工程中添加通讯录服务,实现IContactsService接口:
using Contacts;
using Foundation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;
namespace TerminalMACS.Clients.App.iOS.Services
{
/// <summary>
/// 通讯录获取服务
/// </summary>
public class ContactsService : NSObject, IContactsService
{
const string ThumbnailPrefix = "thumb";
bool requestStop = false;
public event EventHandler<ContactEventArgs> OnContactLoaded;
bool _isLoading = false;
public bool IsLoading => _isLoading;
/// <summary>
/// 异步请求权限
/// </summary>
/// <returns></returns>
public async Task<bool> RequestPermissionAsync()
{
var status = CNContactStore.GetAuthorizationStatus(CNEntityType.Contacts);
Tuple<bool, NSError> authotization = new Tuple<bool, NSError>(status == CNAuthorizationStatus.Authorized, null);
if (status == CNAuthorizationStatus.NotDetermined)
{
using (var store = new CNContactStore())
{
authotization = await store.RequestAccessAsync(CNEntityType.Contacts);
}
}
return authotization.Item1;
}
/// <summary>
/// 异步请求通讯录,此方法由界面真正调用
/// </summary>
/// <param name="cancelToken"></param>
/// <returns></returns>
public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null)
{
requestStop = false;
if (!cancelToken.HasValue)
cancelToken = CancellationToken.None;
// 我们创建了一个十进制的TaskCompletionSource
var taskCompletionSource = new TaskCompletionSource<IList<Contact>>();
// 在cancellationToken中注册lambda
cancelToken.Value.Register(() =>
{
// 我们收到一条取消消息,取消TaskCompletionSource.Task
requestStop = true;
taskCompletionSource.TrySetCanceled();
});
_isLoading = true;
var task = LoadContactsAsync();
// 等待两个任务中的第一个任务完成
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
_isLoading = false;
return await completedTask;
}
/// <summary>
/// 异步加载通讯录,具体的通讯录读取方法
/// </summary>
/// <returns></returns>
async Task<IList<Contact>> LoadContactsAsync()
{
IList<Contact> contacts = new List<Contact>();
var hasPermission = await RequestPermissionAsync();
if (hasPermission)
{
NSError error = null;
var keysToFetch = new[] { CNContactKey.PhoneNumbers, CNContactKey.GivenName, CNContactKey.FamilyName, CNContactKey.EmailAddresses, CNContactKey.ImageDataAvailable, CNContactKey.ThumbnailImageData };
var request = new CNContactFetchRequest(keysToFetch: keysToFetch);
request.SortOrder = CNContactSortOrder.GivenName;
using (var store = new CNContactStore())
{
var result = store.EnumerateContacts(request, out error, new CNContactStoreListContactsHandler((CNContact c, ref bool stop) =>
{
string path = null;
if (c.ImageDataAvailable)
{
path = path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}");
if (!File.Exists(path))
{
var imageData = c.ThumbnailImageData;
imageData?.Save(path, true);
}
}
var contact = new Contact()
{
Name = string.IsNullOrEmpty(c.FamilyName) ? c.GivenName : $"{c.GivenName} {c.FamilyName}",
Image = path,
PhoneNumbers = c.PhoneNumbers?.Select(p => p?.Value?.StringValue).ToArray(),
Emails = c.EmailAddresses?.Select(p => p?.Value?.ToString()).ToArray(),
};
if (!string.IsNullOrWhiteSpace(contact.Name))
{
OnContactLoaded?.Invoke(this, new ContactEventArgs(contact));
contacts.Add(contact);
}
stop = requestStop;
}));
}
}
return contacts;
}
}
}
4、在iOS工程中的Info.plist文件添加通讯录权限使用说明
5、在Android工程中添加读取通讯录权限配置:AndroidManifest.xml
<uses-permission android:name="android.permission.READ_CONTACTS"/>
完整权限配置如下
<?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.terminalmacs.clients.app">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="TerminalMACS.Clients.App.Android"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
6、在Android工程中添加通讯录服务,实现IContactServer接口:ContactsService.cs
using Acr.UserDialogs;
using Android;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Database;
using Android.Provider;
using Android.Runtime;
using Android.Support.V4.App;
using Plugin.CurrentActivity;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;
namespace TerminalMACS.Clients.App.Droid.Services
{
/// <summary>
/// 通讯录获取服务
/// </summary>
public class ContactsService : IContactsService
{
const string ThumbnailPrefix = "thumb";
bool stopLoad = false;
static TaskCompletionSource<bool> contactPermissionTcs;
public string TAG
{
get
{
return "MainActivity";
}
}
bool _isLoading = false;
public bool IsLoading => _isLoading;
//权限请求状态码
public const int RequestContacts = 1239;
/// <summary>
/// 获取通讯录需要的请求权限
/// </summary>
static string[] PermissionsContact = {
Manifest.Permission.ReadContacts
};
public event EventHandler<ContactEventArgs> OnContactLoaded;
/// <summary>
/// 异步请求通讯录权限
/// </summary>
async void RequestContactsPermissions()
{
//检查是否可以弹出申请读、写通讯录权限
if (ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts)
|| ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts))
{
// 如果未授予许可,请向用户提供其他理由用户将从使用权限的附加上下文中受益。
// 例如,如果请求先前被拒绝。
await UserDialogs.Instance.AlertAsync("通讯录权限", "此操作需要“通讯录”权限", "确定");
}
else
{
// 尚未授予通讯录权限。直接请求这些权限。
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, PermissionsContact, RequestContacts);
}
}
/// <summary>
/// 收到用户响应请求权限操作后的结果
/// </summary>
/// <param name="requestCode"></param>
/// <param name="permissions"></param>
/// <param name="grantResults"></param>
public static void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
if (requestCode == RequestContacts)
{
// 我们请求了多个通讯录权限,因此需要检查相关的所有权限
if (PermissionUtil.VerifyPermissions(grantResults))
{
// 已授予所有必需的权限,显示联系人片段。
contactPermissionTcs.TrySetResult(true);
}
else
{
contactPermissionTcs.TrySetResult(false);
}
}
}
/// <summary>
/// 异步请求权限
/// </summary>
/// <returns></returns>
public async Task<bool> RequestPermissionAsync()
{
contactPermissionTcs = new TaskCompletionSource<bool>();
// 验证是否已授予所有必需的通讯录权限。
if (Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts) != (int)Permission.Granted
|| Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts) != (int)Permission.Granted)
{
// 尚未授予通讯录权限。
RequestContactsPermissions();
}
else
{
// 已授予通讯录权限。
contactPermissionTcs.TrySetResult(true);
}
return await contactPermissionTcs.Task;
}
/// <summary>
/// 异步请求通讯录,此方法由界面真正调用
/// </summary>
/// <param name="cancelToken"></param>
/// <returns></returns>
public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null)
{
stopLoad = false;
if (!cancelToken.HasValue)
cancelToken = CancellationToken.None;
// 我们创建了一个十进制的TaskCompletionSource
var taskCompletionSource = new TaskCompletionSource<IList<Contact>>();
// 在cancellationToken中注册lambda
cancelToken.Value.Register(() =>
{
// 我们收到一条取消消息,取消TaskCompletionSource.Task
stopLoad = true;
taskCompletionSource.TrySetCanceled();
});
_isLoading = true;
var task = LoadContactsAsync();
// 等待两个任务中的第一个任务完成
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
_isLoading = false;
return await completedTask;
}
/// <summary>
/// 异步加载通讯录,具体的通讯录读取方法
/// </summary>
/// <returns></returns>
async Task<IList<Contact>> LoadContactsAsync()
{
IList<Contact> contacts = new List<Contact>();
var hasPermission = await RequestPermissionAsync();
if (!hasPermission)
{
return contacts;
}
var uri = ContactsContract.Contacts.ContentUri;
var ctx = Application.Context;
await Task.Run(() =>
{
// 暂时只请求通讯录Id、DisplayName、PhotoThumbnailUri,可以扩展
var cursor = ctx.ApplicationContext.ContentResolver.Query(uri, new string[]
{
ContactsContract.Contacts.InterfaceConsts.Id,
ContactsContract.Contacts.InterfaceConsts.DisplayName,
ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri
}, null, null, $"{ContactsContract.Contacts.InterfaceConsts.DisplayName} ASC");
if (cursor.Count > 0)
{
while (cursor.MoveToNext())
{
var contact = CreateContact(cursor, ctx);
if (!string.IsNullOrWhiteSpace(contact.Name))
{
// 读取出一条,即通知界面展示
OnContactLoaded?.Invoke(this, new ContactEventArgs(contact));
contacts.Add(contact);
}
if (stopLoad)
break;
}
}
});
return contacts;
}
/// <summary>
/// 读取一条通讯录数据
/// </summary>
/// <param name="cursor"></param>
/// <param name="ctx"></param>
/// <returns></returns>
Contact CreateContact(ICursor cursor, Context ctx)
{
var contactId = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.Id);
var numbers = GetNumbers(ctx, contactId);
var emails = GetEmails(ctx, contactId);
var uri = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri);
string path = null;
if (!string.IsNullOrEmpty(uri))
{
try
{
using (var stream = Android.App.Application.Context.ContentResolver.OpenInputStream(Android.Net.Uri.Parse(uri)))
{
path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}");
using (var fstream = new FileStream(path, FileMode.Create))
{
stream.CopyTo(fstream);
fstream.Close();
}
stream.Close();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
}
var contact = new Contact
{
Name = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.DisplayName),
Emails = emails,
Image = path,
PhoneNumbers = numbers,
};
return contact;
}
/// <summary>
/// 读取联系人电话号码
/// </summary>
/// <param name="ctx"></param>
/// <param name="contactId"></param>
/// <returns></returns>
string[] GetNumbers(Context ctx, string contactId)
{
var key = ContactsContract.CommonDataKinds.Phone.Number;
var cursor = ctx.ApplicationContext.ContentResolver.Query(
ContactsContract.CommonDataKinds.Phone.ContentUri,
null,
ContactsContract.CommonDataKinds.Phone.InterfaceConsts.ContactId + " = ?",
new[] { contactId },
null
);
return ReadCursorItems(cursor, key)?.ToArray();
}
/// <summary>
/// 读取联系人邮箱地址
/// </summary>
/// <param name="ctx"></param>
/// <param name="contactId"></param>
/// <returns></returns>
string[] GetEmails(Context ctx, string contactId)
{
var key = ContactsContract.CommonDataKinds.Email.InterfaceConsts.Data;
var cursor = ctx.ApplicationContext.ContentResolver.Query(
ContactsContract.CommonDataKinds.Email.ContentUri,
null,
ContactsContract.CommonDataKinds.Email.InterfaceConsts.ContactId + " = ?",
new[] { contactId },
null);
return ReadCursorItems(cursor, key)?.ToArray();
}
IEnumerable<string> ReadCursorItems(ICursor cursor, string key)
{
while (cursor.MoveToNext())
{
var value = GetString(cursor, key);
yield return value;
}
cursor.Close();
}
string GetString(ICursor cursor, string key)
{
return cursor.GetString(cursor.GetColumnIndex(key));
}
}
}
需要添加 Plugin.CurrentActivity 和 Acr.UserDialogs 包。
7、Android工程添加权限处理判断类
Permission.Util
using Android.Content.PM;
namespace TerminalMACS.Clients.App.Droid
{
public static class PermissionUtil
{
/**
* 通过验证给定数组中的每个条目的值是否为Permission.Granted,检查是否已授予所有给定权限。
*
* See Activity#onRequestPermissionsResult (int, String[], int[])
*/
public static bool VerifyPermissions(Permission[] grantResults)
{
// 必须至少检查一个结果.
if (grantResults.Length < 1)
return false;
// 验证是否已授予每个必需的权限,否则返回false.
foreach (Permission result in grantResults)
{
if (result != Permission.Granted)
{
return false;
}
}
return true;
}
}
}
MainActivity.OnRequestPermissionResult是权限申请结果处理函数,在此函数中调用ContactsService.OnRequestPermissionsResult通知通讯录服务权限处理结果。
MainActivity.cs
using Acr.UserDialogs;
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using TerminalMACS.Clients.App.Droid.Services;
using TerminalMACS.Clients.App.Services;
namespace TerminalMACS.Clients.App.Droid
{
[Activity(Label = "TerminalMACS.Clients.App", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
IContactsService contactsService = new ContactsService();
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
UserDialogs.Init(() => this);
// 将通讯录服务实例传递给共享库,由共享库使用读取通讯录接口
LoadApplication(new App(contactsService));
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
// 通讯录服务处理权限请求结果
ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
8、创建通讯录ViewModel,并使用通讯录服务
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;
using Xamarin.Forms;
namespace TerminalMACS.Clients.App.ViewModels
{
/// <summary>
/// 通讯录ViewModel
/// </summary>
public class ContactViewModel : BaseViewModel
{
/// <summary>
/// 通讯录服务接口
/// </summary>
IContactsService _contactService;
/// <summary>
/// 标题
/// </summary>
public new string Title => "通讯录";
private string _SearchText;
/// <summary>
/// 搜索关键字
/// </summary>
public string SearchText
{
get { return _SearchText; }
set
{
SetProperty(ref _SearchText, value);
}
}
/// <summary>
/// 通讯录搜索命令
/// </summary>
public ICommand RaiseSearchCommand { get; }
/// <summary>
/// 通讯录列表
/// </summary>
public ObservableCollection<Contact> Contacts { get; set; }
private List<Contact> _FilteredContacts;
/// <summary>
/// 通讯录过滤列表
/// </summary>
public List<Contact> FilteredContacts
{
get { return _FilteredContacts; }
set
{
SetProperty(ref _FilteredContacts, value);
}
}
public ContactViewModel(IContactsService contactService)
{
_contactService = contactService;
Contacts = new ObservableCollection<Contact>();
Xamarin.Forms.BindingBase.EnableCollectionSynchronization(Contacts, null, ObservableCollectionCallback);
_contactService.OnContactLoaded += OnContactLoaded;
LoadContacts();
RaiseSearchCommand = new Command(RaiseSearchHandle);
}
/// <summary>
/// 过滤通讯录
/// </summary>
void RaiseSearchHandle()
{
if (string.IsNullOrEmpty(SearchText))
{
FilteredContacts = Contacts.ToList();
return;
}
Func<Contact, bool> checkContact = (s) =>
{
if (!string.IsNullOrWhiteSpace(s.Name) && s.Name.ToLower().Contains(SearchText.ToLower()))
{
return true;
}
else if (s.PhoneNumbers.Length > 0 && s.PhoneNumbers.ToList().Exists(cu => cu.ToString().Contains(SearchText)))
{
return true;
}
return false;
};
FilteredContacts = Contacts.ToList().Where(checkContact).ToList();
}
/// <summary>
/// BindingBase.EnableCollectionSynchronization 为集合启用跨线程更新
/// </summary>
/// <param name="collection"></param>
/// <param name="context"></param>
/// <param name="accessMethod"></param>
/// <param name="writeAccess"></param>
void ObservableCollectionCallback(IEnumerable collection, object context, Action accessMethod, bool writeAccess)
{
// `lock` ensures that only one thread access the collection at a time
lock (collection)
{
accessMethod?.Invoke();
}
}
/// <summary>
/// 收到事件通知,读取一条通讯录信息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnContactLoaded(object sender, ContactEventArgs e)
{
Contacts.Add(e.Contact);
RaiseSearchHandle();
}
/// <summary>
/// 异步读取终端通讯录
/// </summary>
/// <returns></returns>
async Task LoadContacts()
{
try
{
await _contactService.RetrieveContactsAsync();
}
catch (TaskCanceledException)
{
Console.WriteLine("任务已经取消");
}
}
}
}
9、添加通讯录页面展示通讯录数据
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
mc:Ignorable="d"
Title="{Binding Title}"
x:Class="TerminalMACS.Clients.App.Views.ContactPage"
ios:Page.UseSafeArea="true">
<ContentPage.Content>
<StackLayout>
<SearchBar x:Name="filterText"
HeightRequest="40"
Text="{Binding SearchText}"
SearchCommand="{Binding RaiseSearchCommand}"/>
<ListView ItemsSource="{Binding FilteredContacts}"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="10"
Orientation="Horizontal">
<Image Source="{Binding Image}"
VerticalOptions="Center"
x:Name="image"
Aspect="AspectFit"
HeightRequest="60"/>
<StackLayout VerticalOptions="Center">
<Label Text="{Binding Name}"
FontAttributes="Bold"/>
<Label Text="{Binding PhoneNumbers[0]}"/>
<Label Text="{Binding Emails[0]}"/>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
三、源码获取
-
2.Android客户端可成功取得通讯录数据,并可查询;
已编译的Android客户端:https://terminalmacs.com/terminalmacs-clients-app-android
- 3.iOS读取通讯录功能代码也已添加,但由于本人没有iOS测试环境,所以未验证,有条件的朋友可以测试下iOS的通讯录读取功能,如果代码不起作用,可参考本文参考的文章检查iOS代码。
四、参考资料
Getting phone contacts in Xamarin Forms:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/
参考文章末尾有源代码链接。
五、后面计划
Xamarin.Forms客户端基本信息获取,比如IMEI、IMSI、本机号码、Mac地址等。