iOS 开发 — Swift (十) 重载构造函数

重载构造函数
Swift 中支持函数重载,同样的函数名,不一样的参数类型
/// `重载`构造函数
///
/// – parameter name: 姓名
/// – parameter age: 年龄
///
/// – returns: Person 对象
init(name: String, age: Int) {
self.name = name
self.age = age

super.init()
}
注意事项
如果重载了构造函数,但是没有实现默认的构造函数 init(),则系统不再提供默认的构造函数
原因,在实例化对象时,必须通过构造函数为对象属性分配空间和设置初始值,对于存在必选参数的类而言,默认的 init() 无法完成分配空间和设置初始值的工作
调整子类的构造函数
重写父类的构造函数
/// `重写`父类构造函数
///
/// – parameter name: 姓名
/// – parameter age: 年龄
///
/// – returns: Student 对象
override init(name: String, age: Int) {
no = “002”

super.init(name: name, age: age)
}
重载构造函数
/// `重载`构造函数
///
/// – parameter name: 姓名
/// – parameter age: 年龄
/// – parameter no: 学号
///
/// – returns: Student 对象
init(name: String, age: Int, no: String) {
self.no = no

super.init(name: name, age: age)
}
注意:如果是重载的构造函数,必须 super 以完成父类属性的初始化工作

重载和重写
重载,函数名相同,参数名/参数类型/参数个数不同
重载函数并不仅仅局限于构造函数
函数重载是面相对象程序设计语言的重要标志
函数重载能够简化程序员的记忆
OC 不支持函数重载,OC 的替代方式是 withXXX…
重写,子类需要在父类拥有方法的基础上进行扩展,需要 override 关键字

2021年6月中旬流通领域重要生产资料市场价格变动情况

中国统计信息服务中心 卓创资讯   据对全国流通领域9大类50种重要生产资料市场价格的监测显示,2021年6月中旬与6月上旬相比,21种产品价格上涨,25种下降,4种持平。 2021年6月中旬流通领域重要生产资料市场价格变动情况  产品名称 单位 本期价格(元) 比上期 价格涨跌(元) 涨跌幅 (%) 一、黑色金属         螺纹钢(Φ16-25mm,HRB400E) 吨 5075.1 -33.0 -0.6 线材(Φ6.5mm,HPB300) 吨 5412.5 -12.7 -0.2 普通中板(20mm,Q235) 吨 5524.1 -64.8 -1.2 热轧普通薄板(3mm,Q235) 吨 5666.8 -6.8 -0.1 无缝钢管(219*6,20#) 吨 6139.7 -139.0 -2.2 角钢(5#) 吨 5400.6 -50.7 -0.9 二、有色金属         电解铜(1#) 吨 69363.0 -2758.6 -3.8 铝锭(A00) 吨 18820.0 234.6 1.3 铅锭(1#) 吨 15153.2 35.3 0.2 锌锭(0#) 吨 22560.0 -210.0 -0.9 三、化工产品         硫酸(98%) 吨 640.0 40.0 6.7 烧碱(液碱,32%) 吨 516.5 -0.1 0.0 甲醇(优等品) 吨 2389.5 -42.0 -1.7 纯苯(石油苯,工业级) 吨 7694.0 -73.5 -0.9 苯乙烯(一级品) 吨 8898.8 -569.5 -6.0 聚乙烯(LLDPE,7042) 吨 8120.7 -78.7 -1.0 聚丙烯(T30S) 吨 8587.5 -89.8 -1.0 聚氯乙烯(SG5) 吨 9144.3 -25.7 -0.3 顺丁胶(BR9000) 吨 11920.0 180.0 1.5 涤纶长丝(FDY150D/96F) 吨 7495.0 82.5 1.1 四、石油天然气         液化天然气(LNG) 吨 3809.9 12.2 0.3 液化石油气(LPG) 吨 4096.1 2.7 0.1 汽油(95#国VI) 吨 8290.0 100.8 1.2 汽油(92#国VI) 吨 8049.4 100.4 1.3 柴油(0#国VI) 吨 6555.4 69.4 1.1 石蜡(58#半) 吨 7269.7 3.0 0.0 五、煤炭         无烟煤(洗中块) 吨 1300.0 37.5 3.0 普通混煤(4500大卡) 吨 709.0 10.9 1.6 山西大混(5000大卡) 吨 799.0 10.9 1.4 山西优混(5500大卡) 吨 889.0 10.9 1.2 大同混煤(5800大卡) 吨 914.0 10.9 1.2 焦煤(主焦煤) 吨 1950.0 0.0 0.0 焦炭(二级冶金焦) 吨 2626.8 -15.1 -0.6 六、非金属建材         普通硅酸盐水泥(P.O 42.5袋装) 吨 459.3 -10.6 -2.3 普通硅酸盐水泥(P.O 42.5散装) 吨 427.5 -9.5 -2.2 浮法平板玻璃(4.8/5mm) 吨 2856.9 9.7 0.3 七、农产品(主要用于加工)         稻米(粳稻米) 吨 3980.1 -19.4 -0.5 小麦(国标三等) 吨 2532.5 6.1 0.2 玉米(黄玉米二等) 吨 2818.3 -9.8 -0.3 棉花(皮棉,白棉三级) 吨 16278.9 -22.5 -0.1 生猪(外三元) 千克 13.9 -1.9 -12.0 大豆(黄豆) 吨 5259.3 -1.9 0.0 豆粕(粗蛋白含量≥43%) 吨 3473.9 -107.7 -3.0 花生(油料花生米) 吨 8240.0 -145.4 -1.7 八、农业生产资料         尿素(小颗料) 吨 2724.8 50.6 1.9 复合肥(硫酸钾复合肥,氮磷钾含量45%) 吨 2686.3 84.7 3.3 农药(草甘膦,95%原药) 吨 47750.0 93.7 0.2 九、林产品         天然橡胶(标准胶SCRWF) 吨 12306.2 -394.0 -3.1 纸浆(漂白化学浆) 吨 5412.5 -103.4 -1.9 瓦楞纸(高强) 吨 4097.6 25.4 0.6 注:上期为2021年6月上旬。    附注   1.指标解释   流通领域重要生产资料市场价格,是指重要生产资料经营企业的批发和销售价格。与出厂价格不同,生产资料市场价格既包含出厂价格,也包含有经营企业的流通费用、利润和税费等。出厂价格与市场价格互相影响,存在时滞,两者的变动趋势在某一时间段内有可能会出现不完全一致的情况。   2.监测内容   流通领域重要生产资料市场价格监测内容包括9大类50种产品的价格。类别与产品规格说明详见附表。   3.监测范围   监测范围涵盖全国31个省(区、市)300多个交易市场的近2000家批发商、代理商、经销商等经营企业。   4.监测方法   价格监测方法包括信息员现场采价,电话、即时通讯工具和电子邮件询价等。   5.涨跌个数的统计   产品价格上涨、下降、持平个数按照涨跌幅(%)进行统计。   6.发布日期   每月4日、14日、24日发布上一旬数据,节假日顺延。 附表:流通领域重要生产资料市场价格监测产品规格说明表  序号 监测产品 规格型号 说明   一、黑色金属      1   螺纹钢 Φ16-25mm,HRB400E 屈服强度≥400MPa  2 线材 Φ6.5mm,HPB300 屈服强度≥300MPa  3 普通中板 20mm,Q235 屈服强度≥235MPa  4 热轧普通薄板 3mm,Q235 屈服强度≥235MPa  5 无缝钢管 219*6,20# 20#钢材,屈服强度≥245MPa  6 角钢 5# 屈服强度≥235MPa   二、有色金属      7 电解铜 1# 铜与银质量分数≥99.95%  8 铝锭 A00 铝质量分数≥99.7%  9 铅锭 1# 铅质量分数≥99.994% 10 锌锭 0# 锌质量分数≥99.995%   三、化工产品     11  硫酸 98% H2SO4质量分数≥98% 12 烧碱(液碱) 32% NaOH质量分数≥32%的离子膜碱 13 甲醇 优等品 水质量含量≤0.10% 14 纯苯(石油苯) 工业级 苯纯度≥99.8% 15 苯乙烯 一级品 纯度≥99.5% 16 聚乙烯(LLDPE) 7042 熔指:2.0±0.5g/10min 17 聚丙烯 T30S 熔指:3.0±0.9g/10min 18 聚氯乙烯 SG5 K值:66-68 19 顺丁胶 BR9000 块状、乳白色,灰分≤0.20% 20 涤纶长丝 FDY150D/96F 150旦,AA级   四、石油天然气     21 液化天然气 LNG 甲烷含量≥75%,密度≥430kg/m3 22 液化石油气 LPG 饱和蒸汽压1380-1430kPa 23 汽油 95#国VI 国VI标准 24 汽油 92#国VI 国VI标准 25 柴油 0#国VI 国VI标准 26 石蜡 58#半 熔点不低于58℃   五、煤炭     27 无烟煤 洗中块 挥发分≤8% 28 普通混煤 4500大卡 山西粉煤与块煤的混合煤,热值4500大卡 29 山西大混 5000大卡 质量较好的混煤,热值5000大卡 30 山西优混 5500大卡 优质的混煤,热值5500大卡 31 大同混煤 5800大卡 大同产混煤,热值5800大卡 32 焦煤  主焦煤 含硫量<1% 33 焦炭 二级冶金焦 12.01%≤灰分≤13.50%   六、非金属建材     34 普通硅酸盐水泥 P.O 42.5袋装 抗压强度42.5MPa 35 普通硅酸盐水泥 P.O 42.5散装 抗压强度42.5MPa 36 浮法平板玻璃 4.8/5mm 厚度为4.8/5mm的无色透明玻璃   七、农产品(主要用于加工)     37 稻米 粳稻米 杂质≤0.25%,水分≤15.5% 38 小麦 国标三等 杂质≤1.0%,水分≤12.5% 39 玉米 黄玉米二等 杂质≤1.0%,水分≤14.0% 40 棉花(皮棉) 白棉三级 纤维长度≥28mm,白或乳白色 41 生猪 外三元 三种外国猪杂交的肉食猪 42 大豆 黄豆 杂质≤1.0%,水分≤13.0% 43 豆粕 粗蛋白含量≥43% 粗蛋白≥43%,水分≤13.0% 44 花生 油料花生米 杂质≤1.0%,水分≤9.0%   八、农业生产资料     45 尿素 小颗料 总氮≥46%,水分≤1.0% 46 复合肥 硫酸钾复合肥 氮磷钾含量45% 47 农药(草甘膦) 95%原药 草甘膦质量分数≥95%   九、林产品     48 天然橡胶 标准胶SCRWF 杂质含量≤0.05%,灰分≤0.5% 49 纸浆 漂白化学浆 亮度≥80%,黏度≥600cm³/g 50 瓦楞纸 高强 80-160g/m2  

【Android】Android 6.0 获取危险权限、运行时权限、一次申请多个权限

从Android 6.0开始,如果是危险权限,都需要经过用户授权才能使用,我们在写项目的时候往往需要弹出窗口询问用户是否授权,称为运行时权限。本文章对运行时权限进行简单总结

一、危险权限列表
Android手机有很多权限,分为普通权限和危险权限。普通权限是不涉及用户隐私的(比如说网络权限),开发者只需要在AndroidManifest声明就能随意使用,危险权限是比较重要的权限(比如说读取联系人、短信、位置等),除了要在AndroidManifest中声明,还需要经过用户授权才能使用,大概分为9组24个权限

注意:用户一旦授权了其中一个权限(名),该权限所在的权限组中的其他权限也会同时被授权

%title插图%num

二、申请权限
1、AndroidManifest声明
首先要把需要的权限在AndroidManifest声明,比如说我们申请 通话、读写、位置权限

<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”/>
<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION”/>
<uses-permission android:name=”android.permission.READ_PHONE_STATE”/>

2、layout布局
就放两个button,分别用于申请单个和多个权限

activity_main.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<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”
android:gravity=”center”
tools:context=”.MainActivity”>

<Button
android:id=”@+id/btn_single”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”单个权限”/>
<Button
android:id=”@+id/btn_multiple”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”多个权限”/>

</LinearLayout>

3、java代码
MaiActivity.java

public class MainActivity extends AppCompatActivity implements View .OnClickListener{
private Button btn_single,btn_multiple;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

btn_single = findViewById(R.id.btn_single);
btn_single.setOnClickListener(this);
btn_multiple = findViewById(R.id.btn_multiple);
btn_multiple.setOnClickListener(this);
}

@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.btn_single:
requestPermission_single();//申请单个权限
break;
case R.id.btn_multiple:
requestPermission_multiple();//申请多个权限
break;
default:
break;
}
}
private void requestPermission_single(){
……
}
private void requestPermission_multiple(){
……
}

