2016-01-13 / 23:02:13

刚才在微信上看到这篇由cocoachina翻译小组成员翻译的文章,觉得还是挺值得参考的,因此转载至此,原文请移步:http://robots.thoughtbot.com/how-to-handle-large-amounts-of-data-on-maps/。

如何在iOS地图上以用户可以理解并乐于接受的方式来处理和显示大量数据?这个教程将会给大家进行示例说明。

我们要开发一款iOS的app应用,这个应用包含有87000个旅馆的信息,每个旅馆的信息中包括有一个坐标值,一个旅馆名跟一个电话号码。这款app可以在用户拖动、放大缩小地图时更新旅馆数据,而不需要用户重新进行搜索。

为了达到这个目的,我们需要构造一个可快速检索的数据结构。C语言的性能高,所以我们用C语言来构造这个数据结构。为了确保大量的数据不会让用户感到迷惑,所以我们还需要想出一个合并数据的解决方案。最后,为了更好的适应市场,我们需要把app做的更完善一些。

完成这个教学后,你将学到这款app的所有核心内容。

数据结构

首先我们先来分析下数据,搞清我们要如何处理数据。旅馆数据中包含了一系列的坐标点(包括纬度和经度),我们需要根据这些坐标点在地图上进行标注。地图可以任意的拖动并放大缩小,所以我们不需要把所有的点都全部绘制出来,我们只需要绘制可以显示在屏幕上的点。核心问题是:我们需要查询出显示在屏幕上的所有的点,所以我们要想出一个查找算法,查找存在于一个矩形范围内的所有点。

一个简单的解决方式就是遍历所有的点,然后判断(xMin<=x<=xMax并且yMin<=y<=yMax),很不幸,这是一个复杂度为O(N)的算法,显然不适合我们的情况。

这儿有个更好的解决方法,就是我们可以利用对称性来减少我们的查询范围。那么如何能通过查询的每一次的迭代来减少查询的范围呢?我们可以在每个区域内都加索引,这样可以有效减少查询的范围。这种区域索引的方式可以用四叉树来实现,查询复杂度为O(H)(H是查询的那个点所在的树的高度)

四叉树

四叉树是一个数据结构,由一系列的结点(node)构成。每个结点包含一个桶(bucket)跟一个包围框(boundingbox)。每个桶里面有一系列的点(point)。如果一个点包含在一个外包围框A中,就会被添加到A所在结点的桶(bucket)中。一旦这个结点的桶满了,这个结点就会分裂成四个子结点,每个子节点的包围框分别是当前结点包围框的1/4。分裂之后那些本来要放到当前结点桶中的点就都会放到子容器的桶中。

那么我们该如何来对四叉树进行编码呢?

我们先来定义基本的结构:

 1 typedef struct TBQuadTreeNodeData {
 2     double x;
 3     double y;
 4     void* data;
 5 } TBQuadTreeNodeData;
 6 TBQuadTreeNodeData TBQuadTreeNodeDataMake(double x, double y, void* data);
 7
 8 typedef struct TBBoundingBox {
 9     double x0; double y0;
10     double xf; double yf;
11 } TBBoundingBox;
12 TBBoundingBox TBBoundingBoxMake(double x0, double y0, double xf, double yf);
13
14 typedef struct quadTreeNode {
15     struct quadTreeNode* northWest;
16     struct quadTreeNode* northEast;
17     struct quadTreeNode* southWest;
18     struct quadTreeNode* southEast;
19     TBBoundingBox boundingBox;
20     int bucketCapacity;
21     TBQuadTreeNodeData *points;
22     int count;
23 } TBQuadTreeNode;
24 TBQuadTreeNode* TBQuadTreeNodeMake(TBBoundingBox boundary, int bucketCapacity);

TBQuadTreeNodeData结构包含了坐标点(纬度,经度)。

void *data是一个普通的指针,用来存储我们需要的其他信息,如旅馆名跟电话号码。

