`编程`分类下的文章

编程

UIWindow 之坑

Context

做一个Pop View有两种方法.

1.用一个UIView盖在当前UIView或者UIWindow的最上面

2.做一个UIWindow,让他变成keyWindow

第一种的好处是…没想到什么特别的好处,大概写起来简单吧.

第二种的好处是,可以扔个ViewController在里面,哈哈哈.

而且第二种,我觉得做起来结构比较清晰.这个Pop View里面做的事情,可以全部封装好,包括显示和消失.
而使用UIView的话,做显示就一定需要把superview作为参数传入,才可以显示,这个还是蛮麻烦的,特别是当我
想做一个全局接收通知,并在UI上显示的东西的时候.由于很难确定当前最顶上的UIView是什么,所以还是UIWindow比较好.

Bug

然而,在水壶架iOS端中,遇到了一个很奇怪的问题.这个问题貌似只在iOS9.3.5之前的系统上出现.对于之后的系统,没有出现这个问题.

从Leancloud的错误报告中看到,这是一个消息传到了空指针的fetal错误.

那就好好找找,为啥变成了空指针吧.

Come on, Where are you

由于一开始这个问题只在9.3.5的手机上出现了,而我和一大票iOS10都没有这个问题.所以我甚至怀疑,这个是手机的问题.

直到我发现所有的老系统,都爆了!我才慌了.

当弹出这个搜索框,再让他消失的时候,做任何的触摸操作,整个app就挂掉了.

所以,问题肯定就在dismiss方法里面!

-(void)disappear
{
    [UIView animateWithDuration:0.3f animations:^{
        self.view.alpha=0;
    } completion:^(BOOL finished) {
        _displayWindows.rootViewController=nil;
            _displayWindows.hidden=YES;
            _displayWindows=nil;
        [[BluetoothManager sharedManager]stopScan];
        [BluetoothManager sharedManager].delegate=nil;
        if (self.delegate) {
            self.delegate=nil;
        }
    }];
}

每次disappear之后,我就中断了整个app,查看他的UI结构,发现有一个不听话的UIWindow还停留在哪儿~

而我明明都把他设置为hidden了,按理说它已经不是keyWindow了~可是他貌似还在.

而且为什么这个东西在iOS 10就没问题了呢~

于是我就去查了查iOS 10的开发这手册,看看他们都做了些什么改变,导致iOS 10不会闪退.

扯远了.

所以说,当这个alert级的UIWindow设置为hidden的时候,没有key window了……

于是乎Touch Event进来的时候,不知道应该给谁了,就爆掉了……

所以所以,得把之前的UIWindow重新恢复key window的地位.

之后改成这样了:

-(void)disappear
{
    [UIView animateWithDuration:0.3f animations:^{
        self.view.alpha=0;
    } completion:^(BOOL finished) {
        _displayWindows.windowLevel=UIWindowLevelNormal;
        _displayWindows.rootViewController=nil;
        [self.view removeFromSuperview];
            [_displayWindows setHidden:YES];
            _displayWindows=nil;
        [[BluetoothManager sharedManager]stopScan];
        [BluetoothManager sharedManager].delegate=nil;
        if (self.delegate) {
            self.delegate=nil;
        }
    }];
    if (_preWindows) {
        [_preWindows makeKeyAndVisible];
        _preWindows=nil;
    }
}

当然这样一改,在show的时候也要做一些修改了~得把之前的UIWindow给记录下来,不然就懵逼咯.

阅读剩下更多

编程

Block--方便而危险

Context

Block真的很好用对不对?记得很早以前用ASIHttp来做网络请求的时候,得用delegate,然后每次请求的
delegate回调中都要做一下判断,才能知道这个回调是回应了具体哪一次请求,这还是蛮麻烦的.

不过Block出现了,对网络请求的response操作,就可以直接写在block中,不用去判断这个response谁否对应与当前这个request.

除了网络请求,Alert的回调啊,asyncsocket啊,之类的异步操作,用了block都特别的方便.

Situation

然而,有一天我们在炫轮iOS端的DIY界面中发现了一个bug,而这个bug预示着,
我有实例没有成功释放.

Reason

于是我就一行一行的找,感觉没有什么地方出现.但是我发现了一个现象:

我在使用SCLAlertView了之后,就出现了这个bug,所以我就重点查看SCLAlertView的相关代码,
经过排查,我觉得问题只能出现在dismissBlock上.查看了dismissBlock的声明代码,他是个strong~

我想,这里应该是出现了一个循环引用…block中的内容,在声明的时候,会copy一份当时的数据.当AlertView 消失的时候,
调用了dismissBlock,然而,dismissBlock中的内容有引用当前的ViewController,所以.

他们互相引用了!

所以只能在dismissBlock的最后加一行,把这个dismissBlock设置为nil.解决了~

End

这次要不是遇到了这个坑,说不定永远也不会去仔细看Block实现的原理~