//不管用户是否授权,都会回调这个函数
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
……
}
}

4、申请单个权限
private void requestPermission_single(){
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}else {
Log.e(“11111111”, “requestPermission_single: 已经获得写入权限了”);//处理自己的代码
}
}

 

5、申请多个权限
private void requestPermission_multiple(){
List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED){
permissionList.add(Manifest.permission.READ_PHONE_STATE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (!permissionList.isEmpty()){
String[] permissions = permissionList.toArray(new String[permissionList.size()]);
ActivityCompat.requestPermissions(MainActivity.this,permissions,2);
}else {
Log.e(“222222222”, “requestPermission_multiple: 已经获得所有权限”);//处理自己的代码
}
}

6、处理申请结果
//不管用户是否授权,都会回调这个函数
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
if (grantResults.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
Log.e(“1111111”, “onRequestPermissionsResult: 申请单个权限授权成功” );//处理自己的代码
}else{
Log.e(“1111111”, “onRequestPermissionsResult: 申请单个权限授权失败” );
}
break;
case 2:
if (grantResults.length>0){
for (int result : grantResults){
if (result != PackageManager.PERMISSION_GRANTED){
Log.e(“2222222222”, “onRequestPermissionsResult: 用户拒*了一个或多个权限”);
return;
}
}
//处理自己的代码
Log.e(“2222222222”, “onRequestPermissionsResult: 用户授权了所有权限”);//处理自己的代码
}else {
Log.e(“2222222222”, “onRequestPermissionsResult: 程序运行错误” );
}
break;
default:
break;
}
}

————————————————

iOS 实现UILabel的跑马灯效果

github地址:点击打开链接

项目新功能模块UILabel长度有限,想要完全看到字就需要有跑马灯效果。

于是众里寻他千百度·······此处使用的是自定义的UIScrollView···

点击进入原文

效果图

%title插图%num

代码

AutoScrollLabel.h

#import <UIKit/UIKit.h>

#define NUM_LABELS 2

enum AutoScrollDirection {
AUTOSCROLL_SCROLL_RIGHT,
AUTOSCROLL_SCROLL_LEFT,
};

@interface AutoScrollLabel : UIScrollView <UIScrollViewDelegate>{
UILabel *label[NUM_LABELS];
enum AutoScrollDirection scrollDirection;
float scrollSpeed;
NSTimeInterval pauseInterval;
int bufferSpaceBetweenLabels;
bool isScrolling;
}
@property(nonatomic) enum AutoScrollDirection scrollDirection;
@property(nonatomic) float scrollSpeed;
@property(nonatomic) NSTimeInterval pauseInterval;
@property(nonatomic) int bufferSpaceBetweenLabels;
// normal UILabel properties
@property(nonatomic,retain) UIColor *textColor;
@property(nonatomic, retain) UIFont *font;

– (void) readjustLabels;
– (void) setText: (NSString *) text;
– (NSString *) text;
– (void) scroll;

@end

AutoScrollLabel.m

#import “AutoScrollLabel.h”

#define LABEL_BUFFER_SPACE 20 // pixel buffer space between scrolling label
#define DEFAULT_PIXELS_PER_SECOND 30
#define DEFAULT_PAUSE_TIME 0.5f

@implementation AutoScrollLabel
@synthesize pauseInterval;
@synthesize bufferSpaceBetweenLabels;

– (void) commonInit
{
for (int i=0; i< NUM_LABELS; ++i){
label[i] = [[UILabel alloc] init];
label[i].textColor = [UIColor whiteColor];
label[i].backgroundColor = [UIColor clearColor];
[self addSubview:label[i]];
}

scrollDirection = AUTOSCROLL_SCROLL_LEFT;
scrollSpeed = DEFAULT_PIXELS_PER_SECOND;
pauseInterval = DEFAULT_PAUSE_TIME;
bufferSpaceBetweenLabels = LABEL_BUFFER_SPACE;
self.showsVerticalScrollIndicator = NO;
self.showsHorizontalScrollIndicator = NO;
self.userInteractionEnabled = NO;
}

-(id) init
{
if (self = [super init]){
// Initialization code
[self commonInit];
}

return self;
}

– (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder]) {
// Initialization code
[self commonInit];
}
return self;

}

– (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Initialization code
[self commonInit];
}
return self;
}

#if 0
– (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
[NSThread sleepForTimeInterval:pauseInterval];

isScrolling = NO;

if ([finished intValue] == 1 && label[0].frame.size.width > self.frame.size.width){
[self scroll];
}
}
#else
– (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
isScrolling = NO;

if ([finished intValue] == 1 && label[0].frame.size.width > self.frame.size.width){
[NSTimer scheduledTimerWithTimeInterval:pauseInterval target:self selector:@selector(scroll) userInfo:nil repeats:NO];
}
}
#endif

– (void) scroll
{
// Prevent multiple calls
if (isScrolling){
// return;
}
isScrolling = YES;

if (scrollDirection == AUTOSCROLL_SCROLL_LEFT){
self.contentOffset = CGPointMake(0,0);
}else{
self.contentOffset = CGPointMake(label[0].frame.size.width+LABEL_BUFFER_SPACE,0);
}

[UIView beginAnimations:@”scroll” context:nil];
[UIView setAnimationDelegate:self];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
[UIView setAnimationDuration:label[0].frame.size.width/(float)scrollSpeed];

if (scrollDirection == AUTOSCROLL_SCROLL_LEFT){
self.contentOffset = CGPointMake(label[0].frame.size.width+LABEL_BUFFER_SPACE,0);
}else{
self.contentOffset = CGPointMake(0,0);
}

[UIView commitAnimations];
}

– (void) readjustLabels
{
float offset = 0.0f;

for (int i = 0; i < NUM_LABELS; ++i){
[label[i] sizeToFit];

// Recenter label vertically within the scroll view
CGPoint center;
center = label[i].center;
center.y = self.center.y – self.frame.origin.y;
label[i].center = center;

CGRect frame;
frame = label[i].frame;
frame.origin.x = offset;
label[i].frame = frame;

offset += label[i].frame.size.width + LABEL_BUFFER_SPACE;
}

CGSize size;
size.width = label[0].frame.size.width + self.frame.size.width + LABEL_BUFFER_SPACE;
size.height = self.frame.size.height;
self.contentSize = size;

[self setContentOffset:CGPointMake(0,0) animated:NO];

// If the label is bigger than the space allocated, then it should scroll
if (label[0].frame.size.width > self.frame.size.width){
for (int i = 1; i < NUM_LABELS; ++i){
label[i].hidden = NO;
}
[self scroll];
}else{
// Hide the other labels out of view
for (int i = 1; i < NUM_LABELS; ++i){
label[i].hidden = YES;
}
// Center this label
CGPoint center;
center = label[0].center;
center.x = self.center.x – self.frame.origin.x;
label[0].center = center;
}

}

– (void) setText: (NSString *) text
{
// If the text is identical, don’t reset it, otherwise it causes scrolling jitter
if ([text isEqualToString:label[0].text]){
// But if it isn’t scrolling, make it scroll
// If the label is bigger than the space allocated, then it should scroll
if (label[0].frame.size.width > self.frame.size.width){
[self scroll];
}
return;
}

for (int i=0; i<NUM_LABELS; ++i){
label[i].text = text;
}
[self readjustLabels];
}
– (NSString *) text
{
return label[0].text;
}

– (void) setTextColor:(UIColor *)color
{
for (int i=0; i<NUM_LABELS; ++i){
label[i].textColor = color;
}
}

– (UIColor *) textColor
{
return label[0].textColor;
}