TBBoundingBox代表一个用于范围查询的长方形,也就是之前谈到(xMin<=x<=xMax&&yMin<=y<=yMax)查询的那个长方形。左上角是(xMin,yMin),右下角是(xMax,yMax)。

最后,我们看下TBQuadTreeNode结构,这个结构包含了四个指针,每个指针分别指向这个结点的四个子节点。它还有一个外包围框和一个数组(数组中就是那个包含一系列坐标点的桶)。

在我们建立完四叉树的同时,空间上的索引也就同时形成了。这是生成四叉树的演示动画。

下面的代码准确描述了以上动画的过程:

 1 void TBQuadTreeNodeSubdivide(TBQuadTreeNode* node) {
 2     TBBoundingBox box = node->boundingBox;
 3
 4     double xMid = (box.xf + box.x0) / 2.0;
 5     double yMid = (box.yf + box.y0) / 2.0;
 6
 7     TBBoundingBox northWest = TBBoundingBoxMake(box.x0, box.y0, xMid, yMid);
 8     node->northWest = TBQuadTreeNodeMake(northWest, node->bucketCapacity);
 9
10     TBBoundingBox northEast = TBBoundingBoxMake(xMid, box.y0, box.xf, yMid);
11     node->northEast = TBQuadTreeNodeMake(northEast, node->bucketCapacity);
12
13     TBBoundingBox southWest = TBBoundingBoxMake(box.x0, yMid, xMid, box.yf);
14     node->southWest = TBQuadTreeNodeMake(southWest, node->bucketCapacity);
15
16     TBBoundingBox southEast = TBBoundingBoxMake(xMid, yMid, box.xf, box.yf);
17     node->southEast = TBQuadTreeNodeMake(southEast, node->bucketCapacity);
18 }
19
20 bool TBQuadTreeNodeInsertData(TBQuadTreeNode* node, TBQuadTreeNodeData data) {
21     // Bail if our coordinate is not in the boundingBox
22     if (!TBBoundingBoxContainsData(node->boundingBox, data)) {
23         return false;
24     }
25
26     // Add the coordinate to the points array
27     if (node->count < node->bucketCapacity) {
28         node->points[node->count++] = data;
29         return true;
30     }
31
32     // Check to see if the current node is a leaf, if it is, split
33     if (node->northWest == NULL) {
34         TBQuadTreeNodeSubdivide(node);
35     }
36
37     // Traverse the tree
38     if (TBQuadTreeNodeInsertData(node->northWest, data)) return true;
39     if (TBQuadTreeNodeInsertData(node->northEast, data)) return true;
40     if (TBQuadTreeNodeInsertData(node->southWest, data)) return true;
41     if (TBQuadTreeNodeInsertData(node->southEast, data)) return true;
42
43     return false;
44 } 

现在我们已经完成了四叉树的构造,我们还需要在四叉树上进行区域范围查询并返回TBQuadTreeNodeData结构。以下是区域范围查询的演示动画,在浅蓝区域内的是所有的标注点。当标注点被查询到在指定的区域范围内,则会被标注为绿色。

以下是查询代码:

 1 typedef void(^TBDataReturnBlock)(TBQuadTreeNodeData data);
 2
 3 void TBQuadTreeGatherDataInRange(TBQuadTreeNode* node, TBBoundingBox range, TBDataReturnBlock block) {
 4     // If range is not contained in the node's boundingBox then bail
 5     if (!TBBoundingBoxIntersectsBoundingBox(node->boundingBox, range)) {
 6         return;
 7     }
 8
 9     for (int i = 0; i < node->count; i++) {
10         // Gather points contained in range
11         if (TBBoundingBoxContainsData(range, node->points[i])) {
12             block(node->points[i]);
13         }
14     }
15
16     // Bail if node is leaf
17     if (node->northWest == NULL) {
18         return;
19     }
20
21     // Otherwise traverse down the tree
22     TBQuadTreeGatherDataInRange(node->northWest, range, block);
23     TBQuadTreeGatherDataInRange(node->northEast, range, block);
24     TBQuadTreeGatherDataInRange(node->southWest, range, block);
25     TBQuadTreeGatherDataInRange(node->southEast, range, block);
26 }

