扫码登录(1)

Context

前天睡醒之前,隐隐约约的梦到了扫码登录的实现方案,虽然这个扫码登录已经有很多很多很多人实现过了,而且说不定还有了框架(我没有搜索过),但是既然我在梦里想过了一遍,那还是不要辜负自己,把它做出来看看吧~

Requirements

扫码登录:网页上显示个二维码,用登录过的app扫描一下,然后网页上就显示登陆成功

所以需要: 网页一个、app一个、服务器一个

网页用了jqueryjquery-qrcode两个框架.以前做艾米奇定位鞋的时候,用到了很多的二维码,当时把二维码图片文件存在了服务器的本地,我觉得比较难过,因为这次我没有服务器,所有的一切都在我的小电脑中,所以这次就打算让客户端自己去根据字符串生成QRCode了

服务器依然是Django,用了一下Celery(我假装做了一个发送短信验证码的异步操作),Redis(Celery需要的).只配了nginx,没有配uwsgi,因为我想动态的看看服务器运行起来的一些log,用了uwsgi我就不方便调试了~

App嘛,必然又是iOS,我做得快一些~,用了几个很基础的框架,AFNetworking、IQKeyBoardManager

Solution in Dream

在梦里,我是打算在服务器中建立QRCode和一个session的关系,然后App登录过的Session可以将QRCode对应的Session也变为已登陆状态,然后网页端的Session就变成了已登录状态,然后就扫码登录成功了.

For Security

在整套工作过程中,哪些部分可能会出现安全风险呢?

要分析这个问题,可能需要先整理一下,什么情况下,网页端可以登陆成功.下面是根据功能得到的最显而易见的一组条件

1. 网页端有QRCode
2. App扫描了QRCode,并且同意登陆

这意思是,网页问服务器要一个QRCode,然后在那儿一个劲的问服务器,我这个QRCode通过登录了嘛?

App扫描了QRCode,告诉服务器,我是XXX,我扫描了这个QRCode,我要登录

然后服务器就告诉了网页端,你登录了

这样的做法,我觉得大概也是可行的,只不过QRCode这东西可能攻击者伪造一下,就撞到别的已经通过的QRCode,然后他就幸运的登录成功了呢!

所以我想,应该还是要多搞一些规则才行~

然后就建立了这样一个模型

class LoginQRCode(models.Model):
    # 显示的二维码
    code = models.CharField(max_length=255)
    # 传递参数时必备参数
    token = models.UUIDField()
    # session中对应的uuid
    session_token = models.UUIDField()
    # 创建时间(更新时间)
    timestamp = models.DateTimeField()
    # 登陆后记录一下这个二维码对应的用户
    user_id = models.UUIDField(blank=True, null=True)
    # 是否通过
    status = models.BooleanField(default=False)

    def get_fetch_qrcode_response(self):
        return {"code": self.code, "token": str(self.token), "timestamp":get_update_time(self.timestamp)}

在获取QRCode的时候

def ask_for_login_qrcode(request):
    if "login" in request.session:
        if request.session["login"] is True:
            return JsonResponse({"msg": "already login", "status": 1}, status=200)
    if "session_uuid" in request.session:
        old_uuid = request.session["session_uuid"]
        old_qrcode_record = fetch_qrcode_record_with_session(old_uuid)
        if qrcode_record_is_expired(old_qrcode_record) is True:
            record = generate_new_qrcode_record_for_request(request)
            return JsonResponse({"msg": "succeed", "status": 0, "data": record.get_fetch_qrcode_response()}, status=200)
        else:
            return JsonResponse({"msg": "succeed", "status": 0, "data": old_qrcode_record.get_fetch_qrcode_response()},
                                status=200)
    else:
        record = generate_new_qrcode_record_for_request(request)
        return JsonResponse({"msg": "succeed", "status": 0, "data": record.get_fetch_qrcode_response()}, status=200)

意思差不多就是,一个session会产生一个token,一个code,一个session_token,其中session_token会对应记录在session中,以便服务器根据session来判断某个token是否是该会话产生的,(也算是防了一下跨站攻击?),app根据扫描二维码,得到code,将code作为参数告诉服务器,我要这个code登录,然后服务器对这个qrcode纪录进行授权,之后检查到这个session的时候,将这个session标记为已登录,整个流程就走完了~

所以处理App扫码之后做的请求是

@csrf_exempt
@require_POST
@pass_auth
@require_parameter(["code"])
def allow_the_qrcode_login(request):
    code = request.POST["code"]
    user = get_user_from_response_session(request)
    qr_record = fetch_qrcode_record_with_code(code)
    if user is not None and qr_record is not None:
        if qr_record.user_id is not None:
            return JsonResponse({"msg": "expired", "status": -1}, status=400)
        qr_record.user_id = user.user_uuid
        qr_record.status = True
        qr_record.save()
        return JsonResponse({"msg": "succeed", "status": 0}, status=200)
    return JsonResponse({"msg": "code not existed", "status": -400}, status=400)

这里用了两个自己写的修饰器用了确定session是登录过的,并且包含了参数”code”~

当然还有很多个工具方法,看名字大概也知道他是什么意思吧~

最后是刷新登录状态的接口

@csrf_exempt
@require_POST
@require_parameter(["token"])
def checking_login_status(request):
    token_uuid = request.POST["token"]
    qrcode_entity = fetch_qrcode_record_with_token(token_uuid)
    if qrcode_entity is None:
        return JsonResponse({"msg": "bad request", "status": -403}, status=400)
    if "login" in request.session:
        if request.session["login"] is True:
            if request.session["session_uuid"] == qrcode_entity.session_token:
                return JsonResponse({"msg": "pass", "status": 0}, status=200)
            else:
                return JsonResponse({"msg": "token error", "status": -1}, status=403)
        elif qrcode_entity.status is True and request.session["session_uuid"] == str(qrcode_entity.session_token):
            request.session["login"] = True
            request.session["user_id"] = str(qrcode_entity.user_id)
            return JsonResponse({"msg": "pass", "status": 0}, status=200)
        elif qrcode_record_is_expired(qrcode_entity):
            return JsonResponse({"msg": "code is expired,please refresh it", "status": -1}, status=200)
    return JsonResponse({"msg": "waiting", "status": 1}, status=200)

这段我也懒得解释了,反正要改了……

Have a break

这一篇先写这么多,下一篇会讲扫码App的故事(网页端会在很后面讲,因为这个版本网页端和服务端存在一个轮训操作,这个操作效率很低下,我打算在后面加入了websocket之后,再来一起讲网页端~)

Socket

Context

这个故事从三年前开始说起.

那时老师让我搞Android的一个防伪项目(精益防伪),就是那个用Android的NFC功能来鉴别一个酒是否是假冒伪劣的,也是那个项目让我认识了卢师兄(在华为可厉害了呢).

那时的我只用过http来进行网络请求,没有用过socket来进行数据交互.

由于项目需要,那时候就临时在Android上搞了一波socket通信.