– (void) setFont:(UIFont *)font
{
for (int i=0; i<NUM_LABELS; ++i){
label[i].font = font;
}
[self readjustLabels];
}

– (UIFont *) font
{
return label[0].font;
}

– (void) setScrollSpeed: (float)speed
{
scrollSpeed = speed;
[self readjustLabels];
}

– (float) scrollSpeed
{
return scrollSpeed;
}

– (void) setScrollDirection: (enum AutoScrollDirection)direction
{
scrollDirection = direction;
[self readjustLabels];
}

– (enum AutoScrollDirection) scrollDirection
{
return scrollDirection;
}

@end

使用方法
代码篇:

autoScrollLabel.text = @”Hi Mom! How are you? I really ought to write more often.”;
autoScrollLabel.textColor = [UIColor blackColor];//默认白色
如果使用XIB,那么你要创建一个UIScrollView,然后更改它的Class类别为AutoScrollLabel。

当文字不超过ScrollView的大小时不会滚动!

关于一些配置:

scrollDirection: 文字滚动方向(默认为水平滚动).
scrollSpeed: 设置每秒钟移动的像素. (默认30)
pauseInterval: 设置当文字到达后暂停时间. (默认0.5)
bufferSpaceBetweenLabels:设置文字结束和下一次文字出现的间隔.

【Android】APP检测版本升级更新、apk安装

在Android项目中,经常需要检测新版本,然后询问用户是否需要更新

为了方便代码复用,我把大部分代码放到一个类里,大概思路是,先从服务器获取版本信息,再与当前版本信息进行比较,若有新版本,弹出对话框询问是否更新,若更新就进行下载,下完后启动安装

主要用到HttpURLConnection,jsonObject,PackageManager,File,FileProvider等内容

1.MainActivity.java
这里主要调用封装好的类,但是一定要完成权限检测,读写权限需要用户授权。关于运行时权限的请求可以参考之前的文章

【Android】Android 6.0 获取危险权限、运行时权限、一次申请多个权限

public class MainActivity extends AppCompatActivity {
private boolean isCanWrite=false;

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

requestPermission_single();//请求授权
//检查更新
CheckUpdate checkUpdate = new CheckUpdate(this,isCanWrite);
checkUpdate.checkUpdate();

private void requestPermission_single(){
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}else{
Log.e(“请求权限”, “requestPermission_single: 已经获得读写权限” );
isCanWrite = true;
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

if (grantResults.length >0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(getApplicationContext(),”已获取读写权限!!”,Toast.LENGTH_SHORT).show();
isCanWrite = true;
CheckUpdate checkUpdate = new CheckUpdate(this,isCanWrite);
checkUpdate.checkUpdate();
}else {
Toast.makeText(getApplicationContext(),”您拒*了读写权限!!”,Toast.LENGTH_SHORT).show();
}

}
}

2.CheckUpdate.java
public class CheckUpdate extends AppCompatActivity {
private Context context;//上下文对象
private String version,whatisnew,downloadurl;//从服务器获取的版本、说明、下载链接等信息
private ProgressBar mProgressBar;//下载进度条
private int mProgress;//下载的进度
private boolean isCancel = false;//取消下载
private boolean isCanWrite; //是否拥有读写权限
private String savePath;//存储路径
private Dialog downloadDialog;//显示下载的对话框

public CheckUpdate(Context context,boolean isCanWrite){
this.context = context;
this.isCanWrite = isCanWrite;
}

public void checkUpdate(){
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try{
URL url = new URL(“http://XXXXXXX.php”);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(“GET”);
connection.setConnectTimeout(5000);
InputStream in = connection.getInputStream();
reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line=reader.readLine()) != null){
response.append(line);
}
dealUpdateInfo(response.toString());
}catch (Exception e){e.printStackTrace();}
finally {
if(reader != null){
try{reader.close();}catch (Exception e){e.printStackTrace();}
}
if(connection != null){
connection.disconnect();
}
}
}
}).start();
}

private void dealUpdateInfo(final String response){
//获取当前版本信息
runOnUiThread(new Runnable() {
@Override
public void run() {
String version_json = response;
try{
//解析从服务器获取到的json数据,根据自己服务器返回数据的实际情况进行解析
JSONObject jsonObject = new JSONObject(version_json);
version = jsonObject.getString(“version”);
whatisnew = jsonObject.getString(“whatisnew”);
downloadurl = jsonObject.getString(“downloadurl”);

String currentVersion = packageName(context);
//比较版本,看是否有新版本
if(Float.parseFloat(version)>Float.parseFloat(currentVersion)){
AlertDialog.Builder dialog = new AlertDialog.Builder(context);
dialog.setTitle(“检测到新版本”);
dialog.setMessage(whatisnew);
dialog.setCancelable(true);
dialog.setPositiveButton(“下载”, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if(!isCanWrite){
Toast.makeText(context,”请先赋予存储权限!!”,Toast.LENGTH_SHORT).show();
}else{
showDownloadDialog();
Toast.makeText(context,”已经获得权限!!”,Toast.LENGTH_SHORT).show();
}

}
});
dialog.show();
}else {
Toast.makeText(context,”当前是*新版本”,Toast.LENGTH_SHORT).show();
}

}catch (Exception e){e.printStackTrace();}
}
});
}

//获取包名(版本名)
public static String packageName(Context context) {
PackageManager manager = context.getPackageManager();
String name = null;
try {
PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);
name = info.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return name;
}

//显示下载进度条
private void showDownloadDialog(){
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(“下载中”);
View view = LayoutInflater.from(context).inflate(R.layout.dialog_progress,null);
mProgressBar = view.findViewById(R.id.id_progress);
builder.setView(view);
builder.setNegativeButton(“取消”, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
isCancel = true;
}
});
downloadDialog = builder.create();
downloadDialog.show();

downloadAPK();
}

//下载文件
private void downloadAPK() {
new Thread(new Runnable() {
@Override
public void run() {
try{
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
String sdPath = Environment.getExternalStorageDirectory() + “/”;
//文件保存路径
savePath = sdPath + “ice_aFewWord”;

File dir = new File(savePath);
if (!dir.exists()){
dir.mkdir();
}
// 开始下载文件
HttpURLConnection conn = (HttpURLConnection) new URL(downloadurl).openConnection();
conn.connect();
InputStream is = conn.getInputStream();
int length = conn.getContentLength();

File apkFile = new File(savePath, version);
FileOutputStream fos = new FileOutputStream(apkFile);

int count = 0;
byte[] buffer = new byte[1024];
while (!isCancel){
int readData = is.read(buffer);
count += readData;
// 计算当前的下载进度
mProgress = (int) (((float)count/length) * 100);
// 更新进度条
updateProgressHandler.sendEmptyMessage(1);

// 下载完成
if (readData < 0){
updateProgressHandler.sendEmptyMessage(2);
break;
}
fos.write(buffer, 0, readData);
}
fos.close();
is.close();
conn.disconnect();
}
}catch(Exception e){
Log.e(“下载错误”, “run: “+e.toString() );
}
}
}).start();
}

private Handler updateProgressHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case 1:
mProgressBar.setProgress(mProgress);
break;
case 2:
downloadDialog.dismiss();
installAPK();
}
}
};

//安装下载好的apk文件
private void installAPK(){
File apkFile = new File(savePath,version);
if(apkFile.exists()){
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

Uri uri;
//若SDK版本大于等24,需要FileProvider才行,否则报错
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//记得修改com.xxx.fileprovider与androidmanifest相同
uri = FileProvider.getUriForFile(context,”com.ice.ice_aFewWord.fileprovider”,apkFile);
intent.setDataAndType(uri,”application/vnd.android.package-archive”);
}else{
uri = Uri.parse(“file://” + apkFile.toString());
}
intent.setDataAndType(uri, “application/vnd.android.package-archive”);
context.startActivity(intent);
}
}

}

注意:上面的代码使用到了FileProvider,需要在res文件夹新建xml,以及在AndroidManifest中进行配置,详情可参考之前的文章

Android】解决Android 7.0及以上版本文件暴露异常exposed beyond app through Intent.getData()的方法
————————————————

iOS 开发 — Swift (九) 构造函数

构造函数基础
构造函数是一种特殊的函数,主要用来在创建对象时初始化对象,为对象成员变量设置初始值,在 OC 中的构造函数是 initWithXXX,在 Swift 中由于支持函数重载,所有的构造函数都是 init

构造函数的作用

分配空间 alloc
设置初始值 init

必选属性
自定义 Person 对象
class Person: NSObject {

/// 姓名
var name: String
/// 年龄
var age: Int
}
提示错误 Class ‘Person’ has no initializers -> ‘Person’ 类没有实例化器s

原因:如果一个类中定义了必选属性,必须通过构造函数为这些必选属性分配空间并且设置初始值

重写 父类的构造函数
/// `重写`父类的构造函数
override init() {

}
提示错误 Property ‘self.name’ not initialized at implicitly generated super.init call -> 属性 ‘self.name’ 没有在隐式生成的 super.init 调用前被初始化

手动添加 super.init() 调用
/// `重写`父类的构造函数
override init() {
super.init()
}
提示错误 Property ‘self.name’ not initialized at super.init call -> 属性 ‘self.name’ 没有在 super.init 调用前被初始化

为比选属性设置初始值
/// `重写`父类的构造函数
override init() {
name = “张三”
age = 18

super.init()
}
小结
非 Optional 属性,都必须在构造函数中设置初始值,从而保证对象在被实例化的时候,属性都被正确初始化
在调用父类构造函数之前,必须保证本类的属性都已经完成初始化
Swift 中的构造函数不用写 func
子类的构造函数
自定义子类时,需要在构造函数中,首先为本类定义的属性设置初始值
然后再调用父类的构造函数,初始化父类中定义的属性
/// 学生类
class Student: Person {

/// 学号
var no: String

override init() {
no = “001”

super.init()
}
}
小结
先调用本类的构造函数初始化本类的属性
然后调用父类的构造函数初始化父类的属性
Xcode 7 beta 5之后,父类的构造函数会被自动调用,强烈建议写 super.init(),保持代码执行线索的可读性
super.init() 必须放在本类属性初始化的后面,保证本类属性全部初始化完成
Optional 属性
将对象属性类型设置为 Optional
class Person: NSObject {
/// 姓名
var name: String?
/// 年龄
var age: Int?
}
可选属性不需要设置初始值,默认初始值都是 nil
可选属性是在设置数值的时候才分配空间的,是延迟分配空间的,更加符合移动开发中延迟创建的原则