用四叉树这种结构可以进行快速的查询。在一个包含成百上千条数据的数据库中,可以以60fps的速度查询上百条数据。

用旅馆数据来填充四叉树

旅馆的数据来自于POIplaza这个网站,而且已经格式化成csv文件。我们要从硬盘中读取出数据并对数据进行转换,最后用数据来填充四叉树。

创建四叉树的代码在 TBCoordinateQuadTreeController

 1 typedef struct TBHotelInfo {
 2     char* hotelName;
 3     char* hotelPhoneNumber;
 4 } TBHotelInfo;
 5
 6 TBQuadTreeNodeData TBDataFromLine(NSString *line) {
 7     // Example line:
 8     // -80.26262, 25.81015, Everglades Motel, USA-United States, +1 305-888-8797
 9
10     NSArray *components = [line componentsSeparatedByString:@","];
11     double latitude = [components[1] doubleValue];
12     double longitude = [components[0] doubleValue];
13
14     TBHotelInfo* hotelInfo = malloc(sizeof(TBHotelInfo));
15
16     NSString *hotelName = [components[2] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
17     hotelInfo->hotelName = malloc(sizeof(char) * hotelName.length + 1);
18     strncpy(hotelInfo->hotelName, [hotelName UTF8String], hotelName.length + 1);
19
20     NSString *hotelPhoneNumber = [[components lastObject] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
21     hotelInfo->hotelPhoneNumber = malloc(sizeof(char) * hotelPhoneNumber.length + 1);
22     strncpy(hotelInfo->hotelPhoneNumber, [hotelPhoneNumber UTF8String], hotelPhoneNumber.length + 1);
23
24     return TBQuadTreeNodeDataMake(latitude, longitude, hotelInfo);
25 }
26
27 - (void)buildTree {
28     NSString *data = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"USA-HotelMotel" ofType:@"csv"] encoding:NSASCIIStringEncoding error:nil];
29     NSArray *lines = [data componentsSeparatedByString:@"\n"];
30
31     NSInteger count = lines.count - 1;
32
33     TBQuadTreeNodeData *dataArray = malloc(sizeof(TBQuadTreeNodeData) * count);
34     for (NSInteger i = 0; i < count; i++) {
35         dataArray[i] = TBDataFromLine(lines[i]);
36     }
37
38     TBBoundingBox world = TBBoundingBoxMake(19, -166, 72, -53);
39     _root = TBQuadTreeBuildWithData(dataArray, count, world, 4);
40 }

现在我们用iPhone上预加载的数据创建了一个四叉树。接下来我们将处理app的下一个部分:合并数据(clustering)。

合并数据(clustering)

现在我们有了一个装满旅馆数据的四叉树,可以用来解决合并数据的问题了。首先,让我们来探索下合并数据的原因。我们合并数据是因为我们不想因为数据过于庞大而使用户迷惑。实际上有很多种方式可以解决这个问题。GoogleMaps根据地图的缩放等级(zoomlevel)来显示搜索结果数据中的一部分数据。地图放的越大,就越能清晰的看到更细节的标注,直到你能看到所有有效的标注。我们将采用这种合并数据的方式,只显示出来旅馆的个数,而不在地图上显示出所有的旅馆信息。

最终呈现的标注是一个中心显示旅馆个数的小圆圈。实现的原理跟如何把图片缩小的原理差不多。我们先在地图上画一个格子。每个格子中包含了很多个小单元格,每个小单元格中的所有旅馆数据合并出一个标注。然后通过每个小单元格中所有旅馆的坐标值的平均值来决定合并后这个标注的坐标值。

这是以上处理的演示动画。

