編輯:關於Android編程
在手機adb中運行 netcfg或者ifconfig可以看到相關的網絡接口的ip,掩碼,mac地址等信息
Wpa_supplicant為每個網絡接口都分配了一個struct wpa_supplicant, 該結構體存儲了一些必要信息例如 struct dl_list bss(掃描結果); struct wpa_config *conf(配置文件)等等。
每一個網絡接口的掃描,連接等操作都是通過struct wpa_supplicant中定義的相關函數和數據來實現的。下面是結構體中一些重要的元素介紹
struct wpa_supplicant{
//接口的名字和mac地址
unsigned char own_addr[ETH_ALEN];
char ifname[100];
/**************************************/
//外部控制wpa_s的socket
struct ctrl_iface_priv *ctrl_iface;
//網絡接口當前的狀態,斷開/關聯/連接的bssid ssid等
enum wpa_states wpa_state;
u8 bssid[ETH_ALEN];
u8 pending_bssid[ETH_ALEN]; /* If wpa_state == WPA_ASSOCIATING, this
* field contains the target BSSID. */
int reassociate; /* reassociation requested */
int disconnected; /* all connections disabled; i.e., do no reassociate
* before this has been cleared */
struct wpa_ssid *current_ssid;
struct wpa_bss *current_bss;
int ap_ies_from_associnfo;
unsigned int assoc_freq;
//是否支持2.4G/5G
enum { WPA_SETBAND_AUTO, WPA_SETBAND_5G, WPA_SETBAND_2G } setband;
/**************************************/
//配置文件
struct wpa_config *conf;
//配置文件中的ESS網絡的信息
/* Selected configuration (based on Beacon/ProbeResp WPA IE) */
int pairwise_cipher;
int group_cipher;
int key_mgmt;
int wpa_proto;
int mgmt_group_cipher;
/**************************************/
//掃描結果的處理,每次掃描後的結果都會做相應的更新
void (*scan_res_handler)(struct wpa_supplicant *wpa_s,
struct wpa_scan_results *scan_res);
struct dl_list bss; /* struct wpa_bss::list */
struct dl_list bss_id; /* struct wpa_bss::list_id */
size_t num_bss;
unsigned int bss_update_idx;
unsigned int bss_next_id;
//黑名單
struct wpa_blacklist *blacklist;
/**************************************/
//底層相關,驅動的操作函數
struct wpa_driver_ops *driver;
int interface_removed; /* whether the network interface has been removed */
void *drv_priv; /* private data used by driver_ops */
void *global_drv_priv;
//用來處理密鑰相關netlink socket和處理方法
struct l2_packet_data *l2;
struct l2_packet_data *l2_br;
/**************************************/
//掃描相關的一些信息
struct wpa_radio_work *scan_work;
int scanning;
int sched_scanning;
struct os_reltime scan_trigger_time, scan_start_time;
int scan_runs; /* number of scan runs since WPS was started */
int *next_scan_freqs;
int scan_interval; /* time in sec between scans to find suitable AP */
int normal_scans; /* normal scans run before sched_scan */
int scan_for_connection; /* whether the scan request was triggered for
* finding a connection */
//sched_scan
struct wpa_ssid *prev_sched_ssid; /* last SSID used in sched scan */
int sched_scan_timeout;
int sched_scan_interval;
int first_sched_scan;
int sched_scan_timed_out;
//pno掃描
int pno;
int pno_sched_pending;
}
初始化網絡接口配置
在wpa_s的main()函數中,進行完全局初始化後,會調用 wpa_supplicant_add_iface(),為wpa_s運行時傳入的每個網絡接口分配wpa_supplicant結構體,並進行相應的初始化
struct wpa_supplicant * wpa_supplicant_add_iface(struct wpa_global *global,
struct wpa_interface *iface)
{
struct wpa_supplicant *wpa_s = wpa_supplicant_alloc();
wpa_s->global = global;
//網絡接口的初始化,與底層通信的socket,密鑰數據包的socket,wpa_s的初始狀態等
if (wpa_supplicant_init_iface(wpa_s, &t_iface))
{
//讀取兩個conf配置文件
//wpa_s允許提供兩個配置文件,一個是原始的一個overlay,在overlay中的文件可以重新一些參數的值
wpa_s->conf = wpa_config_read(wpa_s->confname, NULL);
wpa_config_read(wpa_s->confanother, wpa_s->conf);
//初始化驅動接口,並注冊接收驅動event的函數 nl80211
if (wpas_init_driver(wpa_s, iface) < 0)
{
//根據傳進來的-inl80211選定對應的wpa_driver_ops(driver_nl80211.c),裡面定義了驅動的操作方法,同時調用對應的global_init()進行初始化
if (wpa_supplicant_set_driver(wpa_s, driver) < 0)
...
列表內容
驅動初始化
下一步是初始化相關驅動的操作參數和與驅動通信的socket
wpa_supplicant支持多種架構的驅動,Android中多使用的是nl80211架構,相關操作函數在文件drivers_ml80211.c中
//根據名稱選擇網絡ops,創建於driver通信的socket
select_driver(wpa_s, i)
{
//調用對應的global_init(),void * nl80211_global_init(void)
global->drv_priv[i] = wpa_drivers[i]->global_init();
{
//初始化netlink socket,設置兩個消息處理函數,netlink_receive中使用
cfg->newlink_cb = wpa_driver_nl80211_event_rtm_newlink;
cfg->dellink_cb = wpa_driver_nl80211_event_rtm_dellink;
//創建一個接收kernel中 route 子系統的消息socket
global->netlink = netlink_init(cfg);
{
//*****該socket用於接收 kernel中
netlink->sock = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
bind(netlink->sock, (struct sockaddr *) &local, sizeof(local));
//注冊內核消息接收函數, 根據接收到的消息type選擇以下兩個處理函數
eloop_register_read_sock(netlink->sock, netlink_receive, netlink, NULL);
static void netlink_receive(int sock, void *eloop_ctx, void *sock_ctx)
{
char buf[8192]; //接收消息的字節數,
left = recvfrom(sock, buf, sizeof(buf), MSG_DONTWAIT, (struct sockaddr *) &from, &fromlen);
//buf中是一個netlink消息,符合netlink規范,根據消息type選在相應的處理函數 newlink 和 dellink
}
}
//創建 global結構體中的 nl_cb(接收消息處理)和nl(發送消息)
if (wpa_driver_nl80211_init_nl_global(global) < 0)
{
//創建連接到 GENERIC_NETLINK 的socket
global->nl = nl_create_handle(global->nl_cb, "nl");
//將nl連接到 內核中的nl80211模塊
global->nl80211_id = genl_ctrl_resolve(global->nl, "nl80211");
//創建接收消息的event socket, 並分別加入到組播組scan mlme regulatory vendor 中
global->nl_event = nl_create_handle(global->nl_cb, "event");
ret = nl_get_multicast_id(global, "nl80211", "scan");
ret = nl_socket_add_membership(global->nl_event, ret);
//設置nl_cb的回調函數 process_global_event(), 該函數又會調用 wpa_supplicant_event() 做進一步處理
nl_cb_set(global->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, process_global_event, global);
//為nl_event注冊接收函數, 通過nl_recvmsgs()調用 process_global_event()
nl80211_register_eloop_read(&global->nl_event, wpa_driver_nl80211_event_receive, global->nl_cb);
}
//創建一個 ioctl_sock, 還不知道干啥用,該sock用於通過ioctl方式向底層發送命令
global->ioctl_sock = socket(PF_INET, SOCK_DGRAM, 0);
}
}
網絡接口初始配置
例如up對應的接口
//調用對應的初始化函數wpa_driver_nl80211_drv_init(),傳入的ifname名稱為 wlan0
wpa_s->drv_priv = wpa_drv_init(wpa_s, wpa_s->ifname);
static void * wpa_driver_nl80211_drv_init(void *ctx, const char *ifname, void *global_priv, int hostapd, const u8 *set_addr)
{
//挺重要的一個結構體
struct wpa_driver_nl80211_data *drv;
//添加了回調函數drv->nl_cb process_drv_event(),最終會調用 wpa_supplicant_event()
if (wpa_driver_nl80211_init_nl(drv))
//添加bss回調函數bss->nl_cb,看著沒啥用,最終處理 wpa_supplicant_event(),這兩個回調函數沒有和socket綁定,目前不清楚調用地方
if (nl80211_init_bss(bss))
//RF射頻省電相關,讀取節點“/dev/rfkill”只是接受消息打印log,不做任何操作
rcfg->blocked_cb = wpa_driver_nl80211_rfkill_blocked;
rcfg->unblocked_cb = wpa_driver_nl80211_rfkill_unblocked;
drv->rfkill = rfkill_init(rcfg);
//檢查當前接口是否為UP狀態
linux_iface_up(drv->global->ioctl_sock, ifname)
//主要為操作網卡wlan0
wpa_driver_nl80211_finish_drv_init(drv, set_addr, 1)
{
//得知網卡地址和設置網口為UP
if (set_addr && (linux_set_iface_flags(drv->global->ioctl_sock, bss->ifname, 0) || linux_set_ifhwaddr(drv->global->ioctl_sock, bss->ifname, set_addr)))
//獲取網口能力,具體能力請查閱struct wpa_driver_capa
if (wpa_driver_nl80211_capa(drv))
//設置網口信息 IF_OPER_DORMANT
netlink_send_oper_ifla(drv->global->netlink, drv->ifindex, 1, IF_OPER_DORMANT);
//獲取網口地址
if (linux_get_ifhwaddr(drv->global->ioctl_sock, bss->ifname, bss->addr))
}
//創建一個socket,並注冊接收函數wpa_driver_nl80211_handle_eapol_tx_status
drv->eapol_tx_sock = socket(PF_PACKET, SOCK_DGRAM, 0);
eloop_register_read_sock(drv->eapol_tx_sock, wpa_driver_nl80211_handle_eapol_tx_status, drv, NULL);
}
5 密鑰處理數據包收發socekt
由netlink創建
//初始化驅動,主要完成了l2_paket回掉函數的初始化,與4次握手相關
if (wpa_supplicant_driver_init(wpa_s) < 0)
{
//主要函數,該函數會初始化l2_packet,並注冊回掉函數,與4次握手相關
if (wpa_supplicant_update_mac_addr(wpa_s) < 0)
{
//wpa_supplicant_rx_eapol()就是4次握手的處理函數了
wpa_s->l2 = l2_packet_init(wpa_s->ifname, wpa_drv_get_mac_addr(wpa_s), ETH_P_EAPOL, wpa_supplicant_rx_eapol, wpa_s, 0);
{
//新建socket
struct l2_packet_data *l2->fd = socket(PF_PACKET, l2_hdr ? SOCK_RAW : SOCK_DGRAM, htons(protocol));
//bind
if (bind(l2->fd, (struct sockaddr *) &ll, sizeof(ll)) < 0)
//將socket與回掉函數l2_packet_receive相關聯,內部實際會調用 wpa_supplicant_rx_eapol()
eloop_register_read_sock(l2->fd, l2_packet_receive, l2, NULL);
{
//相關步驟請看eloop_data結構體,所有的回掉函數和回掉所用的數據都在保存在該結構體中
res = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *) &ll, &fromlen);
l2->rx_callback(l2->rx_callback_ctx, ll.sll_addr, buf, res);
}
}
}
}
wap_cli控制socket
在linux中可以通過wpa_supplicant提供的wpa_cli工具控制wpa_supplicant,在android中也可以通過wpa_cli在沒有framework的情況下實現掃描連接工作
下述代碼就是創建需要的socket和接收發送函數。
在android手機中打開wifi後再 /data/misc/wifi/sockets下的wlan0 和p2p0 就是該段代碼生成的
wpa_s->ctrl_iface = wpa_supplicant_ctrl_iface_init(wpa_s);
{
//分配空間,然後執行真正的初始化工作
struct ctrl_iface_priv *priv;
if (wpas_ctrl_iface_open_sock(wpa_s, priv) < 0)
{
//android上運行不會執行以下兩行代碼
//os_snprintf(addr.sun_path, sizeof(addr.sun_path), "wpa_%s", wpa_s->conf->ctrl_interface);
//priv->sock = android_get_control_socket(addr.sun_path);
//新建sock
priv->sock = socket(PF_UNIX, SOCK_DGRAM, 0);
//bind,地址為,/data/misc/wifi/sockets/wlan0,這個地址是新生成的
bind(priv->sock, (struct sockaddr *) &addr, sizeof(addr))
//注冊接收socket的函數wpa_supplicant_ctrl_iface_receive, 其核心調用函數是 wpa_supplicant_ctrl_iface_process
eloop_register_read_sock(priv->sock, wpa_supplicant_ctrl_iface_receive, wpa_s, priv);
//向socket上傳event的函數
wpa_msg_register_cb(wpa_supplicant_ctrl_iface_msg_cb);
}
}
通過wpa_cli工具使用連接網絡的步驟:
添加接口過程中的全部代碼struct wpa_supplicant * wpa_supplicant_add_iface(struct wpa_global *global,
struct wpa_interface *iface)
{
struct wpa_supplicant *wpa_s;
struct wpa_interface t_iface;
struct wpa_ssid *ssid;
struct wpa_supplicant *wpa_s = wpa_supplicant_alloc();
wpa_s->global = global;
//網絡接口的初始化,與底層通信的socket,密鑰數據包的socket,wpa_s的初始狀態等
if (wpa_supplicant_init_iface(wpa_s, &t_iface))
{
//讀取兩個conf配置文件
wpa_s->conf = wpa_config_read(wpa_s->confname, NULL);
wpa_config_read(wpa_s->confanother, wpa_s->conf);
//初始化驅動接口,並注冊接收驅動event的函數 nl80211
if (wpas_init_driver(wpa_s, iface) < 0)
{
//根據傳進來的-inl80211選定對應的wpa_driver_ops(driver_nl80211.c),裡面定義了驅動的操作方法(),同時調用對應的global_init()進行初始化
if (wpa_supplicant_set_driver(wpa_s, driver) < 0)
{
//根據名稱選擇網絡ops
select_driver(wpa_s, i)
{
//調用對應的global_init(),void * nl80211_global_init(void)
global->drv_priv[i] = wpa_drivers[i]->global_init();
{
//初始化netlink socket,設置兩個消息處理函數,netlink_receive中使用
cfg->newlink_cb = wpa_driver_nl80211_event_rtm_newlink;
cfg->dellink_cb = wpa_driver_nl80211_event_rtm_dellink;
//創建一個接收kernel中 route 子系統的消息socket
global->netlink = netlink_init(cfg);
{
//*****該socket用於接收 kernel中
netlink->sock = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
bind(netlink->sock, (struct sockaddr *) &local, sizeof(local));
//注冊內核消息接收函數, 根據接收到的消息type選擇以下兩個處理函數
eloop_register_read_sock(netlink->sock, netlink_receive, netlink, NULL);
static void netlink_receive(int sock, void *eloop_ctx, void *sock_ctx)
{
char buf[8192]; //接收消息的字節數,
left = recvfrom(sock, buf, sizeof(buf), MSG_DONTWAIT, (struct sockaddr *) &from, &fromlen);
//buf中是一個netlink消息,符合netlink規范,根據消息type選在相應的處理函數 newlink 和 dellink
}
}
//創建 global結構體中的 nl_cb(接收消息處理)和nl(發送消息)
if (wpa_driver_nl80211_init_nl_global(global) < 0)
{
//創建連接到 GENERIC_NETLINK 的socket
global->nl = nl_create_handle(global->nl_cb, "nl");
//將nl連接到 內核中的nl80211模塊
global->nl80211_id = genl_ctrl_resolve(global->nl, "nl80211");
//創建接收消息的event socket, 並分別加入到組播組scan mlme regulatory vendor 中
global->nl_event = nl_create_handle(global->nl_cb, "event");
ret = nl_get_multicast_id(global, "nl80211", "scan");
ret = nl_socket_add_membership(global->nl_event, ret);
//設置nl_cb的回調函數 process_global_event(), 該函數又會調用 wpa_supplicant_event() 做進一步處理
nl_cb_set(global->nl_cb, NL_CB_VALID, NL_CB_CUSTOM, process_global_event, global);
//為nl_event注冊接收函數, 通過nl_recvmsgs()調用 process_global_event()
nl80211_register_eloop_read(&global->nl_event, wpa_driver_nl80211_event_receive, global->nl_cb);
}
//創建一個 ioctl_sock, 還不知道干啥用,該sock用於通過ioctl方式向底層發送命令
global->ioctl_sock = socket(PF_INET, SOCK_DGRAM, 0);
}
}
}
//調用對應的初始化函數wpa_driver_nl80211_drv_init(),傳入的ifname名稱為 wlan0
wpa_s->drv_priv = wpa_drv_init(wpa_s, wpa_s->ifname);
static void * wpa_driver_nl80211_drv_init(void *ctx, const char *ifname, void *global_priv, int hostapd, const u8 *set_addr)
{
//挺重要的一個結構體
struct wpa_driver_nl80211_data *drv;
//添加了回調函數drv->nl_cb process_drv_event(),最終會調用 wpa_supplicant_event()
if (wpa_driver_nl80211_init_nl(drv))
//添加bss回調函數bss->nl_cb,看著沒啥用,最終處理 wpa_supplicant_event(),這兩個回調函數沒有和socket綁定,目前不清楚調用地方
if (nl80211_init_bss(bss))
//RF射頻省電相關,讀取節點“/dev/rfkill”只是接受消息打印log,不做任何操作
rcfg->blocked_cb = wpa_driver_nl80211_rfkill_blocked;
rcfg->unblocked_cb = wpa_driver_nl80211_rfkill_unblocked;
drv->rfkill = rfkill_init(rcfg);
//檢查當前接口是否為UP狀態
linux_iface_up(drv->global->ioctl_sock, ifname)
//主要為操作網卡wlan0
wpa_driver_nl80211_finish_drv_init(drv, set_addr, 1)
{
//得知網卡地址和設置網口為UP
if (set_addr && (linux_set_iface_flags(drv->global->ioctl_sock, bss->ifname, 0) || linux_set_ifhwaddr(drv->global->ioctl_sock, bss->ifname, set_addr)))
//獲取網口能力,具體能力請查閱struct wpa_driver_capa
if (wpa_driver_nl80211_capa(drv))
//設置網口信息 IF_OPER_DORMANT
netlink_send_oper_ifla(drv->global->netlink, drv->ifindex, 1, IF_OPER_DORMANT);
//獲取網口地址
if (linux_get_ifhwaddr(drv->global->ioctl_sock, bss->ifname, bss->addr))
}
。。創建一個socket,並注冊接收函數wpa_driver_nl80211_handle_eapol_tx_status
drv->eapol_tx_sock = socket(PF_PACKET, SOCK_DGRAM, 0);
eloop_register_read_sock(drv->eapol_tx_sock, wpa_driver_nl80211_handle_eapol_tx_status, drv, NULL);
}
}
//初始化wpa_s,主要設置 wpa 狀態機
if (wpa_supplicant_init_wpa(wpa_s) < 0)
{
//設置一些操作函數,請查看結構體 struct wpa_sm_ctx
。。。
//
struct wpa_sm * wpa_sm_init(struct wpa_sm_ctx *ctx)
{
//主要是3個變量值
sm->dot11RSNAConfigPMKLifetime = 43200; //PMK的生存時間(秒)(12小時),超時後要重新計算
sm->dot11RSNAConfigPMKReauthThreshold = 70; //PMK超時多少(70%)後,需要重新身份認證
sm->dot11RSNAConfigSATimeout = 60; //進行身份驗證的最長時間(秒)
}
}
//初始化驅動,主要完成了l2_paket回掉函數的初始化,與4次握手相關
if (wpa_supplicant_driver_init(wpa_s) < 0)
{
//主要函數,該函數會初始化l2_packet,並注冊回掉函數,與4次握手相關
if (wpa_supplicant_update_mac_addr(wpa_s) < 0)
{
//wpa_supplicant_rx_eapol()就是4次握手的處理函數了
wpa_s->l2 = l2_packet_init(wpa_s->ifname, wpa_drv_get_mac_addr(wpa_s), ETH_P_EAPOL, wpa_supplicant_rx_eapol, wpa_s, 0);
{
//新建socket
struct l2_packet_data *l2->fd = socket(PF_PACKET, l2_hdr ? SOCK_RAW : SOCK_DGRAM, htons(protocol));
//bind
if (bind(l2->fd, (struct sockaddr *) &ll, sizeof(ll)) < 0)
//將socket與回掉函數l2_packet_receive相關聯,內部實際會調用 wpa_supplicant_rx_eapol()
eloop_register_read_sock(l2->fd, l2_packet_receive, l2, NULL);
{
//相關步驟請看eloop_data結構體,所有的回掉函數和回掉所用的數據都在保存在該結構體中
res = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *) &ll, &fromlen);
l2->rx_callback(l2->rx_callback_ctx, ll.sll_addr, buf, res);
}
}
}
}
//設置國家碼到驅動中
wpa_drv_set_country(wpa_s, wpa_s->conf->country))
//wpa_s中的 struct wps_context *wps 節點
if (wpas_wps_init(wpa_s))
{
//wps_context *wps中定義了一些方法,具體請查看注釋,沒什麼邏輯處理的問題
}
//初始化 EAPOOL,參考eapol模塊信息
wpa_supplicant_init_eapol(wpa_s)
{
//設置一些方法
。。。
ctx->eapol_send = wpa_supplicant_eapol_send;
。。。
//初始化eapol狀態機
wpa_s->eapol = eapol_sm_init(ctx);
}
//初始化與FWKS的通信的socket???
wpa_s->ctrl_iface = wpa_supplicant_ctrl_iface_init(wpa_s);
{
//分配空間,然後執行真正的初始化工作
struct ctrl_iface_priv *priv;
if (wpas_ctrl_iface_open_sock(wpa_s, priv) < 0)
{
//參數通過啟動參數傳入wlan0的名稱,然後獲取init.rc中wpa_wlan0的socket
//android上運行不會執行以下兩行代碼
//os_snprintf(addr.sun_path, sizeof(addr.sun_path), "wpa_%s", wpa_s->conf->ctrl_interface);
//priv->sock = android_get_control_socket(addr.sun_path);
//新建sock
priv->sock = socket(PF_UNIX, SOCK_DGRAM, 0);
//bind,地址為,/data/misc/wifi/sockets/wlan0,這個地址是新生成的
bind(priv->sock, (struct sockaddr *) &addr, sizeof(addr))
//注冊接收socket 命令的函數wpa_supplicant_ctrl_iface_receive, 其核心調用函數是 wpa_supplicant_ctrl_iface_process
eloop_register_read_sock(priv->sock, wpa_supplicant_ctrl_iface_receive, wpa_s, priv);
//向socket上傳event的函數
wpa_msg_register_cb(wpa_supplicant_ctrl_iface_msg_cb);
}
}
//初始化bss相關,主要是wpa_s中的兩個鏈表
if (wpa_bss_init(wpa_s) < 0)
{
//初始化wpa_s中bss和bssid鏈表
dl_list_init(&wpa_s->bss);
dl_list_init(&wpa_s->bss_id);
//注冊超時函數 ,每隔10秒刷新下bss,去掉無用的bss
eloop_register_timeout(WPA_BSS_EXPIRATION_PERIOD=10, wpa_bss_timeout, wpa_s, NULL);
}
//sim卡 sd卡相關
if (pcsc_reader_init(wpa_s) < 0)
//不知道做什麼用的
if (wpas_init_ext_pw(wpa_s) < 0)
}
/* Notify the control interfaces about new iface */
if (wpas_notify_iface_added(wpa_s)) {
wpa_supplicant_deinit_iface(wpa_s, 1, 0);
return NULL;
}
for (ssid = wpa_s->conf->ssid; ssid; ssid = ssid->next)
wpas_notify_network_added(wpa_s, ssid);
//設置wpas狀態
wpa_supplicant_set_state(wpa_s, WPA_DISCONNECTED);
return wpa_s;
}
流程圖
其中虛線表示數據的傳遞,深棕色部分表示創建的socket以及收發函數
下一篇為wpa_supplicant中的兩個重要的結構體bss和conf
bss中保存著掃描結果, conf中為wpa_s的參數和保存的網絡
隔了很久沒寫博客,現在必須快速脈動回來。今天我還是接著上一個多線程中的異步加載系列中的最後一個使用異步加載實現ListView中的圖片緩存及其優化。具體來說這次是一個綜合
一、新建HelloWorld項目: Project-Android Application Project: 選擇項目保存位置,一路“next”完成項目創建
微信朋友圈,大家都知道存在一個公眾號,一個訂閱號和服務號。是個人,企業,媒體向廣大微信用戶發出自己聲音的平台。然後,一句老話“林子大了,什麼鳥都
前言:從本篇開始,將進入Multimedia框架,包含MediaPlayer, Camera, Surface, MediaRecord, 接下來幾篇都是MediaPla