iOS 单元测试及自动化测试(只看这篇就够了)

前言

单元测试及自动化测试(小白和大神都一定要了解的知识)

一、怎么运行测试类
有三种运行这个测试类的方法:

1、Product\Test 或者 Command-U。这实际上会运行所有测试类。

2、点击测试导航器中的箭头按钮。

%title插图%num
3、点击中缝上的钻石图标。

4、你还可以点击某个测试方法上的钻石按钮单独测试这个方法,钻石按钮在导航器和中缝上都有。

二、怎么查看覆盖率
iOS UnitTest单元测试覆盖率(Code Coverage)

默认情况下是不会显示覆盖率的

设置显示覆盖率前后的对比图

%title插图%num
那怎么显示覆盖率呢?方法如下图:

%title插图%num

三、测试类怎么编写(一、Test)
测试方法的名字总是以 test 开头,后面加上一个对测试内容的描述。

将测试方法分成 given、when 和 then 三个部分是一种好的做法:

在 given 节,应该给出要计算的值。
在 when 节,执行要测试的代码。
在 then 节,将结果和你期望的值进行断言,如果测试失败,打印指定的消息。
点击中缝上或者测试导航器上的钻石图标。App 会编译并运行,钻石图标会变成绿色的对勾!

注意:Given-When-Then 结构源自 BDD(行为驱动开发),是一个对客户端友好的、更少专业术语的叫法。另外也可以叫做 Arrange-Act-Assert 和 Assemble-Activate-Assert。

1、什么叫脱离UI做单元测试

如果你要测试方法是写在view上,那么你单元测试的时候,就不可避免的需要引入这个view。

以你把你把一个获取初始登录账号的方法写在了LoginViewController为例。

#import “LoginViewController.m”

– (NSString *)getLastLoginUserName {
return @”Beyond”;
}

那么你的单元测试必须就会有如下view的引入。这就叫无法脱离view做单元测试。

//每个test方法执行之前调用,在此方法中可以定义一些全局属性,类似controller中的viewdidload方法。
– (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
self.loginViewController = [[LoginViewController alloc] init];
}

//每个test方法执行之后调用,释放测试用例的资源代码,这个方法会每个测试用例执行后调用。
– (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
self.loginViewController = nil;
}

//测试用例的例子,注意测试用例一定要test开头
– (void)testGetLastLoginUserName {
NSString *lastLoginUserName = [self.loginViewContoller getLastLoginUserName];
XCTAssertEqual(lastLoginUserName, @”Beyond”, @”上次登录账号不是Beyond”);
}

那么怎么才能做到脱离view层做单元测试呢?
答:你可以将该方法写在胖Model中,或写在Helper中,或写在ViewModel中。

四、测试类怎么编写(二、UITest)
①、新建类
②、声明方法:一定要以test开头
③、将光标放在自定义的测试方法中,录制宏按钮变成红色,点击它,程序就会自动启动,这时候在程序中所有的操作都会生成相应的代码,并将代码放到所选的测试方法体内。

注意:录制的代码不一定正确,需要自己调整,
如:
app.tables.staticTexts[@”\U5bf9\U8c61”],需要将@”\U5bf9\U8c61”改成对应的中文,不然测试运行的时候会因匹配不了而报错。

五、UITest例子
附:例子1登录

– (void)testLogin {
XCUIApplication *app = [[XCUIApplication alloc] init];

if (app.navigationBars.count) {
XCUIElement *navigationBar = [app.navigationBars elementBoundByIndex:0];

XCUIElement *mainMineButton = navigationBar.buttons[@”main mine”];
XCUIElement *mainMessageButton = navigationBar.buttons[@”main message”];
BOOL isMainViewController = [mainMineButton exists] && [mainMessageButton exists];
if (isMainViewController) {
[mainMineButton tap];

XCUIElement *button = [[app.tables containingType:XCUIElementTypeImage identifier:@”mine_arrow_right”] childrenMatchingType:XCUIElementTypeButton].element;
[button tap];

// 进入个人中心了
XCUIElement *logoutButton = app.buttons[@”退出登录”];
[logoutButton tap];

//XCUIElement *logoutCancelButton = app.buttons[@”取消”];
//[logoutCancelButton tap];
//[logoutButton tap];

XCUIElement *logoutOKButton = app.buttons[@”确定”];
[logoutOKButton tap];

sleep(2);
}
}

// 设置用户名
XCUIElement *userNameTextField = app.textFields[@”用户名”];
[userNameTextField tap];
if (userNameTextField.value) {
NSLog(@”清空初始用户名:%@”, userNameTextField.value);
XCUIElement *userNameClearTextButton = userNameTextField.buttons[@”Clear text”];
[userNameClearTextButton tap];
}
[userNameTextField typeText:@”Beyond”];

// 设置密码
XCUIElement *passwordTextField = app.secureTextFields[@”密码”];
[passwordTextField tap];
[passwordTextField typeText:@”Pass1234″];

BOOL loginCondition = userNameTextField.isSelected && passwordTextField.isSelected;
XCTAssertTrue(loginCondition == NO, @”遇到问题了,检测不通过”);

XCUIElement *loginButton = app.buttons[@”登录”];
[loginButton tap];
// for (NSInteger i = 0; i < 5; i++) {
// [loginButton tap];
// }

//sleep(5);
XCTAssertTrue([self isMainViewController:app], @”成功登录首页”);
}

– (BOOL)isMainViewController:(XCUIApplication *)app {
if (app.navigationBars.count) {
XCUIElement *navigationBar = [app.navigationBars elementBoundByIndex:0];

XCUIElement *mainMineButton = navigationBar.buttons[@”main mine”];
XCUIElement *mainMessageButton = navigationBar.buttons[@”main message”];
BOOL isMainViewController = [mainMineButton exists] && [mainMessageButton exists];
return isMainViewController;

} else {
return NO;
}
}

知识点:

//在当前页面寻找与“用户名”有关系的输入框

XCUIElement *userNameTextField = app.textFields[@”用户名”];
1
//获取焦点成为*响应者,否则会报“元素(此textField)未调起键盘”错误

[userNameTextField tap];
1
//获取文本框的值

NSLog(@”初始用户名:%@”, userNameTextField.value);
1
//为此textField键入字符串

[userNameTextField typeText:@”Beyond”];
1
附:例子2列表(下拉刷新上拉加载等)

#import “STDemoUITestCase.h”

@interface STDemoOrderUITests : STDemoUITestCase {

}
@property (nonatomic, strong) XCUIApplication *app;
@property (nonatomic, strong) XCUIElement *todoStaticText;
@property (nonatomic, strong) XCUIElement *doingStaticText;
@property (nonatomic, strong) XCUIElement *doneStaticText;

@end

@implementation STDemoOrderUITests

– (void)setUp {
// Put setup code here. This method is called before the invocation of each test method in the class.

// In UI tests it is usually best to stop immediately when a failure occurs.
self.continueAfterFailure = NO;

// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
self.app = app;

// In UI tests it’s important to set the initial state – such as interface orientation – required for your tests before they run. The setUp method is a good place to do this.
XCUIElement *segmentScrollView = nil;
for (NSInteger i = 0; i < app.scrollViews.count; i++) {
XCUIElement *scrollView = [app.scrollViews elementBoundByIndex:i];
if (scrollView.staticTexts.count == 3) {
segmentScrollView = scrollView;
break;
}
}
XCTAssertNotNil(segmentScrollView);

XCUIElement *todoStaticText = [segmentScrollView.staticTexts elementBoundByIndex:0];
XCUIElement *doingStaticText = [segmentScrollView.staticTexts elementBoundByIndex:1];
XCUIElement *doneStaticText = [segmentScrollView.staticTexts elementBoundByIndex:2];
self.todoStaticText = todoStaticText;
self.doingStaticText = doingStaticText;
self.doneStaticText = doneStaticText;
}

– (void)changeSegmentIndex:(NSInteger)segmentIndex {

}

– (void)testOrderRefresh {
XCUIApplication *app = self.app;
XCUIElement *todoStaticText = self.todoStaticText;
XCUIElement *doingStaticText = self.doingStaticText;
XCUIElement *doneStaticText = self.doneStaticText;

[todoStaticText tap];
[doingStaticText tap];
[doneStaticText tap];
sleep(2);

[todoStaticText tap];
XCUIElement *table1 = [app.tables elementBoundByIndex:0];
[table1 swipeDown];
[table1 swipeDown];
[table1 swipeDown];
[table1 swipeUp];
sleep(2);

[table1 swipeLeft];
sleep(2);

[table1 swipeDown];
[table1 swipeUp];
sleep(2);

}

@end

六、定位元素
先说明本节包含知识点有如下大三点:
1、UITest类名介绍
2、元素获取方法
3、定位元素

要知道怎么定位元素和元素操作前,我们先了解以下一些元素的基本概念。

1、UITest类名介绍

XCTest一共提供了三种UI测试对象

①、XCUIApplication 当前测试应用target
②、XCUIElementQuery 定位查询当前UI中xctuielement的一个类
③、XCUIElement UI测试中任何一个item项都被抽象成一个XCUIElement类型

1.1、app元素

XCUIApplication *app = [[XCUIApplication alloc] init];
1
这里的app获取的元素,都是当前界面的元素。

app将界面的元素按类型存储,在集合中的元素,元素之间是平级关系的,按照界面顺序从上往下依次排序(这点很重要,有时很管用);元素有子集,即如一个大的view包含了多个子控件。常见的元素有:staticTexts(label)、textFields(输入框)、buttons(按钮)等等。
1
在Tests中如下代码有效,在UITests中,如下代码无效

UIViewController *rootViewController = UIApplication.sharedApplication.keyWindow.rootViewController;
UIViewController *viewController = [UIViewControllerCJHelper findCurrentShowingViewController];
1
2
1.2、元素集合(元素下面还是有元素集合)

XCUIApplication* app = [[XCUIApplicationalloc] init];
1
//获得当前界面中的表视图