关键就是这个copy,所以在block中如果引用了(强引用了该block的)类,就一定要注意,检查会不会出现循环引用!

阅读剩下更多

编程

蛋疼的GATT权限

Context

定位水壶架App的iOS端已经完成了80%,在考完矩阵之前,只要把android端的网络和蓝牙交互做完,就可以没什么太大的压力咯.

然而,在搞android端的时候,还是遇到了很多很多问题的.

第一坑

几天前,当我把iOS端蓝牙配对的代码,翻译为Java版本之后,本来想,肯定会轻轻松松,正正常常的运行了.

但是,在连接上定位器之后,总是会卡在搜索服务或者写入xxx这个阶段,然后就断线了.

当时,我觉得,可能是一些小问题,可能是我写漏了什么东西,所以就卡住了.

也怀疑过是Google的亲儿子Nexus蓝牙硬件不给力~

然后就痛苦的去复习了几天矩阵理论…………

但是,就在两个小时之前,我一步一步调试的时候,发现了一个奇怪的现象.

public void onServicesDiscovered(BluetoothGatt gatt, int status);

BluetoothGattCallback中,搜索到service的时候,竟然没有我们自己的service!

所以for循环就那么做完了……什么service都没有找到,最后就被关闭连接了.

这时候,我开始怀疑人生了

难道说,这蓝牙是为iOS设计的!?

赶紧整了一个锤子过来测测.发现锤子是可以找到所有的service的……

难道说,这蓝牙不是为Google设计的!?

这时候,我突然想到了一个问题.和之前几天的实验,可能有点儿关系.

很久很久的几天前

添哥找我说,蓝牙可以设置一个加密密钥,这样子传输数据,就是加密的了.(虽然说,蓝牙和WiFi一样,是要连接之后才能通信的,但是,无线嘛,肯定是要被窃听的)

于是我们就试验了一下,但是蓝牙加密之后,需要手动在手机上点击配对按钮,才可以进行加密通信.

由于android端,这个配对不是特别的明显,不像iOS那么粗暴地弹出来.所以我们最后没有使用这个方案.

这里还可以顺便说说加密

小时候,以为加密的意思就是,给数据加了一把锁.只要用密码把锁打开了,就可以访问数据了.(真单纯)

原来,加密是把数据都给翻译了一遍.即使知道加密解密的算法,使用错误的密钥,可以得到数据,但是这数据,肯定是错的.

当蓝牙加密通信的时候,会分配一个密钥.咱们配对蓝牙鼠标,蓝牙键盘的时候,也常常看到,需要输入个什么pin码,才能配对.一旦这配对了,之后通信就是加密的了.

(假设蓝牙键盘不用配对,不加密通信……)那就是无线的按键记录器啊!

总而言之

这搜索不出我们需要的service就是这个配对的,当我进入系统蓝牙设置,把这个设备的配对给取消了之后,就正常了~.~

第二坑的Context

BLE的GATT的Characteristic有权限控制.

大概就是Read,Write,WriteWithoutResponse,Notify这四种常见权限.

他们用一个8位的flag来控制.因为不是重点,所以懒得讲了.

第二坑

在我解决了Service Discover的问题了之后,我觉得,这下一定可以顺利地跑下去了!

结果卡在了一个写入之后的读取上…

本来,在写入一个数据之后,需要去读取一个反馈的.但是不知道为什么,没有去读取这个数据.

仔细阅读Monitor里面的log.可以发现,之前的读取,都会调用系统的readCharacteristic()函数,而这一次的读取却没有读取,这是为啥子嘞?

权限!

我们有一个Characteristic在写入之后,需要读取反馈.所以需要Read的权限.

然而,在这个Characteristic没有Read的权限的情况下,iOS端竟然还是可以正确的读取到数据!

而Android直接忽略的readCharacteristic()这个函数调用……

虽然说,这硬件是我们自己设计的,讲道理的话,在连接设备之后,并不需要去检查各种权限的.(除非这个硬件是被山寨的)

但是,加上一个权限验证的话,或许可以提高一些软件的稳健性呢?说不定可以让这个系统可以在遇到山寨货的情况下,可以继续正常的工作下去呢~

End

2016马上要结束了,每年都能更加深入的了解一些蓝牙,真的是好刺激!

阅读剩下更多

编程

初用Volley

Context

水壶架iOS端终于撸完了

由于我Android的界面写的烂的一逼,所以只能拜托xiaopo来帮我撸界面了.

首要目标是干掉蓝牙类,不过蓝牙绑定设备的时候会需要网络请求.

那就顺便把网络也给撸掉吧~

基本用法

1.需要一个请求队列

RequestQueue mQueue = Volley.newRequestQueue(context);

2.好了直接撸请求吧

Form-Data这么搞

StringRequest stringRequest = new StringRequest("一个URL",new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
               Log.d("Network OK", response);
        }
        }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
               Log.e("Network Fail", error.getMessage(), error);
        }
});

