TabHost详解

正文
TabHost的实现分为两种,一个是不继承TabActivity,一个是继承自TabActivity;当然了选用继承自TabActivity的话就相对容易一些,下面来看看分别是怎样来实现的吧。

方法一、定义tabhost:不用继承TabActivity
1、布局文件:activity_main.xml
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
tools:context=”.MainActivity” >
<Button
android:id=”@+id/button1″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Button” />
<TabHost
android:id=”@+id/tabhost”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”>

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical” >

<TabWidget
android:id=”@android:id/tabs”
android:layout_width=”match_parent”
android:layout_height=”wrap_content” >
</TabWidget>

<FrameLayout
android:id=”@android:id/tabcontent”
android:layout_width=”match_parent”
android:layout_height=”match_parent” >

<!– *个tab的布局 –>
<LinearLayout
android:id=”@+id/tab1″
android:layout_width=”match_parent”
android:layout_height=”match_parent” >

<TextView
android:id=”@+id/textView1″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”林炳东” />

</LinearLayout>

<!– 第二个tab的布局 –>
<LinearLayout
android:id=”@+id/tab2″
android:layout_width=”match_parent”
android:layout_height=”match_parent” >

<TextView
android:id=”@+id/textView2″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”张小媛” />

</LinearLayout>

<!– 第三个tab的布局 –>
<LinearLayout
android:id=”@+id/tab3″
android:layout_width=”match_parent”
android:layout_height=”match_parent” >

<TextView
android:id=”@+id/textView3″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”马贝贝” />

</LinearLayout>
</FrameLayout>
</LinearLayout>
</TabHost>

</LinearLayout>
2、JAVA代码

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TabHost th=(TabHost)findViewById(R.id.tabhost);
th.setup(); //初始化TabHost容器

//在TabHost创建标签,然后设置:标题/图标/标签页布局
th.addTab(th.newTabSpec(“tab1”).setIndicator(“标签1”,getResources().getDrawable(R.drawable.ic_launcher)).setContent(R.id.tab1));
th.addTab(th.newTabSpec(“tab2”).setIndicator(“标签2”,null).setContent(R.id.tab2));
th.addTab(th.newTabSpec(“tab3”).setIndicator(“标签3”,null).setContent(R.id.tab3));

//上面的null可以为getResources().getDrawable(R.drawable.图片名)设置图标

}
}
效果图:

%title插图%num

此例源码地址:http://download.csdn.net/detail/harvic880925/6657611  (不要分,欢迎下载)

方法二:Tab的内容分开:不用继承TabActivity
1、*个tab的XML布局文件,tab1.xml:
<?xml version=”1.0″ encoding=”UTF-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/LinearLayout01″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”>
<TextView
android:text=”我是标签1的内容喔”
android:id=”@+id/TextView01″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”>
</TextView>
</LinearLayout>
2、第二个tab的XML布局文件,tab2.xml:
<?xml version=”1.0″ encoding=”UTF-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/LinearLayout02″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”>

<TextView android:text=”标签2″
android:id=”@+id/TextView01″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</LinearLayout>
3、主布局文件,activity_main.xml:
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:orientation=”vertical” >

<TabHost
android:id=”@+id/tabhost”
android:layout_width=”match_parent”
android:layout_height=”match_parent” >

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical” >

<TabWidget
android:id=”@android:id/tabs”
android:layout_width=”match_parent”
android:layout_height=”wrap_content” >
</TabWidget>

<FrameLayout
android:id=”@android:id/tabcontent”
android:layout_width=”match_parent”
android:layout_height=”match_parent” >

</FrameLayout>
</LinearLayout>
</TabHost>

</LinearLayout>
4、JAVA代码:
public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TabHost m = (TabHost)findViewById(R.id.tabhost);
m.setup();

LayoutInflater i=LayoutInflater.from(this);
i.inflate(R.layout.tab1, m.getTabContentView());
i.inflate(R.layout.tab2, m.getTabContentView());//动态载入XML,而不需要Activity

m.addTab(m.newTabSpec(“tab1”).setIndicator(“标签1”).setContent(R.id.LinearLayout01));
m.addTab(m.newTabSpec(“tab2”).setIndicator(“标签2”).setContent(R.id.LinearLayout02));

}
}
效果图:

%title插图%num
此例源码地址:http://download.csdn.net/detail/harvic880925/6657679   (不要分,欢迎下载)

方法三:继承自TabActivity
1、主布局文件,activity_main.xml:
<?xml version=”1.0″ encoding=”utf-8″?>
<FrameLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”>

<!– *个布局 –>
<LinearLayout
android:id=”@+id/view1″
android:layout_width=”match_parent”
android:layout_height=”match_parent” >
<TextView
android:id=”@+id/textView1″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”张小媛” />
</LinearLayout>

<!– 第二个布局 –>
<LinearLayout
android:id=”@+id/view2″
android:layout_width=”match_parent”
android:layout_height=”match_parent” >

<TextView
android:id=”@+id/textView2″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”马贝贝” />
</LinearLayout>

<!– 第三个布局 –>
<TextView android:id=”@+id/view3″
android:background=”#00ff00″
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:text=”Tab3″/>

</FrameLayout>
2、JAVA代码:
先将派生自Activity改为TabActivity,然后代码如下:

 

public class MainActivity extends TabActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(“TabDemoActivity”);
TabHost tabHost = getTabHost();
LayoutInflater.from(this).inflate(R.layout.activity_main,
tabHost.getTabContentView(), true);
tabHost.addTab(tabHost.newTabSpec(“tab1”).setIndicator(“tab1”, getResources().getDrawable(R.drawable.ic_launcher))
.setContent(R.id.view1));
tabHost.addTab(tabHost.newTabSpec(“tab3”).setIndicator(“tab2”)
.setContent(R.id.view2));
tabHost.addTab(tabHost.newTabSpec(“tab3”).setIndicator(“tab3”)
.setContent(R.id.view3));

//标签切换事件处理,setOnTabChangedListener
tabHost.setOnTabChangedListener(new OnTabChangeListener(){
@Override
public void onTabChanged(String tabId) {
if (tabId.equals(“tab1”)) { //*个标签
}
if (tabId.equals(“tab2”)) { //第二个标签
}
if (tabId.equals(“tab3”)) { //第三个标签
}
}
});

}

}
效果如下:
%title插图%num

服务器端配置python运行环境与py文件的运行

配置python环境
我这里只配置了anaconda环境,安装anaconda时需要注意python的版本。
目前很多库对python3.7的支持还不够,依然选择3.6版本。
anaconda官方网站给的anaconda下载链接内置的是python3.7。
可以从下面这个网站中找到anaconda发行的每一个版本:https://repo.continuum.io/archive/
安装教程很多,可以参考这博客或者这博客。

安装所需要的包
根据运行的py文件所需要的包,使用conda install。