XCUIElement* tableView = [app.tables elementBoundByIndex:0];
XCUIElement* cell = [tableView.cells elementBoundByIndex:0];

//元素下面还是有元素集合,如cell.staticTexts

XCTAssert(cell.staticTexts[@”Welcome”].exists);
1
1.3、界面事件

自动化测试无非就是:输入框、label赋值,按钮的点击、双击,页面的滚动等事件

1.3.1、点击事件tap

[app.buttons[@”确认”] tap];
1
1.3.2、输入框的赋值

[[app.textFields elementBoundByIndex:i] typeText:@“张三”];
1
当测试方法执行结束后,模拟器的界面就进入后台了,为了不让它进入后台,可以在方法结尾处下一个断点。这时候的app正在运行中,只要这个测试方法没有结束,我们可以进行别的操作的(不一定就要按照代码来执行)。

2、元素获取方法

@property (nonatomic, strong) XCUIApplication *app;

2.1 顺序获取

// 方法①、按顺序,适合identify变化,一般我们采用这种方法
XCUIElement *todoStaticText = [segmentScrollView.staticTexts elementBoundByIndex:0];
XCUIElement *doingStaticText = [segmentScrollView.staticTexts elementBoundByIndex:1];
XCUIElement *doneStaticText = [segmentScrollView.staticTexts elementBoundByIndex:2];

顺序获取以下两种方法是等价的

XCUIElementQuery *navigationBarItems = navigationBar.buttons;
XCUIElementQuery *navigationBarItems = [navigationBar childrenMatchingType:XCUIElementTypeButton];

XCUIElement *navigationBar = [self.app.navigationBars elementBoundByIndex:0];
XCUIElement *navigationBar = self.app.navigationBars.allElemenstBoundByIndex[0];

2.2 identify 获取

// 方法②、按id,当标签不变的情况下
XCUIElement *todoStaticText = segmentScrollView.staticTexts[@”待配送”];
XCUIElement *doingStaticText = segmentScrollView.staticTexts[@”配送中”];
XCUIElement *doneStaticText = segmentScrollView.staticTexts[@”已配送”];

3、定位元素

要获取到元素,我们的前提是要定位到元素的层次。

3.1 意识定位
3.1.1 获取 app

– (void)setUp {
// Put setup code here. This method is called before the invocation of each test method in the class.

// In UI tests it is usually best to stop immediately when a failure occurs.
self.continueAfterFailure = NO;

// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
self.app = app;

// In UI tests it’s important to set the initial state – such as interface orientation – required for your tests before they run. The setUp method is a good place to do this.

3.1.2 如何获取 UITabBarController 的 Item

NSArray<XCUIElement *> *tabBars = self.app.tabBars.allElementsBoundByIndex;
XCUIElement *tabBar = tabBars[0];
XCUIElementQuery *tabBarItems = [tabBar childrenMatchingType:XCUIElementTypeButton];
XCUIElement *tabBarItem1 = [tabBarItems elementBoundByIndex:0];
XCUIElement *tabBarItem2 = [tabBarItems elementBoundByIndex:1];
XCUIElement *tabBarItem3 = [tabBarItems elementBoundByIndex:2];
XCUIElement *tabBarItem4 = [tabBarItems elementBoundByIndex:3];
[tabBarItem1 tap];
[tabBarItem2 tap];
[tabBarItem3 tap];
[tabBarItem4 tap];

3.1.3 如何获取 UISegmentControl 的 label

XCUIElement *segmentScrollView = nil;
for (NSInteger i = 0; i < app.scrollViews.count; i++) {
XCUIElement *scrollView = [app.scrollViews elementBoundByIndex:i];
if (scrollView.staticTexts.count == 3) {
segmentScrollView = scrollView;
break;
}
}
XCTAssertNotNil(segmentScrollView);

XCUIElement *todoStaticText = [segmentScrollView.staticTexts elementBoundByIndex:0];
XCUIElement *doingStaticText = [segmentScrollView.staticTexts elementBoundByIndex:1];
XCUIElement *doneStaticText = [segmentScrollView.staticTexts elementBoundByIndex:2];
self.todoStaticText = todoStaticText;
self.doingStaticText = doingStaticText;
self.doneStaticText = doneStaticText;

[self.todoStaticText tap];
[self.doingStaticText tap];
[self.doneStaticText tap];

3.1.4 如何获取导航栏及其上的按钮

XCUIElement *navigationBar = [self.app.navigationBars elementBoundByIndex:0];

XCUIElement *mainMineButton = navigationBar.buttons[@”main mine”];
[mainMineButton tap];

XCUIElementQuery *navigationBarItems = navigationBar.buttons;
//XCUIElementQuery *navigationBarItems = [navigationBar childrenMatchingType:XCUIElementTypeButton];
XCUIElement *backButton = [navigationBarItems elementBoundByIndex:0];

3.1.5 如何 label

// 单击 label
XCUIElement *tapStaticText = self.app.staticTexts[@”单击”];
[tapStaticText tap];

XCUIElement *todoStaticText = segmentScrollView.staticTexts[@”待配送”];

3.1.6 如何获取 button

// 单击 button
XCUIElement *tapButton = self.app.buttons[@”确定”];
[tapButton tap];

3.1.7 如何获取 textField 及 其上的值

XCUIElement *userNameTextField = self.app.textFields[@”用户名”];
NSLog(@”用户名:%@”, userNameTextField.value);

3.1.8 如何获取 textField 的 删除键

// 设置用户名
XCUIElement *userNameTextField = self.app.textFields[@”用户名”];
[userNameTextField tap];
if (userNameTextField.value) {
NSLog(@”清空初始用户名:%@”, userNameTextField.value);
XCUIElement *userNameClearTextButton = userNameTextField.buttons[@”Clear text”];
[userNameClearTextButton tap];
}
[userNameTextField typeText:userName];

3.1.9 如何获取 keyboard 的 return 键

// 键盘
XCUIElement *keyboard = [self.app.keyboards elementBoundByIndex:0];
// 键盘 search 键
XCUIElement *keyboardSerch = keyboard.buttons[@”Search”];

3.2 调试定位

以一个标着”1″到”5″标签五个单元的表为例。如下图:

 

当触摸带有标签”3″的单元时候你可以打印如下的日志(为了清晰显示,这里忽略一些关键字输出):

(lldb) po app.tables.element.cells[@”Three”]
1
Query chain:
→Find: Target Application
↪︎Find: Descendants matching type Table
Input: {
Application:{ {0.0, 0.0}, {375.0, 667.0} }, label: “Demo”
}
Output: {
Table: { {0.0, 0.0}, {375.0, 667.0} }
}
↪︎Find: Descendants matching type Cell
Input: {
Table: { {0.0, 0.0}, {375.0, 667.0} }
}
Output: {
Cell: { {0.0, 64.0}, {375.0, 44.0} }, label: “One”
Cell: { {0.0, 108.0}, {375.0, 44.0} }, label: “Two”
Cell: { {0.0, 152.0}, {375.0, 44.0} }, label: “Three”
Cell: { {0.0, 196.0}, {375.0, 44.0} }, label: “Four”
Cell: { {0.0, 240.0}, {375.0, 44.0} }, label: “Five”
}
↪︎Find: Elements matching predicate “”Three” IN identifiers”
Input: {
Cell: { {0.0, 64.0}, {375.0, 44.0} }, label: “One”
Cell: { {0.0, 108.0}, {375.0, 44.0} }, label: “Two”
Cell: { {0.0, 152.0}, {375.0, 44.0} }, label: “Three”
Cell: { {0.0, 196.0}, {375.0, 44.0} }, label: “Four”
Cell: { {0.0, 240.0}, {375.0, 44.0} }, label: “Five”
}
Output: {
Cell: { {0.0, 152.0}, {375.0, 44.0} }, label: “Three”
}

观察输出结果:在*个输入/输出循环中的 -table 方法返回了填充在这个 iphone6 模拟器屏幕里面的列表(table)。再往下就是 -cells 方法返回了所有的单元(cell)。*终,文本查询仅仅在*后返回了一个元素。如果你没有在输出的*后看到带”Output”关键字的输出,说明框架没有找到你想要的元素。

3.3 认识控件的identifier及如何设置

如果控件是 UILabel 、UITextFiled 或者 UIButton 等可以设置 text 的控件,那么其 identifier 就是 text。

其实不管控件是否可以设置 text,都是可以通过 accessibilityIdentifier 设置的。

UILabel *userNameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
userNameLabel.text = @”张三”;
userNameLabel.accessibilityIdentifier = @”userNameLabel”;

则userNameLabel的identifier就由本来的text值”张三”,变成了accessibilityIdentifier值”userNameLabel”;

identifier *好设置成英文,中文的话会被转码,不好找!!!

设置完accessibilityIdentifier后,怎么通过accessibilityIdentifier找到要找的控件。答,可以通过打印allElementsBoundByAccessibilityElement值。

NSLog(@”GS: tabBars%@”,_app.tabBars.allElementsBoundByAccessibilityElement);
NSLog(@”GS: segmentedControls%@”,_app.segmentedControls.allElementsBoundByAccessibilityElement);

七、元素操作
1、点击

太简单了,略

2、视图变化

①刚开始是什么都没处理,直接干;
②后来发现明明OK的,却测试不通过;然后就临时采用了sleep;
③再后来终于找到了精确判断的方法。如同单元测试的异步处理一样;

刚开始*想想到的是sleep,但是sleep短,还是无效。而sleep长,则必然造成每个自动化测试所消耗的时间延长,而且还不一定就都OK。所以*后的方法如下:

// “STDemoUITestCase.h”
@interface STDemoUITestCase : XCTestCase {

}
– (void)waitElement:(XCUIElement *)element untilVisible:(BOOL)visible;

@end

@implementation STDemoUITestCase

– (void)waitElement:(XCUIElement *)element untilVisible:(BOOL)visible {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@”exists == %ld”, visible ? 1 : 0];
[self expectationForPredicate:predicate evaluatedWithObject:element handler:nil];
[self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) {
//NSString *message = @”Failed to find \(element) after 30 seconds.”;
//[self recordFailureWithDescription:message inFile:__FILE__ atLine:__LINE__ expected:YES];
}];
}