以下是代码实现过程。在TBCoordinateQuadTree类中添加了一个方法。

 1 - (NSArray *)clusteredAnnotationsWithinMapRect:(MKMapRect)rect withZoomScale:(double)zoomScale {
 2     double TBCellSize = TBCellSizeForZoomScale(zoomScale);
 3     double scaleFactor = zoomScale / TBCellSize;
 4
 5     NSInteger minX = floor(MKMapRectGetMinX(rect) * scaleFactor);
 6     NSInteger maxX = floor(MKMapRectGetMaxX(rect) * scaleFactor);
 7     NSInteger minY = floor(MKMapRectGetMinY(rect) * scaleFactor);
 8     NSInteger maxY = floor(MKMapRectGetMaxY(rect) * scaleFactor);
 9
10     NSMutableArray *clusteredAnnotations = [[NSMutableArray alloc] init];
11
12     for (NSInteger x = minX; x <= maxX; x++) {
13         for (NSInteger y = minY; y <= maxY; y++) {
14
15             MKMapRect mapRect = MKMapRectMake(x / scaleFactor, y / scaleFactor, 1.0 / scaleFactor, 1.0 / scaleFactor);
16
17             __block double totalX = 0;
18             __block double totalY = 0;
19             __block int count = 0;
20
21             TBQuadTreeGatherDataInRange(self.root, TBBoundingBoxForMapRect(mapRect), ^(TBQuadTreeNodeData data) {
22                 totalX += data.x;
23                 totalY += data.y;
24                 count++;
25             });
26
27             if (count >= 1) {
28                 CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(totalX / count, totalY / count);
29                 TBClusterAnnotation *annotation = [[TBClusterAnnotation alloc] initWithCoordinate:coordinate count:count];
30                 [clusteredAnnotations addObject:annotation];
31             }
32         }
33     }
34
35     return [NSArray arrayWithArray:clusteredAnnotations];
36 }

上面的方法在指定小单元格大小的前提下合并数据生成了最终的标注。现在我们需要做的就是把这些标注绘制到MKMapView上。首先我们创建一个UIViewController的子类,然后用MKMapView作为它的view视图。在可视区域改变的情况下,我们需要实时更新标注的显示,所以我们要实现mapView:regionDidChangeAnimated:的协议方法。

1 - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
2     [[NSOperationQueue new] addOperationWithBlock:^{
3         double zoomScale = self.mapView.bounds.size.width / self.mapView.visibleMapRect.size.width;
4         NSArray *annotations = [self.coordinateQuadTree clusteredAnnotationsWithinMapRect:mapView.visibleMapRect withZoomScale:zoomScale];
5
6         [self updateMapViewAnnotationsWithAnnotations:annotations];
7     }];
8 }

只添加必要的标注

在主线程中我们期望尽可能花费较少时间来做运算,这意味着我们要尽可能的把所有内容都放到后台的线程中。为了在主线程中花费更少的时间来做计算,我们只需要绘制一些必要的标注。这可以避免用户滑动过程中感到很卡,从而保证流畅的用户体验。

开始之前,我们看一下下面的图片:

左边的屏幕截图是地图进行滑动前的地图快照。这个快照中的标注就是目前mapView中的标注,我们称这个为"before集合"。

右边的屏幕截图是地图进行滑动后的地图快照。这个快照中的标注就是从clusteredAnnotationsWithinMapRect:withZoomScale:这个函数中得到的返回值。我们称这个为"after集合"。

我们期望保留两个快照中都存在的标注点(即重合的那些标注点),去除在"after集合"中不存在的那些标注点,同时添加那些新的标注点。

 1 - (void)updateMapViewAnnotationsWithAnnotations:(NSArray *)annotations {
 2     NSMutableSet *before = [NSMutableSet setWithArray:self.mapView.annotations];
 3     NSSet *after = [NSSet setWithArray:annotations];
 4
 5     // Annotations circled in blue shared by both sets
 6     NSMutableSet *toKeep = [NSMutableSet setWithSet:before];
 7     [toKeep intersectSet:after];
 8
 9     // Annotations circled in green
10     NSMutableSet *toAdd = [NSMutableSet setWithSet:after];
11     [toAdd minusSet:toKeep];
12
13     // Annotations circled in red
14     NSMutableSet *toRemove = [NSMutableSet setWithSet:before];
15     [toRemove minusSet:after];
16
17     // These two methods must be called on the main thread
18     [[NSOperationQueue mainQueue] addOperationWithBlock:^{
19         [self.mapView addAnnotations:[toAdd allObjects]];
20         [self.mapView removeAnnotations:[toRemove allObjects]];
21     }];
22 }