第一次做Android就要碰Socket还是蛮困难的(虽然我已经搞了两年iOS,但是iOS和Android毕竟还是不一样的,而且我还是个孩子),因为界面没做过,java也不是很熟悉,甚至连Android的手机都没有用过. 由于时间比较紧张,所以界面就请望神帮我画了,我去写了数据处理和Socket通信. 那也是第一次我知道了Android上面有很严格的线程限制. 在iOS上的主线程中调用网络,会堵塞,卡在那儿,等到请求完了,就会恢复;不过在android上面,主线程碰一下网络竟然就异常了~(真凶)

Socket

Socket其实不应该和http分开说,因为http只是socket的一种扩展罢了,要进行网络通信,总是离不开Socket的.http只不过是在socket上面进行了很多复杂的封装罢了(很复杂).

socket,翻译过来就是,插座.打个比方就是,我要和你收发数据,就把数据线插到你的插孔里面,然后我们就在这根线里面交流.一个网络节点可以有很多的插座孔,所以也就可以同时和很多很多人一起交流信息.而http是插上了插座,发一句话,收一句话,然后就把插头拔掉了(连接结束了)好处就是服务器和客户端不用一直维护一个连接,缺点嘛,这是一个短连接,下一次想要进行网络请求还需要把TCP的握手流程走一遍,而且是单向操作(只能客户端找服务器麻烦,服务器永远无法主动找客户端麻烦)

Now

三年前的旧事讲完了,该说说这次的故事了

这次我们的产品中会涉及用到socket传输文件,这比三年前那个要求更高了一些,因为文件还是蛮大的,之前每次的数据也就十几个字节(128位)(NFC标签儿本来也存不了多少内容)

Server

先从服务器开始说起吧!由于这个socket传输只会一对一的传输,所以这个服务器根根本本不用考虑并发和异步什么的东西,所以

就用Python做个简单的socket服务器吧!

code
import threading
import SocketServer
import datetime
import time

BUFFER_SIZE = 4096


class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):
    t = None
    wantDisconnect = False

    def handle(self):
        if self.t is None:
            self.t = datetime.datetime.now()
            time_interval = 0
        else:
            time_interval = (datetime.datetime.now() - self.t).seconds
        while time_interval < 60 and not self.wantDisconnect:
            data = self.request.recv(BUFFER_SIZE)
            if data:
                self.t = datetime.datetime.now()
                print "prepare to send file"
                f = open(str(data), 'rb')
                l = f.read(BUFFER_SIZE)
                while l:
                    self.request.send(l)
                    print("sending (%x)", l)
                    l = f.read(BUFFER_SIZE)
                    time.sleep(1/20)
                time.sleep(1)
                self.request.sendall("\nsending ok")
                f.close()
            else:
                print threading.currentThread().name, " no data"
                break

    def setup(self):
        print threading.currentThread(), " start"
        self.t = datetime.datetime.now()

    def finish(self):
        self.wantDisconnect = True
        print threading.currentThread(), " end"


class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass


HOST, PORT = "0.0.0.0", 55555

server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
server.serve_forever()

我直接在服务器的文件目录下放了几个图片文件用来做测试的~

这差不多基本上是直接从Python的SocketServer文档中抄来的

一个多线程的socket服务器就是这么方便

其中setup()finish()没什么太大的意思,反正就是又一个socket连接进来了和对方主动断开后会做的事情

关键的操作都在handle中!

先贴一下官方的解释:

handle()

    This function must do all the work required to service a request. The default implementation does nothing. Several instance attributes are available to it; the request is available as self.request; the client address as self.client_address; and the server instance as self.server, in case it needs access to per-server information.

    The type of self.request is different for datagram or stream services. For stream services, self.request is a socket object; for datagram services, self.request is a pair of string and socket.

就是说有请求进来了,就会在这里中断 ,aka “回调”(但我觉得用中断好像更加形象生动)

self.request将获取到socket实例,有了这个socket实例我们就可以读读写写了

这里的代码是传入的数据代表的是文件,服务器根据文件名,把数据回传回去,就是这么简单,看看效果图

效果图

密集恐惧症了吧?哈哈哈~在服务器把发送的数据打印出来,可以方便调试,(肉眼)检查数据有没有收完收对……

等下,time.sleep(1/20)这个东西有啥子用处?

我担心发送的太快接收端粘包,所以就稍微给一点点儿延迟,缓解一下压力,不过实测下来貌似是我多虑了~

iOS

以我的尿性,客户端必然是从iOS开始搞的~

目标很简单,连接服务器,发个文件名,一个劲的收数据,最后显示效果

iOS效果

略~

Doing

多年前在CocoaPods上面看到了CocoaAsyncSocket,今天终于要动手搞一搞了!好激动

一般大家在github的README.md里面都要把自己的库的用法好好的解释一遍,让访问者眼前一亮,然后送出Star,不过这个库真牛逼,README.md里面装了整整一页的逼!

然后我去wiki里面看,竟然还给了好多个链接,一时半会儿还找不到他的使用方法,不过我看了CommonPitfalls这个板块,我觉得写的很屌,如果我前面的那些关于socket的废话你没看懂的话,可以去这个链接里面再看看.里面讲了不少误区,坑,雷……

好吧,其实它真正的用法在Intro_GCDAsyncSocket里面

教了连接动作,连接结果,读操作,写操作

贴代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    socket=[[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    NSError *error;
    buf=[NSMutableData data];
    [socket connectToHost:@"192.168.199.131" onPort:55555 error:&error];
}

-(IBAction)send_action:(id)sender{
    [socket writeData:[self.commandField.text  dataUsingEncoding:NSASCIIStringEncoding] withTimeout:5.f tag:12345];
}

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    NSLog(@"socket is connected");
    [self.sendButton setEnabled:YES];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSLog(@"[%ld]Did receive Completely %@,(%ld)",tag,data,data.length);
    self.console.text=[NSString stringWithFormat:@"%@\n%@",data,self.console.text];
    UIImage *image=[UIImage imageWithData:data];
    self.imageView.image=image;
}

- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag{
    NSLog(@"[%ld]receive (%lu)*1024 bytes",tag,partialLength);
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    if (tag==12345){
        [sock readDataToData:[@"\nsending ok" dataUsingEncoding:NSASCIIStringEncoding] withTimeout:60.f tag:119];
    }
}

- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock{
    NSLog(@"socket closed");
    [self.sendButton setEnabled:NO];
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
    NSLog(@"socket disconnect");
    NSLog(@"buf is %@",buf);
    [self.sendButton setEnabled:NO];
}

这代码好像蛮长的~

其中有partial的回调方法都是接收不完整的时候会做的事儿

Socket本来就是全双工的,就是可以一边儿发送一边儿接收,不过这现在给我做成了个半双工的了,所以我在读取的时候设置了一个结束条件\nsending ok,接收到了这个的时候,就会停止.

Android

android上面我想装逼,不用框架,直接用socket来撸,写的还是蛮痛苦的