@end

所以*终的判断方法如下:

– (void)testWaitViewVisible {
XCUIElement *passwordTextField = self.app.secureTextFields[@”密码”];
[self waitElement:passwordTextField untilVisible:YES];
}

 

附:在测试这个异步方法的时候,遇到过一个奇怪的问题。原来的测试代码如下:

– (void) testWaitViewVisible {
XCUIElement *passwordTextField = self.app.secureTextFields[@”密码”];

NSPredicate *predicate = [NSPredicate predicateWithFormat:@”exists == 1″];//正确空格

[self expectationForPredicate:predicate evaluatedWithObject:passwordTextField handler:nil];
[self waitForExpectationsWithTimeout:2.0 handler:nil];
}

不知道为什么应该测试通过的,却一直在执行到NSPredicate *predicate = [NSPredicate predicateWithFormat:@”exists的时候就崩溃了。百思不得其解。后来通过复制代码及search才发现是如下图所示问题。

其他属性判断请认真查看XCUIElement类及属性和方法的英文注释。
如判断登录button是否enable。

– (void)waitElement:(XCUIElement *)element untilEnable:(BOOL)enable {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@”hittable == %ld”, enable ? 1 : 0];
[self expectationForPredicate:predicate evaluatedWithObject:element handler:nil];
[self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
//NSString *message = @”Failed to find \(element) after 30 seconds.”;
//[self recordFailureWithDescription:message inFile:__FILE__ atLine:__LINE__ expected:YES];
}];
}

附2:以下几个别人也遇到的异步处理的文章,处理方式和本文所讲一样。可略过

– Delay/Wait in a test case of Xcode UI testing
– How to use expectationForPredicate with a XCUIElementQuery UISwitch
– Xcode UI Testing Tip:Delay/Wait

 

附3:UI Testing in Xcode 7这是一篇从上述Delay/Wait in a test case of Xcode UI testing的问题,别人的回答中,找到的一篇UITest的文章。写得很不错,很全,建议看。

八、WebDriverAgent的使用
在进行下节《使用Appium进行iOS的自动化测试》前,我们先了解WebDriverAgent的使用,因为《使用Appium进行iOS的自动化测试》中需要替换Appium中的WebDriverAgent;

先讲下模拟器下的使用:

1、到WebDriverAgent下载*新版本的WebDriverAgent
2、进入下载后的WebDriverAgent文件
3、执行 ./Scripts/bootstrap.sh
4、直接用Xcode打开WebDriverAgent.xcodepro文件
5、连接并选择自己的iOS设备,然后按Cmd+U,或是点击Product->Test
6、运行成功时,在Xcode控制台应该可以打印出一个Ip地址和端口号。
7、在网址上输入http://(iP地址):(端口号)/status,如果网页显示了一些json格式的数据,说明运行成功。

如果真机的话,还需要配置配置WebDriverAgentLib和WebDriverAgentRunner的证书。

appium官网iOS真机问题:https://github.com/appium/appium-xcuitest-driver/blob/master/docs/real-device-config.md

九、使用Appium进行自动化测试
需要

1、安装Appium-Desktop
2、安装appium-doctor
3、更新Appium中的WebDriverAgent
4、安装Appium-Python-Client

2、appium-doctor的安装

2.1、检查是否安装appium-doctor是否安装了,以及与iOS相关配置是否完整

执行appium-doctor –ios指令,查看appium-doctor的安装,以及与iOS相关配置是否完整。如下图,执行后发现未找到命令即未安装。

%title插图%num
2.2、未安装appium-doctor时,进行安装
则我们需要执行sudo npm install appium-doctor -g来进行appium-doctor的安装

%title插图%num
附:如果你忘了添加sudo,只是执行npm install appium-doctor -g的话,会出现如下错误

%title插图%num
2.3、安装后,检查是否是否真的安装了以及与iOS相关配置是否完整

appium-doctor安装后,我们再执行appium-doctor –ios指令,查看appium-doctor是否真的安装了,以及与iOS相关配置是否完整。如果有那一项是打叉的,则进行安装就可以了。如下图发现Xcode Command Line Tools未安装。

%title插图%num
则我们Fix it选择YES,发现还是一样的问题,就自己执行xcode-select –install进行安装。
控制台执行xcode-select –install,在弹出的弹框中选择“安装”,即可进入下载和安装了,安装过程如下图:

%title插图%num
安装成功后,再执行xcode-select –install其会提示我们已经安装了。同时如果执行sudo npm install appium-doctor -g其也会告诉我们appium-doctor与iOS的相关配置也安装成功了。

%title插图%num
3、更新Appium中的WebDriverAgent

进入到Appium中的WebDriverAgent目录/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-xcuitest-driver/,将自己下载并编译后的WebDriverAgent替换Appium原有的WebDriverAgent

4、安装python

因为我们后面是用py脚本文件执行自动化测试,所以需要安装python。

执行python –version检查python是否安装,如果未安装请执行brew install python安装

%title插图%num
5、安装Appium-Python-Client

因为我们的py脚本文件中有from appium import webdriver

%title插图%num
所以,我们需要安装Appium-Python-Client。如果未安装就去执行py文件,则会出现ImportError: No module named appium错误,如下图:

%title插图%num
所以,请确保在执行py脚本文件前,你的

5.1、下载python-client源码Appium-Python-Client是安装了的。

cd /Users/lichaoqian/Desktop
git clone git@github.com:appium/python-client.git

不要加了加sudo

%title插图%num

执行成功如图:

%title插图%num
6、执行脚本

执行python appiumSimpleDemo.py遇到的问题:

%title插图%num
原因是没有安装 libimobiledevice,导致Appium无法连接到iOS的设备。
在介绍怎么安装libimobiledevice前,我们先看看若安装好libimobiledevice后,其执行的结果又是什么?截图如下:

在此之前,我还遇到的问题有ImportError: cannot import name _remove_dead_weakref,如下截图:

%title插图%num
这里我原以为只要执行“更新p就可以了,即:

%title插图%num如果其已经是*新版本,则其提示如下:

%title插图%num

brew install python已经是*新.png

这时候我们去执行总不会报那个表示python版本的问题了吧。然而,实际上它的结果还是和之前一样,可是我们明明已经安装了*新的python了,为什么还是错误,到底问题出在哪里。经过一番摸索,才发现原来它执行的是python@2,而不是python,所以我就尝试着要不先去删掉python@2看看是错误吧。删除的命令如下:brew uninstall –ignore-dependencies python@2

%title插图%num

删除成功后,再执行一遍python appiumSimpleDemo.py命令。这时候的结果变为如下:

%title插图%num

可以看到这时候它调用的就是python命令,而不是python@2了。
解决了python后,这时候还有另一个问题,即图上的Original error: Could not initialize ios-deploy make sure it is installed (npm install -g ios-deploy) and works on your system.。它的意思就是缺少了ios-deploy。
为什么需要ios-deploy呢?因为如果我们要在iOS10+的系统上使用appium,则需要安装ios-deploy。
显然我们肯定需要在iOS10+的系统上使用appium,所以我们根据它的提示npm install -g ios-deploy去安装ios-deploy即可(不要高兴得太早)。然而它提供的命令并不能完全让我们安装成功。如下图:

%title插图%num

你肯定猜到了是sudo的问题吧,不过这里比较特殊,就是即使你加上sudo,即执行的是sudo npm install -g ios-deploy也还是无法成功。那正确的完整的命令应该是怎么样的呢?答:这个问题的解决方法在
https://github.com/phonegap/ios-deploy/issues/188中可以找到,其实就是sudo npm install -g ios-deploy –unsafe-perm=true。执行后,如下图所示:

%title插图%num

好了,解决了这个问题后,我们再回头来执行下py脚本,看看还有什么问题没。
执行如下,

%title插图%num

从图上可以看出,我们终于成功了。。。是的,你成功了。而且你看你的手机,你会发现在这个脚本的执行过程中,你的手机是在自动化测试的。
6.1、libimobiledevice的安装
执行brew install libimobiledevice –HEAD命令,进行libimobiledevice的安装。

%title插图%num

根据错误提示,我们执行在终端继续sudo chown -R $(whoami) /usr/local/share/man/man3 /usr/local/share/man/man5 /usr/local/share/man/man7命令。执行成功后,回头执行之前执行没成功的brew install libimobiledevice –HEAD命令,进行libimobiledevice的安装。可以发现这时候它就正常安装了。如下图:

%title插图%num

但执行过程中,当执行到./autogen.sh的时候又发现另外一个问题,如下图:

%title插图%num

这又是什么原因呢?(PS:Requested ‘libusbmuxd >= 1.1.0’ but version of libusbmuxd is 1.0.10这个问题,您可能在用Flutter的时候也会遇到,如果遇到解决方法跟这边一样。)

我们仔细看,会发现异常所在Requested ‘libusbmuxd >= 1.1.0’ but version of libusbmuxd is 1.0.10,很显然是由于系统要求的*libusbmuxd *版本和所要安装的版本不一致。那怎么解决呢?其实很简单。只要把旧的卸载了,装个新的就是了。
卸载命令为:brew uninstall –ignore-dependencies usbmuxd
安装命令为:brew install –HEAD usbmuxd

如:

%title插图%num

这时候再去执行brew install libimobiledevice –HEAD命令,成功的截图如下:

%title插图%num
软件测试是IT相关行业中*容易入门的学科~不需要开发人员烧脑的逻辑思维、不需要运维人员24小时的随时待命,需要的是细心认真的态度和IT相关知识点广度的了解,每个测试人员从入行到成为专业大牛的成长路线可划分为:软件测试、自动化测试、测试开发工程师 3个阶段。

如果你不想再体验一次自学时找不到资料,没人解答问题,坚持几天便放弃的感受的话,可以加我们的软件测试交流群 313782132 ,里面有各种软件测试资料和技术交流。

android 实现app内部检测*新版本 自动升级到*新版本

app现在基本都有版本更新这个功能,实现起来也很简单

截图效果:

%title插图%num
1. 获取当前app的版本号