JSON这么搞

JsonObjectRequest jsonObjectRequest = new JsonObjectRequest("http://m.weather.com.cn/data/101010100.html", null,
    new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            Log.d("Network OK", response.toString());
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            Log.e("Network Fail", error.getMessage(), error);
        }
    });

Q1 请求是form-data 响应是json

不过这里遇到了一个问题.

我们服务器大佬坑我,请求只能解析Form-Data的,返回的响应是Json

于是只能手动转换了

private static StringRequest FormDataRequest(String url, int method, Map parameter, VKNetworkSucceedBlock succeedBlock, VKNetworkFailBlock failBlock) {
    StringRequest request=new StringRequest(method, url, new Response.Listener<String>() {

        @Override
        public void onResponse(String response) {
            try {
                JSONObject jsonResponse = new JSONObject(response);
                if (jsonResponse.getInt("status") == 10086) {
                    //认证失败
                    failBlock.requestFail(new Exception("SessionExpired"));
                } else {
                    succeedBlock.getResponse(jsonResponse);
                }
            } catch (JSONException e) {
                e.printStackTrace();
                failBlock.requestFail(e);
            }
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            if (error.networkResponse.statusCode == 10000) {
                //认证失败
                failBlock.requestFail(new Exception("SessionExpired"));
            } else
                failBlock.requestFail(error);
        }}){
        @Override
        protected Map<String, String> getParams() throws AuthFailureError {
            return parameter;
        }
    };
    queue().add(request);
    return request;
}

用JSONObject自带的牛逼的初始化方法来解析响应

好不容易,把网络请求跑通了.

才发现,一个劲的403.

原来,Volley本身不处理Cookie的!!

啊席巴

于是就要手动处理

private static StringRequest FormDataRequest(String url, int method, Map parameter, VKNetworkSucceedBlock succeedBlock, VKNetworkFailBlock failBlock) {
    StringRequest request=new StringRequest(method, url, new Response.Listener<String>() {

        @Override
        public void onResponse(String response) {
           //这些你都看过了
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            //这些你都看过了
            }){
        @Override
        protected Map<String, String> getParams() throws AuthFailureError {
            return parameter;
        }

        @Override
        public Map<String, String> getHeaders() throws AuthFailureError {
            Map<String,String> header=super.getHeaders();
            String cookie=getCookie();
            if (cookie!=null){
                header.put("Set-Cookie",cookie);
                //手动从把Cookie放到Header中
            }
            return header;
        }

        @Override
        protected Response<String> parseNetworkResponse(NetworkResponse response) {
            refreshCookie(response.headers);
                //手动从Response的Header中获取Cookie
            return super.parseNetworkResponse(response);
        }
    };
    queue().add(request);
    return request;
}

这其中的Cookie持久化什么的,就不说了.它不是重点.

而重点是……

Final 我放弃了

当Response的Header中有多个Set-Cookie字段时,只会被解析出来一个!

如果需要把所有的Cookie都拿出来的话,需要修改Volley源码!

然而,我使用Gradle来添加Volley工程的,所以,你逗我吗!

于是乎,我就放弃了.

姜还是老的辣

大三的时候做精益防伪,使用了Android-Async-Http

这个很牛逼,功能很强大,还自动缓存了Cookie,真是替我省心.

还好前面撸接口的时候,封装的比较好,所以没有改动太多的东西.

Tip

封装了两个基本网络操作函数

public static void getMethodFormData(String url, Map<String,String> parameter, VKNetworkSucceedBlock succeedBlock, VKNetworkFailBlock failBlock){
    getViaLoopj(url,parameter,succeedBlock,failBlock);
}

public static void postMethodFormData(String url, Map<String,String> parameter, VKNetworkSucceedBlock succeedBlock, VKNetworkFailBlock failBlock){
    postViaLoopj(url,parameter,succeedBlock,failBlock);
}

再来一发Android-Async-Http的实例(封装过了)

public static void postViaLoopj(String url, Map<String,String> params,VKNetworkSucceedBlock succeedBlock,VKNetworkFailBlock failBlock) {
    client.post(url, new RequestParams(params), new JsonHttpResponseHandler(){
        @Override
        public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
            if (statusCode==403){
                failBlock.requestFail(new Exception("SessionExpired"));
            }else
            {
                succeedBlock.getResponse(response);
            }
        }
        @Override
        public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
            super.onFailure(statusCode, headers, throwable, errorResponse);
            if (statusCode==403){
                failBlock.requestFail(new Exception("SessionExpired"));
            }else{
                failBlock.requestFail(new Exception(errorResponse.toString()));
            }
        }
        @Override
        public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
            super.onFailure(statusCode, headers, responseString, throwable);
            if (statusCode==403){
                failBlock.requestFail(new Exception("SessionExpired"));
            }else
                failBlock.requestFail(new Exception(responseString));
        }
    });
}

定义了两个interface