py文件的运行
切换到python文件的目录下,给脚本文件运行权限: chmod 755 ./*.py
执行文件:python ./test.py

如果在脚本内容的开头已经给出了类似于如下的注释:#!/usr/bin/env python或者#!/usr/bin/python
切换到python文件目录直接运行:test.py

对于注释头#!/usr/bin/python或者#!/usr/bin/env python
#!/usr/bin/python 是告诉系统使用哪个编译器去执行当前文件。 这里的python是linux自带的python,但是其版本不一定适合当前我们编写的代码。
于是就有了#!/usr/bin/env python, 这里建立了自己的环境,用自定义环境里面的python执行文件。

服务器上使用python

工具:

服务器
xshell
xftp
网页访问服务器方法:

登录xshell,连接到服务器地址。 输入ipython notebook得到一个端口号例如1990
在网页中输入:https://10.117.63.32:9990 看到的是一个目录,进去你想要创建或者查看的目录搞事情。

传输文件

小型文件直接拖拽到服务器上面; 大文件可以用xftp传输,在xshell点击上方
新建文件传输c+a+f,弹出来xftp(前提:安装了xftp)

ipython的使用

只有python2版本可供选择
可以分段式编译,例如import……为*段,之前解释好的内容会保存在服务器里面,这样对于大型代码不会每次都从头开始运行。接着解释例如读入文件的代码,再接着保存参数有关的代码等等。
注意:每次离开的时候如果程序不要运行记着shutdown。

如何在云服务器上自动运行.py文件

如果你在云服务器上运行的目的是保持一直运行,那就继续往下看吧、

有很多种方法,我这里说的是在linux上操作的一种。

利用screen会话分离。

因为在Screen环境下,所有的会话都独立的运行,并拥有各自的编号、
输入、输出和窗口缓存。

所以长话短说:
*步:安装screen :
yum install screen

第二步: 创建screen -S LX (注释:-S,意思是创建一个screen分屏环境。S是大写,LX是分屏的名字)

第三步:运行
创建后会进入一个空白的界面。看起来跟ctrl+L了清屏了一样。不要慌。
你可以直接运行

python3 lxisgoodman.py
1
然后 ctrl+a+d 退出当前csreen环境。记住要一起按

第三步另外一种操作:
你创建完分屏可以直接 ctrl+a+d ,然后在外面

screen -S LX -X screen python3 lxisgoodman.py
1
第四步:查看
其实上面已经达到目的了。如果你要想回去看运行状况的话
screen -ls 查询所以的screen程序。 然后有个pid。就是几个数字

然后 screen -r 88888 重新连接screen (88888是你-ls出来对应的数字)

第五步:终止
不想运行了咋办。
-ls查一下,然后 kill 88888

第六步:dead
-ls出来有dead标志的。说明程序凉了
creen -wipe 清除dead 会话

没了。简单又好用。= =

iOS集成系统分享功能

使用UIActivityViewController集成系统分享功能

/**
分享

@param title 标题
@param image 图标
@param url 链接
@param target r视图控制器
@param complete 回调
*/
+ (void)shareWithTitle:(NSString *)title image:(UIImage *)image url:(NSString *)url target:(UIViewController *)target complete:(void (^)(BOOL isSuccess, UIActivityType type))complete
{
// 分享内容
NSString *shareTitle = title;
UIImage *shareImage = image;
NSURL *shareUrl = [NSURL URLWithString:url];
NSArray *activityItemsArray = @[shareTitle, shareImage, shareUrl];
//
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:activityItemsArray applicationActivities:nil];
activityVC.modalInPopover = YES;
// 禁用分享渠道
// activityVC.excludedActivityTypes = @[UIActivityTypePrint, UIActivityTypeCopyToPasteboard, UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll, UIActivityTypeAddToReadingList, UIActivityTypePostToFlickr, UIActivityTypePostToVimeo, UIActivityTypeAirDrop, UIActivityTypeOpenInIBooks];

//
activityVC.completionWithItemsHandler = ^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) {
if (complete) {
complete(completed, activityType);
}
};
//
[target presentViewController:activityVC animated:YES completion:nil];
}

系统控件默认显示英文,可通过将info.plist文件的属性Localized resources can be mixed值设置为YES则显示为中文。

云计算技术,给我们程序员的工作流程,带来了哪些改变?

如今,现代软件的开发在硬件和软件资源方面的要求是非常苛刻的,而很多程序人员只是为了编写代码而投购买高端机器的情况并不少见。实际上,这样的处理和投入是值得的,但更重要的是,这些人可能会获得一些改善处境的机会。而令人关注的是,将云计算技术整合到业务的开发工作流程中可以显著提高生产力。

复杂的计算

如果开发研究人员正在进行与数据科学相关的研究或任何涉及大数据集的研究,那么很可能已经遇到了一些试图定期处理大量数据的问题。不幸的是,随着在这些领域的进展时,它会变得更加困难,这就是为什么许多数据科学家依赖外部云服务进行繁重的计算的原因。而人们只要浏览像Programering这样的网站,通常会看到大量类似相关主题的讨论。研究人员通常会设置一个工作流程,可以将数据提交给外部服务,在处理其他任务时对其进行处理,并且只需在计算完成后检查结果即可。

模拟

开发人员也可以检查其程序在不同平台上的运行方式,如果这是特定开发工作中的一个问题。如果开发人员正在开发可用于不同硬件和软件配置的应用程序,并且云计算模拟器不会总是能为其提供100%正确的结果,但采用模拟技术,这一点非常重要,因为这比人们通过判断应用程序工作是否正常要强的多。此外,开发人员可以立即发现一些更加突出的问题,而不必担心在工作中会出现类似的问题,如果开发人员经常遇到较小的问题,那么采用模拟技术可以为其节省大量时间。

测试

在相关说明中,开发人员还可以设置一些自动化测试程序,以便其可以保持软件的检查,而无需经常进行人工处理。如果其经常定期发布新版本的程序,并希望确保不会遇到任何回归问题,这是这些情况中*常见的问题之一,开发人员不应该只是依靠客户报告才了解出现的越来越多的问题。自动化测试将成为*好的方法之一,尽管为了做到这一点需要采用一些外部资源。