/**
* 获取版本号
*
* @throws PackageManager.NameNotFoundException
*/
public static String getVersionName(Context context) throws PackageManager.NameNotFoundException {
// 获取packagemanager的实例
PackageManager packageManager = context.getPackageManager();
// getPackageName()是你当前类的包名,0代表是获取版本信息
PackageInfo packInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
String version = packInfo.versionName;
return version;
}
2.根据版本号名称判断版本高低
/**
* 版本号比较
*0代表相等,1代表version1大于version2,-1代表version1小于version2
* @param version1
* @param version2
* @return
*/
public static int compareVersion(String version1, String version2) {
if (version1.equals(version2)) {
return 0;
}
String[] version1Array = version1.split(“\\.”);
String[] version2Array = version2.split(“\\.”);
int index = 0;
// 获取*小长度值
int minLen = Math.min(version1Array.length, version2Array.length);
int diff = 0;
// 循环判断每位的大小
while (index < minLen
&& (diff = Integer.parseInt(version1Array[index])
– Integer.parseInt(version2Array[index])) == 0) {
index++;
}
if (diff == 0) {
// 如果位数不一致,比较多余位数
for (int i = index; i < version1Array.length; i++) {
if (Integer.parseInt(version1Array[i]) > 0) {
return 1;
}
}

for (int i = index; i < version2Array.length; i++) {
if (Integer.parseInt(version2Array[i]) > 0) {
return -1;
}
}
return 0;
} else {
return diff > 0 ? 1 : -1;
}
}
3.从服务器获取*新版本号
/**
* 从服务器获取版本*新的版本信息
*/
private void getVersionInfoFromServer(){
//模拟从服务器获取信息 模拟更新王者荣耀
versionInfoBean = new VersionInfoBean(“1.1.1″,”http://dlied5.myapp.com/myapp/1104466820/sgame/2017_com.tencent.tmgp.sgame_h162_1.33.1.8_9c4c7f.apk”,”1.修复若干bug\n\n2.新增图片编辑功能”
,getExternalCacheDir()+”/1.1.1.jpg”);
SharedPreferences sharedPreferences = getSharedPreferences(“data”,MODE_PRIVATE);
sharedPreferences.edit().putString(“url”,versionInfoBean.getDownloadUrl()).commit();
sharedPreferences.edit().putString(“path”,versionInfoBean.getPath()).commit();//getExternalCacheDir获取到的路径 为系统为app分配的内存 卸载app后 该目录下的资源也会删除
//比较版本信息
try {
int result = Utils.compareVersion(Utils.getVersionName(this),versionInfoBean.getVersionName());
if(result==-1){//不是*新版本
showDialog();
}else{
Toast.makeText(MainActivity.this,”已经是*新版本”,Toast.LENGTH_SHORT).show();
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}

}

4.弹出dialog提示更新
private void showDialog(){
final Dialog dialog = new Dialog(MainActivity.this);
LayoutInflater inflater = (LayoutInflater)MainActivity.this
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
TextView version,content;
Button left,right;
View view = inflater.inflate(R.layout.version_update,null,false);
version = (TextView)view.findViewById(R.id.version);
content = (TextView)view.findViewById(R.id.content);
left = (Button)view.findViewById(R.id.left);
right = (Button)view.findViewById(R.id.right);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
content.setText(Html.fromHtml(versionInfoBean.getDesc(),Html.FROM_HTML_MODE_LEGACY));
}else{
content.setText(Html.fromHtml(versionInfoBean.getDesc()));
}
content.setMovementMethod(LinkMovementMethod.getInstance());
version.setText(“版本号:”+versionInfoBean.getVersionName());
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
left.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
}
});
right.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
downloadNewVersionFromServer();

}
});
dialog.setContentView(view);
dialog.setCancelable(false);
Window dialogWindow = dialog.getWindow();
dialogWindow.setGravity(Gravity.CENTER);
//dialogWindow.setWindowAnimations(R.style.ActionSheetDialogAnimation);
WindowManager.LayoutParams lp = dialogWindow.getAttributes();
WindowManager wm = (WindowManager)
getSystemService(Context.WINDOW_SERVICE);
lp.width =wm.getDefaultDisplay().getWidth()/10*9;
dialogWindow.setAttributes(lp);
dialog.show();
}

5.启动service后台下载
/**
* 启动服务后台下载
*/
private void downloadNewVersionFromServer(){
if(new File(versionInfoBean.getPath()).exists()){
new File(versionInfoBean.getPath()).delete();
}
Toast.makeText(MainActivity.this,”开始下载…”,Toast.LENGTH_SHORT).show();
LoadingService.startUploadImg(this);
}

6.定义广播接受者接受下载状态
声明变量isLoading表示下载状态
/**
* 定义广播接收者 接受下载状态
*/
public class MyReceive extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(“android.intent.action.loading_over”.equals(action)){
isLoading = false;
}else if(“android.intent.action.loading”.equals(action)){
isLoading = true;
}
}
}
7.创造service
public class LoadingService extends IntentService {
private HttpUtils httpUtils;
NotificationManager nm;
private String url,path;
private SharedPreferences sharedPreferences;
public LoadingService(String name) {
super(name);
}
public LoadingService() {
super(“MyService”);

}

public static void startUploadImg(Context context)
{
Intent intent = new Intent(context, LoadingService.class);
context.startService(intent);
}

public void onCreate() {
super.onCreate();
httpUtils = new HttpUtils();
httpUtils.configCurrentHttpCacheExpiry(0);
nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
sharedPreferences = getSharedPreferences(“data”,MODE_PRIVATE);
}

@Override
protected void onHandleIntent(@Nullable Intent intent) {
updateApk();
}

//开始下载apk 网络请求使用的是xutils框架
private void updateApk(){
url = sharedPreferences.getString(“url”,””);
path = sharedPreferences.getString(“path”,””);

httpUtils.download(url,
path , new RequestCallBack<File>() {
@Override
public void onLoading(final long total, final long current,
boolean isUploading) {
createNotification(total,current);
sendBroadcast(new Intent().setAction(“android.intent.action.loading”));//发送正在下载的广播
super.onLoading(total, current, isUploading);
}

@Override
public void onSuccess(ResponseInfo<File> arg0) {
nm.cancel(R.layout.notification_item);
Toast.makeText(LoadingService.this,”下载成功…”,Toast.LENGTH_SHORT).show();
installApk();//下载成功 打开安装界面
stopSelf();//结束服务
sendBroadcast(new Intent().setAction(“android.intent.action.loading_over”));//发送下载结束的广播
}

@Override
public void onFailure(HttpException arg0, String arg1) {
Toast.makeText(LoadingService.this,”下载失败…”,Toast.LENGTH_SHORT).show();
sendBroadcast(new Intent().setAction(“android.intent.action.loading_over”));//发送下载结束的广播
nm.cancel(R.layout.notification_item);
stopSelf();
}
});
}
/**
* 安装下载的新版本
*/
protected void installApk() {
Intent intent = new Intent();
intent.addCategory(Intent.CATEGORY_DEFAULT);
File file = new File(path);
Uri uri = Uri.fromFile(file);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, “application/vnd.android.package-archive”);
this.startActivity(intent);
}
//发送通知 实时更新通知栏下载进度
private void createNotification(final long total, final long current){
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);//必须要设置这个属性,否则不显示
RemoteViews contentView = new RemoteViews(this.getPackageName(),R.layout.notification_item);
contentView.setProgressBar(R.id.progress, (int)total, (int)current, false);
builder.setOngoing(true);//设置左右滑动不能删除
Notification notification = builder.build();
notification.contentView = contentView;
nm.notify(R.layout.notification_item,notification);//发送通知
}
ok 大功告成
————————————————

Android 实现自动更新及强制更新功能

引言
目的不言而喻,肯定是为了自己版本的迭代更新。想做到版本的完全控制手段还是比较多的。今天我要分享的这个方法是用蒲公英提供的版本查询接口和版本下载接口来做的。有条件的同学也可以在自己的服务端开这两个接口。

需求分析
我们要想实现这个自动更新的功能大致分三步:

查询线上版本号,然后拿本地版本号与之对比。
若线上版本号比本地版本号大,则下载线上版本号
把下载好的版本号安装,并替换当前旧版本
权限
根据上面的需求我们可以知道,需要用到的权限应该有网络权限、本地文件写入权限,本地文件读取权限。使用网络权限去获取线上的版本号,然后下载保存到本地,安装的时候再去本地取来。

<uses-permission android:name=”android.permission.INTERNET” />
<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”/>
<uses-permission android:name=”android.permission.READ_EXTERNAL_STORAGE”/>

步骤
根据上面的需求,我们一步一步来实现这个功能。首先在APP的入口处去检测版本号。

ProUtils.canUpdata(this);
1
在这里我把检测方法给封装到了utils中,这样用起来也方便。

1,检测线上版本
public void check() {
//当所用app前版本号
int codeversin = getVersion();
getLineVersion(checkurl, codeversin);
}

这个方法里面包含了两步,*步是去获取本地版本号,第二步是去获取线上版本号
获取本地版本号:

public int getVersion() {
PackageInfo pkg;
int versionCode = 0;
String versionName = “”;
try {
pkg = activity.getPackageManager().getPackageInfo(activity.getApplication().getPackageName(), 0);
versionCode = pkg.versionCode;

} catch (PackageManager.NameNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return versionCode;
}

获取线上版本号:

OkHttpClient okHttpClient = new OkHttpClient();
RequestBody formBody = new FormBody.Builder()
.add(“_api_key”, BuildConfig.PYAPIKEY)
.add(“appKey”, BuildConfig.PYAPPID)
.build();
Request request = new Request.Builder()
.url(url)
.post(formBody)
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {

@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {

}

});

在上面有两个KEY,这两个可以都是从蒲公英上取来的。就像引言所说,若有条件可以自己搭建。

版本比较
在上面请求成功后,在response中就会获取到我们的线上版本号。然后我们拿上一步中获取到的线上版本号和本地版本号来做对比:

if (lineVersion > nowcodeversin) {
//去弹窗提示用户
}

这个时候,如果比较的结果是线上版本比较大,则去下载线上版本

提示用户
我们要给用户提示的,当然你若想直接下载也可以,只不过为了用户体验而已,自己斟酌。
提醒用户