public interface VKNetworkSucceedBlock {
    void getResponse(JSONObject responseObject);
}

public interface VKNetworkFailBlock {
    void requestFail(Exception error);
}

于是乎做网络请求,就方便的很了

public static void mLogin(String email, String verify, boolean isChina, NetworkUtils.VKNetworkSucceedBlock succeed, NetworkUtils.VKNetworkFailBlock fail){
    HashMap<String,String> map=new HashMap<String, String>(){{
            put("hhh","123@163.com");
            put("jjj","3434");
            put("kkk",String.valueOf(1));
        }};
      NetworkUtils.postMethodFormData(UrlManager.URL_Login,map,succeed,fail);
}

最后网络的效果,就留到具体调用他的类去实现吧.

所以,封装了一遍就是好瞧把你能耐的,更换网络框架,不需要跑到每个Activity啊,Class里面去改.

response也是一套,

顶层完全不用去关注,底层的网络是如何实现的.(好像段老师还是郝老师这么说过)

当然我相信,来看这篇文章的大佬们,肯定早就知道这个了

阅读剩下更多

编程

用状态机来实现复杂操作

Context

这次新产品的蓝牙绑定过程有点儿复杂

与炫轮作比较

炫轮只有一个characteristic,不同的操作之间没有顺序关系,都是并列的

新产品有多个characteristic,每个characteristic有不同的意义和不同的指令,每个操作都有先后顺序

Solution

突然想起来,本科的微处理器,还是数字电路课,讲过状态机这个玩意儿.我觉得,可以拿来搞一搞.这样清晰明了,可以省去很多中间变量.

比如

BOOL isConnecting;
BOOL isDiscovering;
BOOL didWriteXXX;
BOOL didReadXXX;
.....

使用了状态机,并且规定好了状态转移的条件,那么,整个系统就一定在这个状态列表中滚.不会出现位置状态,导致程序跑飞.

因为从状态A–>状态B,只有当前条件是xxxx的时候才会实现.这时候不用再去管其他的变量,其他的事件.

使用了状态机,可以轻松地对已有逻辑进行扩充,因为只要拿出,状态转移图来.找到需要添加的地方,就能确定在代码中,有多少地方需要修改,如此一来,就不用在需要该需求的时候,重新阅读已有代码了!

使用了状态机,一般只需要一个记录状态的变量就够了,而且重写这个变量的Setter方法,还可以方便做一些其他的事情:

-(void)setStage:(VKLocatorPairState)stage{
    NSLog(@"pairing status %@",[PublicHelper localizedStringForPairStatus:stage]);
    //发送通知
    //如果是终止状态,发送完通知后,回到原始状态
    [[NSNotificationCenter defaultCenter]postNotificationName:kPairLocatorStatusUpdated object:@(stage)];
    if (stage==1) {  //透露太多了,要被周老板干掉的
        //透露太多了,要被周老板干掉的
    }
    if (stage==1) {  //透露太多了,要被周老板干掉的
        //透露太多了,要被周老板干掉的
    }
    if (stage==1) {  //透露太多了,要被周老板干掉的
        //透露太多了,要被周老板干掉的
    }
}

对,转移状态,和UI可以分开处理,免得以后要界面逻辑,还要去状态转移中找代码.

变量和类名都起得很不要脸啊,小伙子

总之

这一篇,看起来像硬广啊.

再总之

Android端也可以很轻松的做移植.

所以做Android的这个操作,差不多就是在做翻译工作,把Objective-C 翻译为 Java就好了.

阅读剩下更多

编程

Android上的蓝牙

Context

如果说Android和iOS一样,一出新版本,每个用户都回去升级,那我就不用写这篇咯

先说说iOS端的蓝牙扫描

@param serviceUUIDs A list of <code>CBUUID</code> objects representing the service(s) to scan for.
//serviceUUIDs是一个过滤NSArray
//根据蓝牙外设的广播信号中的Service UUID来过滤
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;

假定说,我们的app需要过滤两个产品的蓝牙信号.那么只要在serviceUUIDs里面填写他们特有Service UUID.

然后,在代理方法中做区分.

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI{
    // 从广播信号中获取Service UUID 的列表
    NSArray *serviceUUIDs=advertisementData[CBAdvertisementDataServiceUUIDsKey];
    //做一下判断
    if ([serviceUUIDs containsObject:[CBUUID UUIDWithString:@"FFFF"]]){
        //欧耶,这是我们的产品1
    }else if ([serviceUUIDs containsObject:[CBUUID UUIDWithString:@"FFFF"]]){
        //欧耶,这是产品2
        //直接else当然也可以.但是要保险一些嘛
    }
}

方便,真是方便

正题

Android中,貌似没有那么轻松

当然,这就要提起Context里面说的事情了.

BLE是android 4.3引入的,所以最早拥有蓝牙更能的版本,就是API 18.

大概是由于API 18中,蓝牙搜索的方法,有点儿蛋疼,所以,他们改了新的方法吧?