不要仅仅因为没有正确使用外部工具而限制软件开发人员的潜力。通过采用云计算可提高生产力,开发人员将获得很多益处。一旦开发人员将其集成到更基本的工作流程中,甚至不需要考虑这些程序,只需在自动执行模式下执行它们,并且可以更好地将注意力集中在编程代码方面的更大问题上。

iOS—-获取系统启动时间和App安装(更新)时间

2020年发生了很多事情,是多灾多难的一年
想了一下,今年的*后一篇博客还是写给iOS吧,毕竟是我开始工作赚钱学会的*份技能
2020年的*后一天,跟时间有关,所以写一篇关于时间的博客提醒一下自己,*批90后已经30岁了。。。

获取系统的启动时间,原理很简单,就是获取kernel_task的启动时间,kernel_task大家都知道,系统主进程,系统挂了他也挂,系统起了他也起,所以获取kernel_task的启动时间就相当于获取到了系统的启动时间。

#include <sys/sysctl.h>

+ (void)boots {
struct timeval boottime;
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size = sizeof(boottime);

if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
{
NSLog(@”%@”, [NSNumber numberWithLong:boottime.tv_sec]);
}
}

获取App安装、更新时间,方法也很取巧,就是查看info.plist的创建时间,每次更新App的时候info.plist会重新创建,因此可以作为获取App安装时间的*佳方式。

+ (NSNumber *)appUpdateTime {
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@”Info” ofType:@”plist”];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:bundlePath error:nil];
NSDate *date = [fileAttributes objectForKey:NSFileCreationDate];
return [NSNumber numberWithDouble:date.timeIntervalSince1970];
}

iOS 10 不提示「是否允许应用访问数据」,导致应用无法使用的解决方案

这个坑*近弄得我很抓狂,不过现在基本弄清楚了。记录一下过程中我收集到的信息,分享给大家。

症状

iOS 10 之后,陆陆续续地有用户联系我们,说新机*次安装、*次启动的时候,app 首屏一片空白,完全没数据。kill 掉重新打开就好了。

一开始以为是用户网络情况不好,但随着越来越多的用户报告这个问题,我意识到这并不是偶然情况。但是并非所有用户都如此。

而且卸载掉之后,如果再装,也不会出现这现象。问题只会出现在这台设备*次安装、*次启动的情况下。如果把手机抹掉、重置,问题还能重现。

定位问题

这个问题真的很棘手,也很难定位。幸运的是,公司同事想到把手机抹掉重置,得以在我眼前重现问题。

我发现的是,app 首次启动会弹出一个询问用户“是否允许应用访问数据”的弹框,类似下图:

%title插图%num询问网络权限的弹框

虽然 app 刚打开的时候是一片空白,但我发现进去之后,登录、下拉刷新等都没问题。因此很容易猜测出这样的结论:用户点“允许”之前,网络请求全都是失败的;而点“允许”之后,网络请求就能正常进行了。

问题原因

有了方向之后就好查了。很快查到了掘金的这篇文章,得知这个弹框来自于工信部的要求。这篇文章里还有如果弹框不出现,用户可以采取的解决方案。另外,从少数派的这篇文章 看到,只有国行手机有这个功能。这也就解释了为何有些用户出现、而有些用户没出现这个问题。

%title插图%num蜂窝移动网络的两种界面

进到手机的 设置->蜂窝移动网络,如果看到如左图就说明是不会弹框的机型,如果看到如右图,说明是会弹框的机型。

那么这个新功能会为用户带来哪些问题呢?问题主要在于,用户点击“允许”之前,所有网络请求都是被禁止的。具体有两种表现:

  1. 少部分用户根本不显示弹框,所以网络请求一直被禁止。针对这部分用户,只能通过客服引导,按照掘金的这篇文章,逐个尝试里面的解决方案;
  2. 对于*大部分用户,弹框会正确显示;然而从 app 启动到用户点击“允许”需要一段时间,在这段时间内发出的网络请求全都会直接失败;

如果用户点击“不允许”,app 永远无法访问网络,Wifi 和数据流量均不可以。当然,这是用户自己的选择,我们没什么可做的。我们主要需要解决的是上面的第二个问题。