AlertDialog dialog = new AlertDialog.Builder(activity).setTitle
(“Tips”).setMessage(“Have new version,please update!”)
.setNeutralButton(“Cancel”, new DialogInterface
.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).setNegativeButton(“Update”, new DialogInterface
.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
pyUtils.downUrl();
}
}).show();
dialog.setCanceledOnTouchOutside(false);//可选
dialog.setCancelable(false);//可选

在这里我给了用户两种选择,一种是立即更新,一种是稍后再说。点击稍后,那就会在下次再出发的时候再去提醒用户。点击更新的话,会立刻开始下载应用的新版本。
如果你想强制用户去更新的话,可以把稍后的选项去掉,顺便把
dialog.setCanceledOnTouchOutside(false);//可选,点击dialog其它地方dismiss无效
dialog.setCancelable(false);//可选,点击返回键无效
这两项给加上,用户就不得不更新了。

下载
上面都做好了,那就下载吧,就是一个文件下载的方法:

public void downUrl() {
DownloadUtil.get().download(downUrl,activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(), new DownloadUtil.OnDownloadListener() {
@Override
public void onDownloadSuccess(File file) {
LogUtils.e(“AutoUpdate”,”下载完成去安装”);
}

@Override
public void onDownloading(int progress) {
}

@Override
public void onDownloadFailed() {
LogUtils.e(“AutoUpdate”,”下载失败”);
}
});

}

这个下载没什么好说的

安装
下载好之后,我们直接使用系统的方法去安装即可

调用系统的安装方法
private void installAPK(File savedFile) {
//调用系统的安装方法
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri data;
// 判断版本大于等于7.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// “net.csdn.blog.ruancoder.fileprovider”即是在清单文件中配置的authorities
data = FileProvider.getUriForFile(activity, “com.thinker.member.bull.android_bull_member.fileprovider”, savedFile);
// 给目标应用一个临时授权
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
LogUtils.e(“AutoUpdate”,”7.0data=”+data);
} else {
data = Uri.fromFile(savedFile);
LogUtils.e(“AutoUpdate”,”111data=”+data);
}
intent.setDataAndType(data, “application/vnd.android.package-archive”);
activity.startActivity(intent);
activity.finish();
}

在这里需要注意,有个7.0的方法。需要在你的项目清单文件中配置如下

<!– 解决apk安装失败的问题 –>
<provider
android:name=”android.support.v4.content.FileProvider”
android:authorities=”com.ligo.anomo.fileprovider”
android:exported=”false”
android:grantUriPermissions=”true”>
<meta-data
android:name=”android.support.FILE_PROVIDER_PATHS”
android:resource=”@xml/file_paths” />
</provider>

然后再资源res中添加xml/file_paths.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<resources xmlns:android=”http://schemas.android.com/apk/res/android”>
<paths>
<external-path path=”” name=”download”/>
</paths>
</resources>

基本就可以了。读书百卷,不如手写一遍。自己去试试吧。
————————————————

Android app自动更新总结

1.配置:

1.1 AndroidManifest.xml中添加权限和FileProvider:

  1. ——————————————————————————————————————–
  2. <uses-permission android:name=“android.permission.INTERNET”/>
  3. <uses-permission android:name=“android.permission.READ_EXTERNAL_STORAGE”/>
  4. <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE”/>
  5. <uses-permission android:name=“android.permission.REQUEST_INSTALL_PACKAGES” />
  6. ——————————————————————————————————————–
  7. <provider
  8. android:name=“androidx.core.content.FileProvider”
  9. android:authorities=“com.fengzhi.wuyemanagement.fileprovider”
  10. android:grantUriPermissions=“true”
  11. android:exported=“false”>
  12. <meta-data
  13. android:name=“android.support.FILE_PROVIDER_PATHS”
  14. android:resource=“@xml/file_paths” />
  15. </provider>

1.2 新建文件(路径:res\xml\file_paths.xml):

  1. <paths>
  2. <external-path path=“.” name=“external_storage_root” />
  3. </paths>

1.3 (app的)build.gradle:

  1. implementation “com.lzy.net:okgo:3.0.4”//okgo 网络请求
  2. implementation ‘com.google.code.gson:gson:2.8.2’//gson
  3. implementation “org.permissionsdispatcher:permissionsdispatcher:4.3.1”//权限
  4. annotationProcessor “org.permissionsdispatcher:permissionsdispatcher-processor:4.3.1”//权限

2.这里以点击按钮进行更新为例:

2.1 核心代码:

  1. private int version;
  2. /* 更新进度条 */
  3. private ProgressBar mProgress;
  4. private AlertDialog mDownloadDialog;
  5. ——————————————————————————————————————–
  6. //点击按钮,检查权限,,,检查更新的方法
  7. @NeedsPermission({Manifest.permission.READ_EXTERNAL_STORAGE,
  8. Manifest.permission.WRITE_EXTERNAL_STORAGE,
  9. Manifest.permission.REQUEST_INSTALL_PACKAGES})
  10. protected void checkUpdate() {
  11. showLoadingDialog(“检测更新中…”);
  12. version = AppUpdateUtil.getAppVersionCode(this);//检查当前版本号
  13. // 调用方法,,,接口的具体实现,接收传过来的参数,再调自己的方法,
  14. requestAppUpdate(version, new DataRequestListener<UpdateAppBean>() {
  15. @Override
  16. public void success(UpdateAppBean data) {
  17. // 返回的json,getStatus为0时,去下载apk文件,这里是下载apk文件的方法
  18. updateApp(data.getData().getApk_url());
  19. }
  20. @Override
  21. public void fail(String msg) {
  22. // 返回的json,getStatus为1时,提示:”已是*新版本!”
  23. SToast(msg);
  24. dismissLoadingDialog();
  25. }
  26. });
  27. }
  28. //检查版本号,*次请求(post),,,UpdateAppBean根据服务器返回生成
  29. private void requestAppUpdate(int version, final DataRequestListener<UpdateAppBean> listener) {
  30. OkGo.<String>post(Const.HOST_URL + Const.UPDATEAPP).params(“version”, version).execute(new StringCallback() {
  31. @Override
  32. public void onSuccess(Response<String> response) {
  33. Gson gson = new Gson();
  34. UpdateAppBean updateAppBean = gson.fromJson(response.body(), UpdateAppBean.class);
  35. if (updateAppBean.getStatus() == 0) {
  36. listener.success(updateAppBean);
  37. } else {
  38. listener.fail(updateAppBean.getMsg());
  39. }
  40. }
  41. @Override
  42. public void onError(Response<String> response) {
  43. listener.fail(“服务器连接失败”);
  44. dismissLoadingDialog();
  45. }
  46. });
  47. }
  48. //如果有新版本,提示有新的版本,然后下载apk文件
  49. private void updateApp(String apk_url) {
  50. dismissLoadingDialog();
  51. DialogUtils.getInstance().showDialog(this, “发现新的版本,是否下载更新?”,
  52. new DialogUtils.DialogListener() {
  53. @Override
  54. public void positiveButton() {
  55. downloadApp(apk_url);
  56. }
  57. });
  58. }
  59. //下载apk文件并跳转(第二次请求,get)
  60. private void downloadApp(String apk_url) {
  61. OkGo.<File>get(apk_url).tag(this).execute(new FileCallback() {
  62. @Override
  63. public void onSuccess(Response<File> response) {
  64. String filePath = response.body().getAbsolutePath();
  65. Intent intent = IntentUtil.getInstallAppIntent(mContext, filePath);
  66. // 测试过这里必须用startactivity,不能用stratactivityforresult
  67. mContext.startActivity(intent);
  68. dismissLoadingDialog();
  69. mDownloadDialog.dismiss();
  70. mDownloadDialog=null;
  71. }
  72. @Override
  73. public void downloadProgress(Progress progress) {
  74. // showDownloadDialog();
  75. // mProgress.setProgress((int) (progress.fraction * 100));
  76. if (mDownloadDialog == null) {
  77. // 构造软件下载对话框
  78. AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
  79. builder.setTitle(“正在更新”);
  80. // 给下载对话框增加进度条
  81. final LayoutInflater inflater = LayoutInflater.from(mContext);
  82. View v = inflater.inflate(R.layout.item_progress, null);
  83. mProgress = (ProgressBar) v.findViewById(R.id.update_progress);
  84. builder.setView(v);
  85. mDownloadDialog = builder.create();
  86. mDownloadDialog.setCancelable(false);
  87. mDownloadDialog.show();
  88. }
  89. mProgress.setProgress((int) (progress.fraction * 100));
  90. }
  91. });
  92. }

2.2 DataRequestListener:

  1. public interface DataRequestListener<T> {
  2. //请求成功
  3. void success(T data);
  4. //请求失败
  5. void fail(String msg);
  6. }

2.3 AppUpdateUtil:

  1. /**
  2. * 获取App版本码
  3. *
  4. * @param context 上下文
  5. * @return App版本码
  6. */
  7. public static int getAppVersionCode(Context context) {
  8. return getAppVersionCode(context, context.getPackageName());
  9. }

2.4 IntentUtil:

  1. public class IntentUtil {
  2. /**
  3. * 获取安装App(支持7.0)的意图
  4. *
  5. * @param context
  6. * @param filePath
  7. * @return
  8. */
  9. public static Intent getInstallAppIntent(Context context, String filePath) {
  10. //apk文件的本地路径
  11. File apkfile = new File(filePath);
  12. if (!apkfile.exists()) {
  13. return null;
  14. }
  15. Intent intent = new Intent(Intent.ACTION_VIEW);
  16. Uri contentUri = FileUtil.getUriForFile(context, apkfile);
  17. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  18. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  19. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  20. }
  21. intent.setDataAndType(contentUri, “application/vnd.android.package-archive”);
  22. return intent;
  23. }

2.5 FileUtil:

  1. /**
  2. * 将文件转换成uri(支持7.0)
  3. *
  4. * @param mContext
  5. * @param file
  6. * @return
  7. */
  8. public static Uri getUriForFile(Context mContext, File file) {
  9. Uri fileUri = null;
  10. if (Build.VERSION.SDK_INT >= 24) {
  11. fileUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + “.fileprovider”, file);
  12. } else {
  13. fileUri = Uri.fromFile(file);
  14. }
  15. return fileUri;
  16. }