API 21

先看看API 21怎么搜索蓝牙的.

BluetoothAdapter的一个静态方法

void startScan (List<ScanFilter> filters,ScanSettings settings,ScanCallback callback)
参数 参数 解释
filters List ScanFilters for finding exact BLE devices.
settings ScanSettings Settings for the scan.
callback ScanCallback Callback used to deliver scan results.

其中ScanFilter很牛逼

Service UUIDs which identify the bluetooth gatt services running on the device.
Name of remote Bluetooth LE device.
Mac address of the remote device.
Service data which is the data associated with a service.
Manufacturer specific data which is the data associated with a particular manufacturer.

这过滤方法,可比iOS的搜索强多了

然而,这个是API 21才有的……

API 18

由于,咱们要覆盖所有的,拥有BLE功能的手机用户

所以,只能用API 18了.

boolean startLeScan (UUID[] serviceUuids, BluetoothAdapter.LeScanCallback callback);

这过滤,和iOS一样了,只能用serviceUUID来过滤.

不过,通过我的实际测试,我发现serviceUuids填写的不能是不同设备的service uuid.

这样做,总是会显示没有match uuid list (有机会再好好研究一下)

那过滤两种设备,可以选择,做两遍这个方法,给不同的callback,好像还省去了用if去判断设备这个工作.

但是,如果有一大排设备需要去过滤呢……

boolean startLeScan (BluetoothAdapter.LeScanCallback callback);

直接全部搜索吧……还过滤啥呀

手动解析广播数据,进行分类,做不同的操作

BluetoothAdapter.LeScanCallback bluetoothScanCallback = new BluetoothAdapter.LeScanCallback() {

    @Override
    public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
        //已经搜索过的设备,就不用重新解析啦
        if (locatorList.containsKey(bluetoothDevice.getAddress())){
            LocatorPeripheral peripheral=locatorList.get(bluetoothDevice.getAddress());
            peripheral.updateInformation(i);
        }else if(keyList.containsKey(bluetoothDevice.getAddress())){
            KeyPeripheral peripheral=keyList.get(bluetoothDevice.getAddress());
            peripheral.updateInformation(i);
        }else if(ignoreList.contains(bluetoothDevice)){
            return;
        } else {
            //这里开始重新解析
            VKPeripheral peripheral = VKPeripheral.createPeripheral(i,bytes,bluetoothDevice);
            if (peripheral instanceof LocatorPeripheral){
                //这是设备1
                locatorList.put(bluetoothDevice.getAddress(),(LocatorPeripheral) peripheral);
            }else if (peripheral instanceof KeyPeripheral){
                //这是设备2
                keyList.put(bluetoothDevice.getAddress(),(KeyPeripheral) peripheral);
            }else {
                //这不是咱们的东西
                ignoreList.add(bluetoothDevice);
                return;
            }
            //通知UI

        }
    }
};

我水平有限,所以就是这样写的~

这里有个VKPeripheral 类名也是直接从iOS版移植过来了

用来根据广播信息来新建我自己的蓝牙设备实例.

他生成的KeyPeripheral和LocatorPeripheral都是他的子类.如果不是我们需要的设备,就加到忽略的列表中去.

还有几个小坑

1.从android 6.0开始,蓝牙扫描如果需要得到结果,就必须获得定位权限(你逗我的吧…凭什么呀)

所以要把这个加到Manifest里面去

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

2.还是android 6.0的锅. 由于6.0可以动态获取权限,所以上面这个权限,直接写在Manifest里面,说不定还没用呢…
还要手动的去获取权限

找个Activity加上这一段(不唯一)

if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)!= PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION},1);
}else if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.BLUETOOTH)!=PackageManager.PERMISSION_GRANTED){
    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.BLUETOOTH,Manifest.permission.BLUETOOTH_ADMIN},1);
}else {
    //你是不是不想用这个App!!
}

End

哎,灵活的Android呀.开放的Android呀.

阅读剩下更多

编程

微信小网页的安全性

微信网页的安全性

这篇文章其实很早以前就可以写一下了,但是当时担心自己的方案不够安全,所以,没有敢写.

为啥子要做防御呢?

因为,我们这次的产品涉及到了比较隐私的信息,严重点儿说,还会涉及到用户的安全.

所以,至少在前端,不能让第三方可以轻松的获取到用户的信息.

由于这次做的网页,不是单网页程序,所以,每个页面所需要的初始数据,都是在URL中体现的,所以第一步需要对URL进行防御

当然,我们也不希望用户在微信外浏览网页.因为这样他们可以看到请求的参数,而且也不利于我们控制入口~

直接开始吧

一个网页需要获取到点击用户的openid,需要通过微信用户访问https://open.weixin.qq.com/connect/oauth2/authorize?appid=公众号ID&redirect_uri=回调URL

用户点击了授权,或者以前早就授权过了,才会访问跳转的URL,并且带着openid的参数