这样我们尽可能的确保在主线程上做少量的工作,从而提升地图滑动的流畅性。

接下来我们来看下如何绘制标注,并且在标注上显示出来旅馆的个数。最后我们给标注加上点击事件,这样使得app从头到脚都可以表现的非常完美。

绘制标注

由于我们在地图上并没有完全显示出全部旅馆,所以我们需要在剩余的这些标注上表现出真实的旅馆总量。

首先创建一个圆形的标注,中间显示合并后的个数,也就是旅馆的真实总量。这个圆形的大小同样可以反映出合并后的个数。

为了实现这个需求,我们要找出一个方程式,允许我们在1到500+的数值中生成一个缩小后的数值。用这个数值来作为标注的大小。我们将用到以下的方程式。

x值较低的时候f(x)增长的比较快,x在值变大的时候f(x)增长变缓慢,β值用来控制f(x)趋于1的速度。α值影响最小值(在我们的项目中,我们的最小合并值(也就是1)能占总共最大值的60%)。

 1 static CGFloat const TBScaleFactorAlpha = 0.3;
 2 static CGFloat const TBScaleFactorBeta = 0.4;
 3
 4 CGFloat TBScaledValueForValue(CGFloat value) {
 5     return 1.0 / (1.0 + expf(-1 * TBScaleFactorAlpha * powf(value, TBScaleFactorBeta)));
 6 }
 7
 8 - (void)setCount:(NSUInteger)count {
 9     _count = count;
10
11     // Our max size is (44,44)
12     CGRect newBounds = CGRectMake(0, 0, roundf(44 * TBScaledValueForValue(count)), roundf(44 * TBScaledValueForValue(count)));
13     self.frame = TBCenterRect(newBounds, self.center);
14
15     CGRect newLabelBounds = CGRectMake(0, 0, newBounds.size.width / 1.3, newBounds.size.height / 1.3);
16     self.countLabel.frame = TBCenterRect(newLabelBounds, TBRectCenter(newBounds));
17     self.countLabel.text = [@(_count) stringValue];
18
19     [self setNeedsDisplay];
20 }

现在标注的大小已经OK了。让我们再来把这个标注做漂亮些。

 1 - (void)setupLabel {
 2     _countLabel = [[UILabel alloc] initWithFrame:self.frame];
 3     _countLabel.backgroundColor = [UIColor clearColor];
 4     _countLabel.textColor = [UIColor whiteColor];
 5     _countLabel.textAlignment = NSTextAlignmentCenter;
 6     _countLabel.shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75];
 7     _countLabel.shadowOffset = CGSizeMake(0, -1);
 8     _countLabel.adjustsFontSizeToFitWidth = YES;
 9     _countLabel.numberOfLines = 1;
10     _countLabel.font = [UIFont boldSystemFontOfSize:12];
11     _countLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters;
12     [self addSubview:_countLabel];
13 }
14
15 - (void)drawRect:(CGRect)rect {
16     CGContextRef context = UIGraphicsGetCurrentContext();
17
18     CGContextSetAllowsAntialiasing(context, true);
19
20     UIColor *outerCircleStrokeColor = [UIColor colorWithWhite:0 alpha:0.25];
21     UIColor *innerCircleStrokeColor = [UIColor whiteColor];
22     UIColor *innerCircleFillColor = [UIColor colorWithRed:(255.0 / 255.0) green:(95 / 255.0) blue:(42 / 255.0) alpha:1.0];
23
24     CGRect circleFrame = CGRectInset(rect, 4, 4);
25
26     [outerCircleStrokeColor setStroke];
27     CGContextSetLineWidth(context, 5.0);
28     CGContextStrokeEllipseInRect(context, circleFrame);
29
30     [innerCircleStrokeColor setStroke];
31     CGContextSetLineWidth(context, 4);
32     CGContextStrokeEllipseInRect(context, circleFrame);
33
34     [innerCircleFillColor setFill];
35     CGContextFillEllipseInRect(context, circleFrame);
36 } 