先只贴代码,先不做解释了,因为,写的不是特别好……等到优化好了,再写一篇新的,好好解释一番~

先搞个center

public class SocketCenter {
    SocketEntity socket;
    Handler handler = new Handler();
    Thread socket_thread;
    Handler network_handler;
    public void setup() {
        EventBus.getDefault().register(this);
        socket_thread=new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                network_handler=new Handler();
                Looper.loop();
            }
        });
        socket_thread.start();
    }
    public void connect_server() {
        network_handler.post(new Runnable() {
            @Override
            public void run() {
                socket = new SocketEntity("192.168.199.131", 55555, handler);
                socket.connect();
            }
        });
    }
    public void disconnect() {
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(SocketEvent event) {
        if (event.getName().equalsIgnoreCase(EventConstant.Socket_Connected)) {
            network_handler.post(new Runnable() {
                @Override
                public void run() {
                    socket.ask_for_file("img1.jpg", "\nsending ok");
                }
            });
        }
    }
}

再把实例搞出来

public class SocketEntity {
    private Socket socket;
    String address;
    int port;
    BufferedReader reader;
    BufferedWriter writer;
    byte[] temp_data;
    ArrayList<byte[]> received_queue;
    WeakReference<Handler> handler_reference;
    Thread async_thread;
    SocketEntity(String address, int port, Handler handler) {
        socket = new Socket();
        handler_reference = new WeakReference<Handler>(handler);
        this.address = address;
        this.port = port;
        received_queue = new ArrayList<>();
        async_thread = new Thread();
    }
    Socket getSocket() {
        return socket;
    }
    void connect() {
        Log.e("SocketUtil", "Start Connect");
        if (socket != null) {
            if (!socket.isConnected()) {
                try {
                    socket.connect(new InetSocketAddress(address, port), 5000);
                    writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                    reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    SocketEvent event = SocketEvent.connectEventInstance();
                    EventBus.getDefault().post(event);
                } catch (IOException e) {
                    e.printStackTrace();
                    SocketEvent event = SocketEvent.failBaseEventInstance(-200);
                    EventBus.getDefault().post(event);
                    writer = null;
                    reader = null;
                }
            }
        }
    }
    void disconnect() {
        if (socket != null) {
            if (socket.isConnected()) {
                try {
                    socket.close();
                    writer.close();
                    reader.close();
                    writer = null;
                    reader = null;
                } catch (IOException e) {
                    e.printStackTrace();
                    SocketEvent event = SocketEvent.failBaseEventInstance(-400);
                    EventBus.getDefault().post(event);
                    if (!socket.isConnected()) {
                        writer = null;
                        reader = null;
                    }
                }
            }
        }
    }
    void ask_for_file(String filename, String endPoint) {
        send_command(filename);
//        receive_data_until(endPoint.getBytes());
        receive_data(endPoint.getBytes());
    }
    private void receive_data(byte[] endpoint) {
        try {
            char[] buf = new char[1024];
            int total = 0;
            boolean isEnd = false;
            String s = "";
            while (!isEnd) {
                int length = reader.read(buf, 0, 1024);
                isEnd = (length == -1);
                if (length > 0) {
                    char[] valid_data = Arrays.copyOfRange(buf, 0, length);
                    String string = String.valueOf(valid_data);
                    s=s+string;
                    isEnd = string.contains(new String(endpoint));
                    if (!isEnd) {
                        total += length;
                    } else {
                        total += length;
                        total -= endpoint.length;
                    }
                    Log.e("Socket_Received", String.valueOf(HexUtil.encodeHex(string.getBytes())));
                    Log.e("String is ", string);
                }
            }
            Log.e("Socket_Received", "Total Size is " + total + " bytes");
//            temp_data=s.getBytes();
            temp_data=Arrays.copyOfRange(s.getBytes(),0,total);
            Log.e("Socket_Received", "Total binary Size is " + temp_data.length + " bytes");
            SocketEvent event= SocketEvent.fileReceivedEventInstance(temp_data);
            EventBus.getDefault().post(event);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void receive_data_until(byte[] ends) {
        try {
            byte[] temp = null;
            byte[] temp_buffer = null;
            while (ends != temp) {
                String line = reader.readLine();
                byte[] bytes = line.getBytes();
                Log.e("Socket_Received", String.valueOf(HexUtil.encodeHex(bytes)));
                int length = ends.length;
                if (bytes.length >= length) {
                    temp = Arrays.copyOfRange(bytes, (bytes.length - length), bytes.length);
                    temp_buffer = line.getBytes();
                } else if (temp_buffer != null) {
                    int rear = bytes.length;
                    int front = ends.length - rear;
                    if (temp_buffer.length > front) {
                        temp = new byte[ends.length];
                        for (int i = 0; i < front; i++) {
                            temp[i] = temp_buffer[temp_buffer.length - front + i];
                        }
                        for (int i = 0; i < rear; i++) {
                            temp[front + i] = bytes[i];
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void send_command(byte[] command) {
        String cmd = Arrays.toString(command);
        send_command(cmd);
    }
    private void send_command(String command) {
        if (socket != null && socket.isConnected()) {
            try {
                Log.e("SocketUtil", "Send " + command);
                writer.write(command);
                writer.flush();
            } catch (IOException e) {
                e.printStackTrace();
                SocketEvent event = SocketEvent.failBaseEventInstance(-1);
                EventBus.getDefault().post(event);
            }
        }
    }
}

这里面有很多的问题,不过还是成功的做到了连接,发送指令,接收数据,由于最后BitmapFactory.decodeByteArray存在一些问题,估计和接收的数据有关系,所以没有效果图……(难过)

End

先写这么多了,等我下次优化完了,说不定就会有更深入的理解,更好的代码,更加出色的性能,到时候再贴出来留作纪念!

突变的SHA1

Context

故事从两个低分评价开始.

那一天,Google Play上面,Cycplus 007突然惊现两个低分评价,除了一个翻译的小问题之外,他们还异口同声的反应到,他们的报警界面上的Google Map里面空白一片了,什么都不显示.

在全球发售之前,我们特地把样品寄到了全球各地(的某些地方),进行测试,以保证我们的产品是可以在海外正常工作的.

而且这两个用户都是波兰人,难道,波兰的Google也被禁了?hahaha

那是不可能的,他们可是从Google Play上面下载的App

Google and Android

突然,我陷入了恐慌.在很久很久之前我担心的一个问题,不会灵验了吧!

我们知道,Android是Google的,但是Android上面又可以没有google的东西(国产手机们基本上都没有Google Play套件),那Google Map作为Android的原生地图,会不会在那些缺少Google套件的手机上无法运行呢?

毕竟,iOS是可以直接使用原生地图的,使用原生地图还可以节省不少的空间呢,因为原生的地图空间,所需要的容量早就算在系统里面了~

所以我就很担心,会不会只有安装了Google Play套件的手机才可以使用Google Play~

操作了一番,竟然是真的.

GoogleMapNotWorking

不过回头一想,他们是Google Play上面下载的App,难道会没有Google Play套件?

Debug

我自信满满地再一次打开了app,看着正常运行的Google Map,陷入了沉思.

我不明白为什么我就可以运行

然后我就删掉了本地的App,从Google Play上面再下载了一份,运行起来,

竟然真的是空白!

吓得我突然之间手忙脚乱,手足无措,明明之前都是可以使用的,突然之间怎么就坏掉了!

我连接上数据线,查看catlog,竟然发现Google Map说我的签名错了!

我急忙登上Google Console,并且用keytool查看了我打包时候使用的Sha1:

1B:xxxxxxx

在Google Console上面登记着的:

1B:xxxxxxx

catlog里面写着我使用的Sha1:

ED:xxxxxxx

What the fuck?

怎么,怎么会变了呢

Google Play App Signing

在Google Play的Console里面有一个菜单是应用签名

在里面我惊奇的发现了两个证书:

-应用签名证书
-上传证书

而我登记在Google Map的Console里面的正是这个上传证书,而下载安装之后显示的,是这个应用签名证书的Sha1

这时我恍然大悟,前几天着急更新的时候,好像是点击了一个什么Google Play App Signing之类的按钮,当时也没管它是干什么的,就着急的去更新软件了.

下面来转载一下他的官方介绍:

Without Google Play App Signing: You sign the app with your app signing key, upload your app to Google Play, and then your app is delivered to the user.

With Google Play App Signing: You sign your app with your upload key. Then, Google verifies and removes the upload key signature. Finally, Google re-signs the app with the original app signing key you provided and delivers your app to the user.

总之他的意思大概是担心坏人得到了这个apk之后,通过各种我反正不懂的方法之后,可以得到原来签名的key

这会不安全,所以Google用各种我反正不懂的方法,把这个原来签名的key给去掉,换一个它自己的key,这样子就不会被坏人破解出上传APK的key,来做坏事儿了.

看起来确实,是稍微安全了一些呢

下面还介绍了可以设置的各种key,反正我(你)可能也用不到,我就不说了

可是

最关键的是:

进来了,还想走?

Google Play App Signing is an optional program. If you prefer, you can continue managing your own keys.

Once you've enrolled your app in Google Play App Signing, withdrawal is not supported. To preserve the security of your app signing keys, we don't have the ability to remove keys from the secure server.

这就很是麻烦了……因为国外我们可以在Google Play中下载,但是国内我们将安装文件放在了服务器上,供用户直接下载安装.

而国内这个安装文件,肯定使用的是upload key,所以,他们的Sha1是不一样的.

可能这个情况Google早就考虑到了,所以在Google Map的Console中,我们是可以添加好多好多个Sha1的,所以加上一个新的Sha1之后,Google Play版本的用户就可以正常使用Google Map了.

但是

百度地图没有想到这个问题,他们的配置界面只有两个Sha1可以填写,而且一个事发布饭一个是开发版.

这就意味着,如果开发版里面填写着我自己debug用的sha1,那么Google Play的用户或者国内用户中,至少有一方是不能使用百度地图查看他们的报警信息的

你大概会觉得:

反正外国人不会用百度地图的,就不用给他们添加到配置页面了咯

NO

我就是在国内,使用Google Play的中国人……我相信还有很多人会和我一样,因为害怕国内的商店太霸道,又不喜欢App内更新,所以我还是喜欢和App Store一样的商店.

于是乎,我就只能把debug的key换了下来,改成了Google play的sha1

End

这个故事并没有涉及到什么高深的技术,但是却说明了很重要的一件事

鼠标点慢一些!

iOS图片取色

上周兴冲冲的在炫轮的iOS版中加了梦寐以求的GIF制作功能,这个功能两年前我就像加,只是当时胆子不够大,觉得iOS不能从相册读取GIF,所以这个功能就一直处于被砍掉的状态.

Not Important

用UIImagePickerViewController从图库中选择一个图片,这个百度一下,教程是有很多的,就不在这个本来就不重要的地方说了.

关键就在于如何处理选择的图片

在UIImagePickerViewController的delegate的回调方法中,有个info

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info

这个info可厉害了,里面有很多很多的东西

UIKIT_EXTERN NSString *const UIImagePickerControllerMediaType
UIKIT_EXTERN NSString *const UIImagePickerControllerOriginalImage //原图 UIImage,不过GIF只有第一帧
UIKIT_EXTERN NSString *const UIImagePickerControllerEditedImage
UIKIT_EXTERN NSString *const UIImagePickerControllerCropRect
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaURL     UIKIT_EXTERN NSString *const UIImagePickerControllerReferenceURL //原图的文件地址
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaMetadata
UIKIT_EXTERN NSString *const UIImagePickerControllerLivePhoto

直接使用UIImagePickerControllerOriginalImage获取UIImage肯定是不行的,这就是一个静图,所以我们使用UIImagePickerControllerReferenceURL获取到这个图片的位置,然后再获取到数据,然后再解析成GIF~完美

于是我就用NSData获取了这个url,然后发现,这个操作得到的NSData是nil!

我仔细一看这个url,是AssetLibrary的链接,不是一个绝对路径!

看来需要操作AssetLibrary了……

AssetLibrary && Photos

于是我就去Apple Developer官网找AssetLibrary的资料,吃鲸的发现,AssetLibrary已经灭绝(废弃)了!

这是天大的好消息啊,因为查看了其他的资料,据说AssetLibrary是非ARC的,还有一堆同步异步等乱七八糟的东西,总之就是坑很多.现在用Photos来代替,我紧张的去查询Photos,担心他要求的系统SDK比我当前部署的SDK高.

iOS 8 !

哈哈哈哈,和我当前的部署版本是一样的,这就意味着,这个版本的炫轮app的GIF功能是可以对所有用户开放的!

Photos依然有很多异步操作(毕竟有的图片可能是在iCloud上面的),所以我就干脆定义了一个block来

typedef void(^LoadingAssetBlock)(NSArray * images);

我是为了获取GIF的,那么这个返回的参数给个数组,就刚刚好了~

下面我们来根据url获取GIF吧!

- (void)imagesForURL:(NSURL *)url andBlock:(LoadingAssetBlock)block {
    PHFetchResult *assets= [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil]; // 1
    PHAsset *asset=[assets objectAtIndex:0]; // 2
    [[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { //3
        NSData *data=imageData;
        NSString *type=[NSData sd_contentTypeForImageData:data];  //这是SDWebImage的大佬们写的
        if ([type isEqualToString:@"image/gif"]) {
            UIImage *img = [UIImage sd_animatedGIFWithData:data];  这也是SDWebImage的大佬们写的
            NSMutableArray *renderedImage=[NSMutableArray array];
            for (UIImage *item in img.images) {
                VKMutableImage *mutable=[VKMutableImage imageWithImage:item];
                /**
                *这里真的不重要
                */
                [renderedImage addObject:mutable];
            }
            block([NSArray arrayWithArray:renderedImage]);
        } else{
            UIImage *image=[UIImage imageWithData:imageData];
            VKMutableImage *mutable=[VKMutableImage imageWithImage:image];
                /**
                *这里真的不重要
                */
            block(@[mutable]);
        }
    }];
}

其实只要看注释 1,2,3就够了……

They are not important!

我发现我好像跑题了,这一片的主要内容是,获取UIImage上的各个点的颜色的,然后我们使用RGB数组制作一个UIImage

再再介绍一下背景吧~

按理说上面说的这个图片操作的功能,早在炫轮刚面世的时候就存在的,为啥现在才来说这个东西呢?

因为从一开始就写错了!直到现在才发现

就当我上周兴冲冲的写完iOS的GIF制作功能之后,我就开始写android版的GIF制作功能,花了3天才终于把功能做完了!

正当我准备发一波朋友圈得瑟得瑟的时候,我发现了一个很严重的问题…

Android GIF ScreenShot
iOS GIF ScreenShot

使用了一样的调色,为什么结果不一样!按照以前的习惯,我一定是会怀疑Android版本出现了问题的,可是根据原图,我总觉得,Android上面的结果好像是正确的!

那就意味着,发布了快3年的炫轮iOS版App存在一个致命的图像处理的问题!

Here we go

正片开始了~

经过一连串debug手法,我找到了出问题的代码位置:以下是有问题的代码

CGImageRef imageRef=originImage.CGImage;
CGContextRef context = newBitmapRGBA8ContextFromImage(imageRef);
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(context, rect, imageRef);
unsigned char *bitmapData = (unsigned char *)CGBitmapContextGetData(context);
size_t bytesPerRow = CGBitmapContextGetBytesPerRow(context);
size_t bufferLength = bytesPerRow * height;
unsigned char *newBitmap = NULL;
if(bitmapData) {
    newBitmap = (unsigned char *)malloc(sizeof(unsigned char) * bytesPerRow * height);
    if(newBitmap) {
        for(int i = 0; i +3 < bufferLength;i=i+3) {
            int R=bitmapData[i];
            int G=bitmapData[i+1];
            int B=bitmapData[i+2];
            /**
            * 这里一点儿也不重要
            */
            newBitmap[i] = R;
            newBitmap[i+1]=G;
            newBitmap[i+2]=B;
            newBitmap[i+3]=255;
        }
    }
    free(bitmapData);
} else {
    NSLog(@"Error getting bitmap pixel data\n");
}    
CGContextRelease(context);
tempImage=[PublicHelper convertBitmapRGBA8ToUIImage:newBitmap withWidth:(int)width withHeight:(int)height];
free(newBitmap);
return tempImage;

newBitmap就是经过处理的图片的RGBA数组

关键错误应该是在这里:

int R=bitmapData[i];
int G=bitmapData[i+1];
int B=bitmapData[i+2];

虽然说我们知道图片是由一堆RGBA构成的,但是我们并不知道RGBA的顺序呀~这并不像Android里面那么爽,Color是一个32bit(4字节)的数字(int),每个字节分别代表着ARGB

那么iOS呢?

查了很多的资料,看到各位大佬都说iOS上面是RGBA,那么我原来的写法,好像很RGBA啊,好像没什么问题啊~

little-endian,big-endian

难道说,和大小端有关??

虽然说是RGBA,但是存在数组里面的是ABGR?

抱着试一试和重构的态度,借鉴了各种大佬的代码,之后改成了这样

const int imageWidth = originImage.size.width;
const int imageHeight = originImage.size.height;
size_t bytesPerRow = imageWidth * 4;
uint32_t* rgbImageBuf = (uint32_t*)malloc(bytesPerRow * imageHeight);    
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace,kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), originImage.CGImage);
int pixelNum = imageWidth * imageHeight;
uint32_t* pCurPtr = rgbImageBuf;
for (int i = 0; i < pixelNum; i++, pCurPtr++)
{
    uint8_t *ptr = (uint8_t*)pCurPtr;
    ptr[0] // Alpha
    ptr[1] // Blue
    ptr[2] // Greem
    ptr[3] // Red
    /**
     * 这里一点儿也不重要
     */
}
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight,ProviderReleaseData);
CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight, 8, 32, bytesPerRow, colorSpace,kCGImageAlphaLast | kCGBitmapByteOrder32Little, dataProvider,NULL, true, kCGRenderingIntentDefault);    
CGDataProviderRelease(dataProvider);
tempImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return tempImage;

void ProviderReleaseData (void *info, const void *data, size_t size)
{
    free((void*)data);
}

现在用uint32_t来类比android中的Color

还用了DataProvider来把字节流转换成UIImage~这样修改后的代码,变得更加简洁了!

关键是,现在工作的正常了!

iOS GIF Right

End

不打不相识,Android和iOS不一起开发互相比较的话,就不能互相进步~

好了,这次不仅添加了GIF制作这个功能,还把以前的图片改色功能给修好了~真是一举两得啊!

演示视频

iOS<=====>Android

iOS这个视频里面的颜色修改,还没有改~.~

Linkit One 空调遥控器

使用linkit one 控制空调

这一篇会比较长,会写很多踩过的坑,一些知识,弯路,废话.

如果你想直接看结果,可以直接看小标题Release

Context

这夏天真是热,虽然成都比杭州的温度低7-10度,但是相比较舒适的22-25度来说,还是比较燥热的.

所以空调肯定是要开的.

可是,一直开会比较冷,开一会儿关掉又会比较热,即使空调有定时开定时关的功能,那这个操作在睡觉期间,只能做一次.它并不能智能的开关开关开关……

但是,自己做一个遥控器,就可以啊!

于是就开始着手搞这个了!

My Limitation

我是个纯种的程序员,虽然说数电学的还不错,但是模电基本上都忘光了.所以要我画画板子,计算机算电阻电容,那我还不如直接放弃算了~

之前玩过Arduino,所以我对Arduino还算有点儿了解,所以肯定就选择这个平台了.

因为我们公司自己的智能硬件大部分都涉及到低功耗蓝牙,所以我觉得用蓝牙来进行互动对我来说开发起来也比较方便,所以我就选择了之前参加比赛骗来的Linkit One开发版.它自带了蓝牙、Wifi、GPS、GSM模块(一波广告)

Requirements

DHT22温湿度传感器,IR发射器,IR接收器.

面包版,线,线,线,线,线…

Linkit One,Arduino开发环境, Linkit One SDK.

Knowledge

以下内容基本上是摸索过程中学的,因为我一开始并没有觉得这个东西会那么的复杂

遥控器和空调的通信

一查就知道,他们用一个叫做38khz红外来通信.

那么38khz红外是个啥玩意儿呢?

先上图

irsignal

就是发射器在一定的时间内通过亮灭亮灭的快速切换来发送0,1,0,1

比如说 载波发射0.56ms,不发射0.56ms表示数字信号”0”;载波发射0.56ms,不发射1.68ms表示数字信号”1”

上面说的这个操作只是个例子,不代表所有的空调,电视什么的都用这种方式来表示0和1.

Arduino

这个怎么解释呢?

举个例子吧~

单片机工作起来就是在永远循环执行一个代码块儿.

在Arduino中有void loop()这个函数,在这个函数中把要循环做的事情放进去,就可以让它正常工作了.

比如说把某一个引脚置为高电平,只要简单的写一句DigitalWrite(Pin Number,HIGH);就OK了

读取某个引脚的点平值,只要简单的写一句DigitalRead(Pin Number);就OK了

差不多就这么个操作,基本上不用考虑什么中断、分片、多线程之类的东西

发送指令*

虽然我们知道了如何让IR发射器发送红外信号,但是我们依然不知道这个空调的发射指令是什么~

这个学习指令几年前在机顶盒遥控器上面,就见过了.在拥有了IR接收器之后,他的原理其实蛮简单的.

IR接收器在收到高低点平切换的时候,记录一下时间,就可以知道发射端高低信号的持续时间了.

比如说:

我抓到的关机指令

4360,4410,477,2205,237,266,520,1678,514,1734,484,693,367,527,533,1767,479,735,370,482,539,1956,309,491,630,550,477,2208,2234,473,532,1748,482,563,515,1585,1065,1449,281,1590,1090,1402,347,461,539,2139,208,1542,506,2262,185,283,769,343,536,516,551,522,942,1422,420,512,556,513,1150,1339,331,1584,544,1762,552,430,581,496,565,1133,198,316,846,266,539,489,574,526,807,339,531,1694,503,1729,517,1614,521,1753,456,1645,522,5371,4506,4419,463,1699,726,292,531,1601,838,1442,524,476,576,508,1098,1414,279,501,567,514,1063,1484,309,466,555,510,557,1766,693,1394,538,1135,205,1455,511,503,547,1773,450,1674,517,1719,480,1634,555,486,562,1736,535,1564,540,1758,737,288,499,520,553,540,822,339,522,1663,532,504,1096,200,381,1680,509,1762,436,1650,524,505,571,1044,205,389,753,322,560,527,565,1199,190,260,789,334,512,1625,903,1428,434,1599,1139,1335,316,1592,559

开机指令

4385,4385,471,1690,447,579,527,1600,996,1236,548,498,584,490,8521,561,106,171,65,62,61,64,67,58,62,450,226,562,504,1590,571,528,722,1503,546,1593,839,312,555,481,571,1630,583,1695,528,1577,572,1693,516,1589,560,518,658,1564,553,1582,753,405,535,524,591,491,571,511,771,410,497,1621,563,1747,514,466,579,1601,567,726,457,459,537,509,627,491,557,531,653,473,527,1620,609,489,852,1388,518,1776,437,1712,485,1740,441,5368,4596,4410,400,1570,553,523,556,1700,567,1552,569,526,809,351,491,1634,554,528,560,621,506,1597,594,493,592,786,360,1571,572,1590,657,463,554,1599,566,1721,502,535,549,513,563,1701,506,1607,562,1771,463,1596,552,1806,458,474,580,1582,556,1709,500,510,567,522,558,536,820,320,534,525,564,1611,584,1677,526,488,561,1874,338,519,565,517,569,512,575,520,660,479,547,523,560,1618,735,377,564,1606,576,1685,520,1619,557,1721,505

有了这一坨数据,后面只要控制IR发射器Data引脚*,就可以发送红外指令了!

PWM(脉冲宽度调制)

嗯,这个东西我琢磨了会儿才明白.

一开始我一直不明白,我要这玩意儿干嘛.

引用百度百科里面的解释: 脉冲宽度调制是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中.

不过后面在pj大佬的教导下,我明白了它的意思,简单说就是我要发高电平,就发PWM波;我要发低点平,就不发PWM波.这看起来和AM调制一样,不同的是AM调制是对一个模拟信号进行调制;而我这里用到的是对数字信号进行调制.

我就假设我说明白了~

Dev

对于DHT和蓝牙的开发我就不多写了,因为真的蛮简单的,也没什么大坑.最多吐槽一下Linkit One这个板子对BLE蓝牙的限制吧~这家伙不能修改广播数据(改不了名字、服务UUIDs、制造商数据……),而且外设模式下Notify貌似是没有用的~

好嘞,蓝牙和温湿度都讲完了!下面可以一心一意的讲IR发射的坑了.

Arduino-IRRemote

在我真正开始这个项目的之前,我做了一些估计.查了我需要的各种模块的开发难度,找找有没有什么可以用的库可以直接调用.

DHT温湿度传感器

IR发射接收

美的空调Arduino发射接收

我找到了这三个库,觉得这个项目在元件齐全了之后大概只要一个下午就可以搞定了.

于是,收到IR发射器之后,我就迫不及待的开工了!

~~~/Arduino/libraries/IRremote/irISR.cpp:18:5: error: expected constructor, destructor, or type conversion before '(' token
ISR (TIMER_INTR_NAME)

编译错误!我溯源找到了这个类,发现了ISR()这个函数,并没有写他的来源,说明这个函数可能是板子自带的.于是我就用Arduino UNO来编译相同的代码,编译通过!

天啦噜,Arduino-IRRemote这个库是不支持Linkit One的!

吓得我赶紧去看看那个直接发美的空调指令的库,这个库,也是调用Arduino-IRRemote的

异想天开的做法

然后我就把发射代码搞出来,放到了Arduino UNO上面来运行.竟然可以成功的关闭空调了!

于是我陷入了深深的沉思~

难道说,我得用两颗单片机来做这个简单的小玩意儿??

说实话,我真的这么折腾了一段时间,效果和想象中的并不一样,这可能和模拟电路的知识有关~

手动生成PWM!

回到原始的方法,我开始理解IRRemote发送数据的工作原理

发送原始数据

void  IRsend::sendRaw (unsigned int buf[],  int len,  int hz)
{
    // Set IR carrier frequency
    enableIROut(hz);

    for (int i = 0;  i < len;  i++) {
        if (i & 1)  space(buf[i]) ;
        else        mark (buf[i]) ;
    }

    space(0);  // Always end with the LED off
}

这好像是发高电平

void  IRsend::mark (int time)
{
    TIMER_ENABLE_PWM; // Enable pin 3 PWM output
    if (time > 0) delayMicroseconds(time);
}

这好像是发低电平

void  IRsend::space (int time)
{
    TIMER_DISABLE_PWM; // Disable pin 3 PWM output
    if (time > 0) delayMicroseconds(time);
}

这就当作是配置PWM的频率吧

void  IRsend::enableIROut (int khz)
{
    // Disable the Timer2 Interrupt (which is used for receiving IR)
    TIMER_DISABLE_INTR; //Timer2 Overflow Interrupt

    pinMode(TIMER_PWM_PIN, OUTPUT);
    digitalWrite(TIMER_PWM_PIN, LOW); // When not sending PWM, we want it low
    TIMER_CONFIG_KHZ(khz);
}

TIMER_DISABLE_PWMTIMER_ENABLE_PWM里面调用了很多和ISR,PWM有关的东西,所以这里开始就要重新了

先写个高电平

void mark(int time){
    int t=0;
    boolean isHigh=false;
    while(t<=time){
        isHigh=!isHigh;
        t+=enable_pwm(isHigh);
    }
}

int enable_pwm(boolean high){
    if(high){
        digitalWrite(3,HIGH);
    }else{
        digitalWrite(3,LOW);
    }
    delayMicroseconds(13);
    return 13;
}

通过计算38khz的一个周期是26微秒,所以我们13微秒就要转换一次电平~

再写个低电平

void space(int time){
    digitalWrite(3,LOW);
    delayMicroseconds(time);
}

这个不需要发转电平,所以就轻松一些了~

由于我已经固定了13微秒转换一次,所以也不用实现enableIROut(int khz)这个函数了.

运行!编译通过!发送!IR接收机采集到了IR信号!可是~空调好像没反应~

还有这种操作

气氛一度很尴尬,不过我毕竟只是个普通人,看不到发送的数据长什么样子,所以在添哥大佬的帮助下我使用了高档示波器

这是空调遥控器发出的信号在接受端的样子

right signal

这是我发出的信号在接受端的样子

error signal

尖峰哪儿来的?

检查代码真的没有问题,这个异常我只能归结为硬件问题了,要知道linkit one是一个很高级,很牛逼的硬件,它甚至有多线程的操作,所以,它会不会强行的我把我操作所在的线程给中断了呢?又或者linkit one并没有能力做那么高频的电平转换.

analogWriteAdvance

气氛再一度尴尬,不过添哥大佬在官方文档的Analog I/O中找到了analogWriteAdvance()这个函数!

void analogWriteAdvance(
uint32_t pin, 
uint32_t sourceClock, 
uint32_t clockDivider, 
uint32_t cycle, 
uint32_t dutyCycle
);

而他的例子里面就是生成了一个PWM波!

在经过简单的计算之后,我改写了PWM波的生成和暂停

//Enable 38khz PWM
analogWriteAdvance(PWM_PIN,PWM_SOURCE_CLOCK_13MHZ,PWM_CLOCK_DIV1,342,171);
//Disable 38khz PWM
analogWriteAdvance(PWM_PIN,PWM_SOURCE_CLOCK_13MHZ,PWM_CLOCK_DIV1,342,0);

编译上传运行,,空调开了~(由于这是在公司做的,所以信号序列也是公司空调的序列,所以回到寝室,还得再改一下)

时好时坏的遥控器

回到了寝室,我赶紧把新的数据放进去,对准空调,发了开机指令……再发一次……再发一次

还是没用??不过关机指令倒是可以了

这说明发送是可以的,只是内容还有问题.

在网上搜寻了很久的开关机数据,都不太行,这是我想起了那个美的空调遥控指令的库!

之前,我不能使用它,是因为IRRemote中无法使用Linkit One的PWM波发生器,所以如果我改了那个发射函数,不就OK了嘛!

它需要什么方法我就给什么方法,连名字和参数都一样!

VKIRSender.h

class VKIRSender{
public:
    void mark(int time);
    void space(int time);
    void enableIROut(int hz);
};

VKIRSender.cpp

#include "VKIRSender.h"
#include <Arduino.h>

#define PWM_PIN 3

void VKIRSender::mark(int time){
    analogWriteAdvance(PWM_PIN,PWM_SOURCE_CLOCK_13MHZ,PWM_CLOCK_DIV1,342,171);
    delayMicroseconds(time);
}

void VKIRSender::space(int time){
    analogWriteAdvance(PWM_PIN,PWM_SOURCE_CLOCK_13MHZ,PWM_CLOCK_DIV1,342,0);
    delayMicroseconds(time);
}

void VKIRSender::enableIROut(int hz){

}

在这个库的帮助下,我甚至可以不使用抓来的数据,我甚至可以设定温度,冷热模式,风速……

试验了一下,!成功了

TODO

开发历程差不多就是这样,真的好艰辛~

当然,现在还存在一个比较大的问题,就是发射距离!

我只能在离空调1M的位置,让空调听话,稍微远一点,他就不听话了~

在解决了这个问题之后,就可以配合温湿度传感器,让它智能起来了!

RELEASE

前面废话那么多,这下面才是最有用的~

代码开源了,其实并没有什么很难得地方,只是由于踩了很多坑,所以我觉得公开给大家伙用用,可能还是蛮好的.

Github看,给个Star当然也是极好的~

接线差不多是这个样子的

board

Android上见鬼的蓝牙

以前发的各种蓝牙开发的文章中使用的程序,都是在我自己的手机上完美运行过的.

我信心满满的就把app发布了.

之后就收到了很多邮件…各种投诉,说蓝牙完全搜索不到啦!蓝牙连不上啦!用不了啦!

和公司之前所有Android版本的蓝牙功能相比,我做的这个版本在扫描附近蓝牙的时候,可以实时的更新蓝牙信号的强度.
完美解决了之前用户拿着app不知道哪一个蓝牙是前轮,哪一个蓝牙是后轮.

这一套在我自己的手机(Nexus 6P)上完美流畅的运行了,在一些用户的手机上竟然出现了,完全扫描不到蓝牙外设的情况!

其实在发布之前,我们公司内部所有的android手机都参与了测试,虽然手机不同,性能不同,但是都没有出现完全搜索不到的现象.

于是我掏出我的坚果手机(不是坚果pro),打开了搜索界面,也出现了搜索的结果,只不过…过了一会儿,它们就消失了!

Debug

于是我就接上电脑看看控制台都说了一些什么…

在搜索到了某一个设备的蓝牙广播之后,系统好像就不想再收这个蓝牙外设的广播信号了!

我的搜索界面是定时刷新可用的蓝牙外设列表的,在判断某一个蓝牙外设是不是还存在的时候,我用了时间戳.每次底层搜索到蓝牙外设的时候,我会在model层更新这个蓝牙外设的时间戳.每次View层获取最新列表数据的时候,把每个设备的更新时间戳减去当前的时间戳,如果超过一个阈值,说明这个蓝牙外设已经不存在了.

public class ScanItem {

    private BluetoothDevice device;
    private int rssi;
    private int model;
    private long updateTime;
    private boolean connecting;
    private boolean connected;

    /**
    ...
    **/

    public boolean isAvailable(){
        return ((System.currentTimeMillis()-updateTime)<2000);
    }
}

在判断这里我的阈值是2秒,看来在某些垃圾手机上面,是不能酱紫的.于是我就改成了20秒.

运行起来,发现还是不行……

因为,不管过多少秒,这个垃圾系统都不在把收到的广播信息返回给我了!

See see dalao’s work

然后我想到,有那么多做蓝牙的app,我看看它们怎么运行的,怎么实时更新信号强度的.

我下载了我最喜欢的BLE Scanner,运行了起来,发现很完美啊.

然后我就在控制台里面看,发现这个app在一个劲的停止搜索,开始搜索,停止搜索,开始搜索……

或许这是一个好方法!既然这种破系统一次扫描对一个设备只会给一次广播信号,那我只能多次停止扫描开启扫描咯.

if (VKBluetoothManager.getInstance().isScanning()){
    VKBluetoothManager.getInstance().stopScan();
    handler.postDelayed(scanControlRunnable,50);
}else {
    VKBluetoothManager.getInstance().scanDevices();
    handler.postDelayed(scanControlRunnable,950);
}

心满意足的运行了起来,发现完美,这个破坚果终于达成了实时刷新的效果!

But……在Nexus 6P上面竟然出现了系统错误

E/BtGatt.GattService: App 'XuanWheel' is scanning too frequently

Are you f**king kidding me?

这样不行,那样也不行~

扫描的太频繁,意思应该是刚结束扫描后,再次开始的时间间隔太短了吧.

于是我就改了两个时间postdelay的延迟时间

if (VKBluetoothManager.getInstance().isScanning()){
    VKBluetoothManager.getInstance().stopScan();
    handler.postDelayed(scanControlRunnable,2000);
}else {
    VKBluetoothManager.getInstance().scanDevices();
    handler.postDelayed(scanControlRunnable,4000);
}

这样子就可以了!

嗯,虽然说这样子实现了实时刷新信号强度的功能,但是毕竟刷新率还是很低的……而且本来Nexus 6p可以拥有更高的刷新率,为了兼容这些臭系统,只能一起降低效果了……这么看起来,还是iOS比较好~

Why

那么,为啥某些Android系统要这么搞呢!

我猜,他们为了提高电池寿命,节省CPU资源,让UI更加流畅,所以牺牲了一些使用频率不那么高的外设的性能,于是我们可怜的蓝牙,就这么被阉割了.不过你们在系统里面搞幺蛾子,用户不会说什么,但是用户的手机在使用需要蓝牙的app的时候,这些开发者就很悲催了呀~他们(我们)明明遵守着设计准则,却发现自己的程序跑在了假系统上,还被用户吐槽,被打一星,真是有苦说不清啊…

用handler/thread来做定时器

刚开始接触android编程的时候,是大三.那时候在做精益防伪的演示版App,网络协议用的是socket.

使用socket的时候,编译器告诉我,需要在新线程中来使用socket操作.

于是乎,就学习了Thread的使用.

当时觉得Thread好像还是蛮简单的.在thread里面搞一个runnable,在runnable里面跑代码块,用start(),stop()来控制线程的开始和结束.

之后在thread中操控了ui,就闪退了.因为UI只能在主线程中去操作.这iOS就牛逼了,即使不在主线程中去操作,他也不会闪退.他会自动的在主线程中去渲染.

分界线

言归正传,后面上课,老师让我们用thread来做一个定时器,在主线程中显示一个数字,在另一个线程中定时增加数字.

这玩意儿是来学习Thread和Handler的.

之后需要用到定时器的地方我都用Thread来实现了~

然而

Thread不是很安全.而且,对于不同的手机厂商,不同的ROM,对thread的操作也是不一样的.

Thread workingThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (isWorking) {
                byte[] wantSend = sendingData();
                if (wantSend == null || mainCharacteristic == null || mainGatt == null) {
                    dataPointer = 0;
                    isWorking=false;
                    workingThread.interrupt();
                } else {
                    mainCharacteristic.setValue(wantSend);
                    mainGatt.writeCharacteristic(mainCharacteristic);
                    if (dataPointer == 0 && dataStack.size() > 0) {
                        try {
                            Thread.sleep(gifInterval);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        try {
                            Thread.sleep(baseInterval);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    });

某些手机上,这个thread是停不下来的.在某些手机上,这个thread是无法重启的.在某些手机上,这个thread是无法中断的.

这就意味着,某些手机上,炫轮app只能发送一次图片;在某些手机上,炫轮app无法显示附近的蓝牙.

查了一些资料,我才发现thread,的很多接口都被废弃了,而且非常不安全,很容易出现锁死卡死的情况.

于是我去检查了很早很早很早的炫轮app.发现是用延迟来做定时器功能的.

于是我把Runnable提取出来了:

Runnable sendingRunnable=new Runnable() {
    @Override
    public void run() {
        if (isWorking) {
            byte[] wantSend = sendingData();
            if (wantSend == null || mainCharacteristic == null || mainGatt == null) {
                dataPointer = 0;
                isWorking=false;
            } else {
                mainCharacteristic.setValue(wantSend);
                mainGatt.writeCharacteristic(mainCharacteristic);
                if (dataPointer == 0 && dataStack.size() > 0) {
                        mHandler.postDelayed(sendingRunnable,gifInterval);
                } else {
                        mHandler.postDelayed(sendingRunnable,baseInterval);
                }
            }
        }
    }
};

用Handler的postDelayed来做延迟执行,这竟然就解决了前面所有的问题!

原因,我还要好好查些资料,再来补充.下次更新!

缓冲区为啥不溢出

软件安全性分析课,里面花了很多时间讲缓冲区溢出的故事.

大一为了加入凝聚工作室,我也是查过相关的资料,也做过类似的实验,但都是瞎搞.

在老师给出例子之前,我一直把失败归结于自己的代码有问题,或者攻击方式有问题.

在看了老师的例子之后,我觉得我好像没写错什么…

Run directly

现在内核牛逼了呢!

检查到了pc指向非法地址,就直接被内核终止了,所以,我也没办法

Debug with GDB

GDB真是个牛逼的东西,可以反编译,可以打断点,可以查看堆栈地址数据……

只要我在执行非法内存之前加个断点,看看栈数据是不是变得乱七八糟了,我的实验目的就达到了.

根据老师说的

char c[20];
char a;

这玩意儿运行的时候,在栈上面的顺序是ca

所以只要用不安全的函数,冲破c的长度,就可以覆盖a的数据.

然而gdb断点的时候,我惊奇的发现顺序是ac…

于是乎我就换了个代码

char a;
char c[20];
char b;

发现,栈里面是abc…

这,这你让我怎么溢出??

还要再研究研究…不论我是否使用优化编译,结果都是这样的…

等我找到答案,再来更新这一篇!

啥都不懂…

这学期好像做了个死,选了一堆学不会的课~

什么机器学习啊(全是数学,全是概率,已弃疗)

什么Linux内核啊(除了学会了装内核,使用gdb,和装逼,别的都没听懂)

什么软件安全性分析啊(我自己实验永远做不出老师的效果)

安全通信啊(这个我还听得懂……)

报名了华为软件挑战赛(也弃疗了)

为了要听懂Linux内核的课和一部分软件安全性分析的课,我借了买了一些计算机组成原理,编译原理,Binary Hack的书.

好像也没咋看懂~

然后要做推荐算法,就看数据挖掘,好像还是没咋看懂~

所以我承认我笨了…

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给记录下来,不然就懵逼咯.