这一块好像没什么可以攻击的,所以我也没做什么处理

服务器收到了redirectURL的请求的时候,是可以获取到openid的.假设,直接根据openid来返回数据,那么微信所在的手机如果网络数据被抓包.就可以很轻松的知道了如何使用接口不使用HTTPS

那么伪造openid来获取他人的数据,也变得简单了~所以,我给他加了密,用了很多很多位的对称加密算法,把openid做了加密.每次请求都需要openid和eid(我称为加密的id…)
,配对正确,才可以正常的请求,否则就送一个403的大礼包.

这上面算是最后一道防线吧?

以前参与精益防伪项目的时候,学了好多的保密措施,感觉还是蛮有用的~

当然,这一套还存是在问题的.比如说现在的密钥是固定的,如果根据服务器的Mac来生成,或者再建立一个动态的密钥.就可以防止一个服务器被攻破之后,
另外的服务器也被轻松攻破的尴尬现象了.

当然咯

我们不希望用户在微信外防伪网页,最好的方法就是,阻止用户点击右上角的按钮,选择里面的复制链接,分享给朋友,用浏览器打开……

还好Wechat JS SDK里面就做了这个功能,可以控制微信浏览器,隐藏那些按钮~

各位大大如果找到了漏洞,一定要告诉我呀!这个关系到用户的人身安全的!

阅读剩下更多

编程

安卓初体验?

安卓初体验~

其实这并不算是初体验吧,毕竟大三的时候已经玩过android开发了,而且看起来也蛮像回事儿的.

只不过当时搞了NFC,业务逻辑和网络请求.剩下的最重要的界面和界面跳转,都是在鹅厂的望神帮我写的~

第一节课其实我没去

那一节课,据说老师讲了布局文件

然后好像就布置了一个计算器的作业,然后就没什么了.

反正,我就在界面编辑器里面拖控件.然后findViewById,之后给他们做了一些逻辑~

第二节课说是要做SD卡读取图片,并且显示出来

我一想,觉得有点儿难啊,于是就开始查官方文档去了,

整理了一下,获取文件夹的路径,文件夹内文件这些好像都不难.

选择一个内容显示估计也不难,但是如何做一个漂亮的选择页面,可能比较复杂.

我打算用GridView,做一个和相册一样.

Let’s do this

先搞了一个SDCardHelper的类(写iOS时候留下的习惯,各种Helper)

//外部储存是否可写
static public boolean isExternalStorageWritable()
//外部储存是否可读
static public boolean isExternalStorageReadable()
//储存路径
static public String storagePath()
//为GridView用的文件列表
static public List<GridItem> getFileList()

Android的GridView和iOS的UICollectionView好像差不太多

Android的GridView需要的适配器里面需要重写

public int getCount();
public Object getItem(int position);
public long getItemId(int position);
public View getView(int position, View convertView, ViewGroup parent);

iOS的UICollectionView需要实现DataSource协议的

-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

这样就可以正常显示一个GridView(UICollectionView)

可是,我要自定义控件呀!不然用原始的GridView中每个item的View或者UICollectionView中原始的UICollectionViewCell
,那得多难看…

在iOS中,只要写一个继承于UICollectionViewCell的类,对它进行界面操作,数据操作,并且在Protocol中复用Cell并且返回,就可以完成目的

在Android中,貌似没有类似UICollectionViewCell的东西.所以得自己重写一个类,用来持有Item的xml布局文件中的各个控件

import android.widget.ImageView;
import android.widget.TextView;

/**
 * Created by vikingwarlock on 16-9-23.
 * This is the class for view
 */
public class GridItemView {
    public ImageView imageView;
    public TextView filename;
}

之前的图片列表,也专门写了一个类,用来存放这个数据

import java.io.Serializable;

/**
 * Created by vikingwarlock on 16-9-23.
 * This is a class that represent a grid item
 */
public class GridItem implements Serializable{
    public String filepath;
    public String filename;
}

这里用了一个Serializable,是为了后面Intent传递用的~

最后在Adapter类里面实现View

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    GridItemView item;
    GridItem data = gridItemList.get(position);
    if (convertView == null) {
        convertView = inflater.inflate(R.layout.gridviewitem, null);
        item = new GridItemView();
        item.filename = (TextView) convertView.findViewById(R.id.itemName);
        item.imageView = (ImageView) convertView.findViewById(R.id.itemImage);
        convertView.setTag(item);
    } else {
        item = (GridItemView) convertView.getTag();
    }
    ImageLoader.getInstance().displayImage("file://"+data.filepath,item.imageView);
    item.filename.setText(data.filename);
    return convertView;
}

界面就差不多搞定了~

至于点击效果,iOS依然是实现一个Protocol~就可以了

-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;

Android需要手动添加