添加最后的touch事件

目前的标注可以很好的呈现出我们的数据了,让我们最后添加一些touch事件来让我们的app用起来更有趣。

首先,我们需要为新添加到地图上的标注做一个动画。如果没有添加动画的话,新的标注就会在地图上突然出现,体验效果将会大打折扣。

 1 - (void)addBounceAnnimationToView:(UIView *)view {
 2     CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
 3
 4     bounceAnimation.values = @[@(0.05), @(1.1), @(0.9), @(1)];
 5
 6     bounceAnimation.duration = 0.6;
 7     NSMutableArray *timingFunctions = [[NSMutableArray alloc] init];
 8     for (NSInteger i = 0; i < 4; i++) {
 9         [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
10     }
11     [bounceAnimation setTimingFunctions:timingFunctions.copy];
12     bounceAnimation.removedOnCompletion = NO;
13
14     [view.layer addAnimation:bounceAnimation forKey:@"bounce"];
15 }
16
17 - (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
18     for (UIView *view in views) {
19         [self addBounceAnnimationToView:view];
20     }
21 }

接下来,我们想要根据地图的缩放比例来改变在合并时的小单元格(cell)的大小。在地图进行放大时,小单元格变小。所以我们需要定义一下当前地图的缩放比例。也就是scale=mapView.bounds.size.width/mapView.visibleMapRect.size.width:

1 NSInteger TBZoomScaleToZoomLevel(MKZoomScale scale) {
2     double totalTilesAtMaxZoom = MKMapSizeWorld.width / 256.0;
3     NSInteger zoomLevelAtMaxZoom = log2(totalTilesAtMaxZoom);
4     NSInteger zoomLevel = MAX(0, zoomLevelAtMaxZoom + floor(log2f(scale) + 0.5));
5
6     return zoomLevel;
7 }

我们为每个地图缩放的比例都定义一个常量 zoomLevel

 1 float TBCellSizeForZoomScale(MKZoomScale zoomScale) {
 2     NSInteger zoomLevel = TBZoomScaleToZoomLevel(zoomScale);
 3
 4     switch (zoomLevel) {
 5         case 13:
 6         case 14:
 7         case 15:
 8             return 64;
 9         case 16:
10         case 17:
11         case 18:
12             return 32;
13         case 19:
14             return 16;
15
16         default:
17             return 88;
18     }
19 }

现在我们放大地图,我们将看到逐渐变小的标注,直到最后我们能看到代表每个旅馆的那个标注。

源代码见:the complete working version of the app on GitHub。

转载于:https://www.cnblogs.com/xjy-123/p/5126010.html

如何在iOS地图上高效的显示大量数据相关推荐

  1. ios 按钮下面加下划线_如何在iOS按钮上的文字添加下划线

    在开发ios的时候很多用户们都会想如何在iOS按钮上的文字添加下划线,那么接下来的内容中我们就一起去看看在iOS按钮上的文字添加下划线的方法. 问题:实现下图中右侧的按钮文字效果 方法: [MyToo ...

  2. 解决 .webp 格式图片在 ios 设备上无法正常显示的问题

    解决.webp 格式图片在 ios 设备上无法正常显示的问题 使用字符串的 replace() 方法,将 webp 的后缀名替换为 jpg 的后缀名 // 定义请求商品详情数据的方法 async ge ...

  3. android手机icoude邮件,如何在Android设备上访问Apple应用和数据?教程来了!

    原标题:如何在Android设备上访问Apple应用和数据?教程来了! 对于一些出于工作原因,拥有ios和Android不同系统手机的人来说,有时候会觉得在共享资料或数据上会比较麻烦.但是,其实并非如 ...

  4. vscode重置应用程序_如何在Windows 10上重置应用程序的数据

    vscode重置应用程序 With Windows 10's Anniversary Update, you can now reset an app's data without actually ...

  5. 苹果隐藏应用_如何在iOS 14上隐藏应用页?苹果手机上管理应用更方便

    Apple的新iOS 14带来了一种新方法,可以帮助您整理iPhone上数量不断增长的应用程序.这是一种隐藏几乎从未或从未使用过的应用程序的方法. 即使您设法将iPhone保持在一个或两个应用程序的屏 ...

  6. 怎么在谷歌地图上画图_如何在Google地图上规划具有多个目的地的公路旅行

    怎么在谷歌地图上画图 Whether you're planning a day out on the town, or want to orchestrate the perfect road tr ...

  7. EMQ x 阿里云:云上高效构建,IoT 数据一站处理|直播预告

    随着物联网与云计算的发展,进入云时代以来,各企业的数字化转型也纷纷「云」化.在云上构建可弹性伸缩.自动化管理.承载海量物联网设备连接的数据中心,从而实现企业的降本增效,成为大势所趋. 为了帮助企业应对 ...

  8. ios 拍照 实现 连拍_如何在iOS设备上使用连拍模式拍照

    ios 拍照 实现 连拍 We're sure you've tried to capture that perfect moment with your camera and you're just ...

  9. HTML内嵌pdf在ios设备上无法正常显示

    问题描述 今天在制作网页的时候,需要内嵌一个pdf,使用html中的embed来实现: <embed src="doc.pdf" type="application ...

  10. 信道检测手机软件 ios_如何在iOS设备上用PS4或Xbox One的手柄玩游戏?

    苹果此前已经确认将在iOS 13系统中增加对PS4和Xbox One手柄的支持,玩家们可以通过蓝牙连接iOS 13的苹果设备,然后用手柄进行游戏(需要游戏本身支持,现在数量也有不少了,比如<堡垒 ...

最新文章

  1. 一致性hash算法简介
  2. Html,xhtml,xml的定义和区别
  3. addr2line命令
  4. 并发编程-13线程安全策略之两种类型的同步容器
  5. 撸过一万行代码,你看过这篇文章吗?
  6. 【渝粤题库】国家开放大学2021春2180办公室管理题目
  7. 融合大数据能力,解决在存量时代下的力分之困
  8. 30岁的你,目标工资是多少?
  9. iOS开发UI篇—APP主流UI框架结构
  10. 蓝桥杯 ADV-202算法提高 最长公共子序列(动态规划)
  11. Alexa Top 1000000
  12. 波波腾机器人_GitHub - bobowire/Wireboy.SDK.CQP: C# .net框架的酷Q机器人插件SDK
  13. android 设置锁屏壁纸
  14. !doctype html public 广告飘窗不能用了,页面广告飘窗
  15. mac + win ,用python一行代码批量下载哔哩哔哩视频
  16. 【视野】中国的程序员培训是不是有问题?
  17. 高中信息技术计算机网络教案,信息技术 - 第八册计算机教案(全册)-四年级...
  18. 重要的, 需要记下来的
  19. 微博数据解析 | 可口可乐 VS 元气森林
  20. 进入IT互联网行业一定要报培训班嘛?

热门文章

  1. mapreduce 多种输入
  2. Linux系统磁盘管理基本知识
  3. 中国“互联网+酒店”专题研究报告2015
  4. 25 条 SSH 命令和技巧
  5. html 获取mac地址,JS获取客户端IP地址与MAC地址示例
  6. MySQL show processlist
  7. svn checkout的时候报E000022错误
  8. python加mysql加界面用代码写,使用python写一个监控mysql的脚本,在zabbix web上加上模板...
  9. java8 function 多线程安全_Java8新特性_传统时间格式化的线程安全问题
  10. 升级ssl后ssh登录失效_centos7升级openssl、openssh常见问题及解决方法