影响范围

这个特性推出之后,大部分 app 应该都会受到不同程度的影响。可以着重在这几个方面检查一下自己的 app:

  1. 首屏数据。首屏几个 tab 的数据往往在 app 启动时即加载,也就是在用户点“允许”之前。很容易造成用户*次进入时,首屏数据空白。
  2. 推送。通常的处理逻辑是,把注册设备远程推送的代码写在 appDelegate 里。经过测试发现,这种写法下允许推送的弹框和允许使用网络的弹框出现的顺序没有一定。如果先出允许推送的弹框,用户点击允许,此时注册 deviceToken 是不能成功的。当然如果用户允许访问网络,第二次打开 app 时也会走一遍注册远程推送方法,此时就能注册成功了。
  3. 其他首次启动的处理。诸如广告页、活动页之类,需要在启动时请求的数据。新版本的更新检查往往也在启动时进行,但这一点影响不大,因为首次打开的用户一般都是处于*新版。另外,常常会在新设备首次启动时,上传一个设备唯一标识用于统计目的,例如 IDFA。

解决方案

在重置过的手机上,尝试装了一些大大小小的 app,发现不少 app 在适配这个新特性上都存在一些小问题。而有些 app 也做了比较有特色的处理。

不幸的是,苹果这个功能可能出得太仓促,并没有给开发者提供相应的 API。所以,我们没办法检测到用户点击“允许”或“不允许”网络请求的回调,也没法检测到当前用户是否授权的状态。只能通过一些特殊处理,来尽量减小对用户的影响。

总体来说,主要有如下几个解决方案:

  1. 延迟请求。对于首次启动的所有接口,如果能延迟到用户点击“允许”之后再请求,或者重新请求一次,就能把对用户的影响降到*低,是一个比较好的解决方案。因为首次启动往往有几屏引导页,一个比较好的时机是引导页结束时。此时用户已经进行了授权,数据都能正确得到。
  2. 允许用户手动重新请求。出现数据空白时,如果在空白页面上有“重新加载”的按钮,也可以让用户体验好一些。比较有趣的是,测试中发现网易严选的处理是这样的:

    %title插图%num网易严选的首屏界面
    加了一个“查看解决方案”的按钮。点击这个按钮会跳转到一个描述解决方案的页面,内容跟上面掘金的文章类似。很有意思的处理,虽然不能避免白屏,但用户会尝试重新打开,还可以帮到少部分始终不显示弹框的用户。

  3. 稍后重新请求。网络框架如果做了请求失败时,定时重新请求的处理,应该也能解决首次请求失败的问题。另外,首次启动时各种处理的逻辑都可以写成一旦失败,下次启动重试。如每次启动都会注册远程推送。另一个例子是上传设备唯一标识的逻辑,可以写成类似这样:
  1. NSString *storedIDFA = [[NSUserDefaults standardUserDefaults] objectForKey:kIDFAKey];
  2. NSString *idfaString = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
  3. if ([storedIDFA isEqualToString:idfaString]) {
  4. return;
  5. }
  6. [HAMCommonBusinessStore requestUploadIDFA:idfaString success:^(id response) {
  7. [[NSUserDefaults standardUserDefaults] saveObject:idfaString forKey:kIDFAKey];
  8. }];

每次打开 app 都调用这段代码,而上传成功时才保存到本地。这样首次请求失败也无妨,下次打开时仍能重试上传,直到成功为止。

开发者的无奈

临时出现这种变故,作为开发者也表示很无奈。为了排查问题,技术同事牺牲手机反复重置,老板还一副不相信的样子:“那其他家 app 怎么就没出问题?”

好在总算能用各种特殊处理,把问题先掩盖过去。还是希望苹果能在 iOS 系统的新版本里完善这个新功能,提供类似相机权限的 api 吧。不要再折磨广大开发者了。

iOS获取当前版本号 Bundle ID等信息的方法

1:获取bundle Id信息:[[NSBundle mainBundle] bundleIdentifier];

2:获取版本号:[[[NSBundle mainBundle]infoDictionary] objectForKey:@”CFBundleShortVersionString”];

3:获取build号:[[[NSBundle mainBundle]infoDictionary] objectForKey:@”CFBundleVersion”];

4:获取App显示名:[[[NSBundle mainBundle]infoDictionary] objectForKey:@”CFBundleDisplayName”];

 