GridView gv = (GridView) findViewById(R.id.gridView);
if (gv != null) {
    gv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
            GridItem item = datalist.get(position);
            Intent intent=new Intent();
            intent.setClass(SDView.this,MainActivity.class);
            Bundle bundle=new Bundle();
            bundle.putSerializable("result",item);
            intent.putExtras(bundle);
            setResult(2,intent);
            finish();
        }
    });

这里的gv是GridView的意思,不要想歪了.

做了一堆和Bundle有关的事情,主要是为了Intent中可以传递一个对象,而不仅仅是字符串.

setResult()用来传递一个Intent回上一个需要Result的Activity.

finish()就是结束当前Activity.

这个花了我最多时间的东西就这样做完了~

当然啦,后面还遇到了图片显示的问题.比如卡住啊,比如OOM啊~最后借助了universal-image-loader
都解决了!!

最后坐在旁边的同学跟我说,他们的代码,只有几十行.原来老师给的demo中获取图片是这样写的

@Override
public void onClick(View v) {
    Intent intent = new Intent();
    intent.setType("image/*");
    /* 使用Intent.ACTION_GET_CONTENT这个Action */
    intent.setAction(Intent.ACTION_GET_CONTENT);
    /* 取得相片后返回本画面 */
    startActivityForResult(intent, 1);
}

我好生气!说好的操作SD卡呢!

这样的写法,和iOS中的UIImagePickerViewController一样了~

Anyway,至少我学会了自定义控件的GridView,体验了一把OOM,还学习了SDCard的操作

还望各位大大指导我!

阅读剩下更多

编程

BLE的广播帧

之前还以为BLE也就GATT那些东西嘛,没什么好说的了

其实

BLE或者说,蓝牙,的广播帧还是有很多很多奥秘的.还是有些东西可以拿来琢磨琢磨,学习学习

Context

至于为什么要突然学习这个东西,肯定是和新产品有关系的,具体的,可以等上市之后,买来一个试试看啊,哈哈哈哈哈哈

另一个原因,这个东西的官方文档真的很难找,至少在 www.bluetooth.com 上面我就没有看到讲述广播帧结构的文档,最后在TI的一个文档中,终于找到了相关的内容.

平时用BLE只要是在连接成功之后,搜索Service和Characteristc,后进行读写操作.这样的工作模式,只能一对一,不能做到一对多.但是利用了广播信息,可以做到一对多的发送消息…虽然这时候不能反过来写入.

Let’s go

我们先来上一张图片,看看Bluetooth到底广播了什么吧

AD

我是做上位机软件开发的,所以只关心应用层的东西,那么上面图片中,iOS和Android能被处理的数据是标红的那一段

一共31个字节,看起来好像不是很足够,但是由于协议是规定好了的,所以对应的位置表示对应的数据,还是够用的.

说说我们熟悉的一些广播信息吧:(iOS端CoreBluetooth的广播帧字典的Key)

1.LocalName (外设名称)
2.ServiceUUIDs (Service列表)
3.ManufacturerData (制造商数据)
4.IsConnectable (是否可连)

下面再给点儿我第一次知道的广播信息类型:

1.ServiceData (某个Service的广播数据)
2.TxPowerLevel (传输信号强度~.~,用来计算损耗用...好像暂时没啥用处,反正我也不会算)
3.OverflowServiceUUIDs (没懂他和ServiceUUIDs的区别~.~)
4.SolicitedServiceUUIDs (貌似和从设备没啥关系~.~)

说实话,平时用到的只有ServiceUUIDs,因为我们用这个来过滤出来我们需要的蓝牙外设,也用这个来区分不同的硬件.

LocalName的话iOS和Android都已经自动提取并封装成简单的接口了

上面是软件层面的东西,我们来看看硬件层面的呗~

一个广播帧会有很多类型的数据,每个类型有自己的type,上位机也是通过type字段来分辨数据是UUID还是Name或者其他的东西~

每一类数据都由3部分组成:

1.length of data (这个type的总长度)
2.type (定义type)
3.data (根据type的协议来填入数据)

这里简单的列举几个type:

0x02 不完整的16位UUID
0x04 不完整的32位UUID
0x06 不完整的128位UUID
0x08 设备全名
0x16 16位UUID的Service Data
0x20 32位UUID的Service Data
0x21 128位UUID的Service Data
0xFF 制造商自定义的数据

Example of Peripheral and Bluetooth Central

一台名叫VK的外设,公布了一个Service UUID 3344,5566;其中3344的Service Data是”0xAA,0xBB,0xCC”,制造商的自定义数据是”0x12,0x34,0x56,0x78”

那么他的广播数据是~

04 08 56 4B 05 02 44 33 66 55 06 16 44 33 cc bb aa 07 FF 00 00 78 56 34 12

拆分数据分析分析

04 08 56 4B
长度4 设备完整名称 V K

05 02 44 33 66 55
长度5 不完整的16位列表 3344 5566

06 16 44 33 cc bb aa
长度6 16位UUID的ServiceData UUID=3344 数据是 AA BB CC

07 FF 00 00 78 56 34 12
长度7 厂商自定义数据 厂商ID=0000 厂商自定义数据=12345678

如果有错误不要打我…

iOS端解析广播数据

-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
    // 名字的字符串
    NSLog(@"Name %@",advertisementData[CBAdvertisementDataLocalNameKey]);
    // CBUUID的NSArray
    NSLog(@"Service UUID %@",advertisementData[CBAdvertisementDataServiceUUIDsKey]);
    // Key为CBUUID,Value为NSData的NSDictionary
    NSLog(@"Service Data %@",advertisementData[CBAdvertisementDataServiceDataKey]);
    // 制造商的数据NSData
    NSLog(@"ManufacturerData %@",advertisementData[CBAdvertisementDataManufacturerDataKey]);
}

Android端解析

@Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        // scanRecord 就是所有的广播数据呗
   }

iOS端为了隐私保护,做了各种各样奇怪的限定,所以不像Android那么爽,可以直接把广播帧的字节流都提取出来,所以就只能好好阅读协议,才能让iOS的App获取到想要的数据

相同的,Android虽然数据很开放,下位机可以不按照BLE协议去设置广播数据,Android App依然可以读得出来数据,但是解析数据就很头疼的样子了.

嗯,各有各的好处~

BTW

其中一个type是

0x17  Public Target Address
目标Mac地址

官方定义是

The Public Target Address data type defines the address of one or more intended recipients of an advertisement when one or more devices were bonded using a public address. This data type is intended to be used to avoid a situation where a bonded device unnecessarily responds to an advertisement intended for another bonded device.

what are you 弄啥嘞…

我YY他的用处就是,这个BLE设备只能被指定Mac地址的主机连接.

好啦,这次又学到了一点儿蓝牙的知识,写个文章当笔记.

阅读剩下更多

编程

django服务器从linux迁移到windows

教研室接了一个检察院的项目

老实说,这个项目坑爆了

一开始说可以提供一个全新的服务器,系统也是我们来定,于是,嫌弃windows的我在Linux上面很熟练的搭建起服务器来

直到…

有一天,他们的技术人员告诉我 “由于经费紧张,咱们就不能买新的服务器了,以前的服务器你想要什么系统都可以”

于是报了三个Windows Server的版本给我…

好吧,反正我也没的选,那就先看看改到windows上面有哪些改变吧?

Context 那边的服务器由于安全问题,是不允许接入外网的

所以PIP都不能用了,所以先把需要的包给下了吧,什么Django,Arrow,Python-DateUtil……

windows上面没有uwsgi,那就用FastCGI吧

想得美,我才不为了你单独去搞FastCGI呢,Django跑跑就够了!!

Nginx还是要的,不然我还得在django里面写下载接口,好在Nginx是有Windows版的 good job

等下

Windows 没有自带 Python ~~

为了保险起见,把32位的和64位的都下载下来

Context 他们是不允许使用优盘的,所以一切的一切,都只能使用刻录的光盘…

OK,前面基本都是废话,简单说,就是在Windows上面装环境啦…

代码改动?

嗯,上传文件接口,Django的Media_Root需要改个位置,当然Nginx的下载地址要改个位置,还有之前使用uwsgi的,现在得换成Django

windows的nginx貌似是不会自动选择conf/nginx.cfg,所以启动的时候还得手动写

nginx.exe -c conf/nginx.cfg

本来是用uwsgi的,那现在就直接Django启动到指定端口

python manage.pyc runserver 9999

哦哦哦,pyc文件的生成方法也很重要,毕竟这个项目xxxx,所以我还是选择不提供源代码给他们.

那就得编译一下再给他们

python -m py_compile manage.py

以此类推,把他们都编程编译过的文件

注意

linux下一个路径是
xxx/xxx/xxx

windows下一个路径是

X:\xxx\xxx

你以为修改django代码的时候就应该把

xxx/xxx/xxx 改成 X:\xxx\xxx

其实…还是

X:/xxx/xxx

本来打算写一个shell来自动安装的,现在看来,只能写成bat文件了~

由于msi的安装不会用bat来做,所以还是手动安装一下比较好

@echo "Start Unzip python packages"
Unzip Django-1.9.9.zip -d C:\
Unzip python-dateutil-patches-for-1.5 -d C:\
Unzip arrow-master -d C:\
@echo "Install Nginx"
Unzip nginx-1.11.3 -d C:\
@echo "install python packages"
python C:\Django-1.9.9\setup.py install
python C:\python-dateutil-patches-for-1.5\setup.py install
python C:\arrow-master\setup.py install
@echo "Copy Nginx configure file"
copy nginx.cfg C:\nginx-1.11.3\conf\nginx.cfg
@echo "build download folder"
md "C:\file"

在启动之前添加一些用户,然后就能启动了.

windows server还是适合使用windows自己的服务器~ASP.net

阅读剩下更多

返回顶部