其实 [[NSBundle mainBundle]infoDictionary] 获得的是一个字典,里边放着Info.plist文件中的各种信息,根据不同的键去即可,如:

 

CFBundleDevelopmentRegion

CFBundleDisplayName

CFBundleExecutable

CFBundleExecutablePath

CFBundleIdentifier

CFBundleInfoDictionaryVersion = “6.0”;

CFBundleInfoPlistURL

CFBundleName

CFBundlePackageType

CFBundleShortVersionString

CFBundleSignature

CFBundleSupportedPlatforms

iOS Wifi 列表获取

iOS 上获取 Wifi 列表其实有很大限制,在 iOS 9 以前是不能获取Wifi列表的,只能获取当前连接的 Wifi 信息,也就表示只有连接了 Wifi 才能确定位置。
Apple 在 iOS 9 以后,提供了获取Wifi列表的API,但是获取Wifi列表是有门槛的,主要步骤有:

向 Apple 申请开发 Network Extension 权限
申请包含 Network Extension 的描述文件
配置 Info.plist
配置 entitlements
iOS 获取 Wifi 列表代码实现
获取Wifi列表回调
向 Apple 申请开发 Network Extension 权限
首先要先写封邮件给 networkextension@apple.com ,问苹果要开发 Network Extension 的权限。 苹果收到邮件后会自动回复邮件,在 https://developer.apple.com/contact/network-extension/ 里面填写申请表格,内容包括:

Organization:

Company / Product URL:

What’s your product’s target market?

What’s your company’s primary function?

Describe your application and how it will use the Network Extension framework.

What type of entitlement are you requesting?

申请后大概两周左右能收到 Aplle的 确认信,如:

Hi,

Thanks for your interest in the Network Extension APIs.

We added a new template containing the Network Extension entitlements to your team.

……

申请包含 Network Extension 的描述文件
选择包含 Network Extension 的描述文件,后点击下载,下载完成双击描述文件。

xcode中开启Network Extensions权限

%title插图%num
配置 entitlements
xxx.entitlements(xxx是项目名称) 里添加 Key-Value: com.apple.developer.networking.HotspotHelper -> YES,没有此文件需要先创建一个:

%title插图%num
iOS 获取 Wifi 列表代码实现
#import <NetworkExtension/NetworkExtension.h>
– (void)getWifiList {

if (![[[UIDevice currentDevice] systemVersion] floatValue] >= 9.0) {return;}
dispatch_queue_t queue = dispatch_queue_create(“com.leopardpan.HotspotHelper”, 0);
[NEHotspotHelper registerWithOptions:nil queue:queue handler: ^(NEHotspotHelperCommand * cmd) {
if(cmd.commandType == kNEHotspotHelperCommandTypeFilterScanList) {
for (NEHotspotNetwork* network in cmd.networkList) {
NSLog(@”network.SSID = %@”,network.SSID);
}
}
}];
}

kNEHotspotHelperCommandTypeFilterScanList: 表示扫描到 Wifi 列表信息。

NEHotspotNetwork 里有如下信息:

SSID:Wifi 名称
BSSID:站点的 MAC 地址
signalStrength: Wifi信号强度,该值在0.0-1.0之间
secure:网络是否安全 (不需要密码的 Wifi,该值为 false)
autoJoined: 设备是否自动连接该 Wifi,目前测试自动连接以前连过的 Wifi 的也为 false 。
justJoined:网络是否刚刚加入
chosenHelper:HotspotHelper是否为网络的所选助手

获取Wifi列表回调
当你把上面的代码写完,并成功运行项目后,发现并没有Wifi列表的回调。因为你还没刷新Wifi列表,你需要:

打开手机系统设置 -> WLAN -> 系统 Wifi 列表加载出来时,上面代码部分才会回调,才能获取到 Wifi 列表。
这个时候你就能看到控制台源源不断的Log。

注意事项
1、获取Wifi列表功能由于是需要申请后台权限,所以能后台激活App(应用程序),而且激活后App的进程能存活几个小时。
2、整个获取Wifi列表不需要App用户授权,也就是在App用户无感知下获取设备的Wifi列表信息,使用时请正当使用。
3、Wifi列表获取 NetworkExtension 是 iOS 9以后才出的,目前 iOS 9 已经覆盖很广了。