<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文主要介紹內容有:
如果讓你設計一個APP首頁查詢的介面,它需要查使用者資訊、需要查banner
資訊、需要查標籤資訊等等。
一般情況,小夥伴會實現如下:
public AppHeadInfoResponse queryAppHeadInfo(AppInfoReq req) { //查使用者資訊 UserInfoParam userInfoParam = buildUserParam(req); UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam); //查banner資訊 BannerParam bannerParam = buildBannerParam(req); BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam); //查標籤資訊 LabelParam labelParam = buildLabelParam(req); LabelDTO labelDTO = labelService.queryLabelInfo(labelParam); //組裝結果 return buildResponse(userInfoDTO,bannerDTO,labelDTO); }
這段程式碼會有什麼問題嘛? 其實這是一段挺正常的程式碼,但是這個方法實現中,查詢使用者、banner、標籤資訊,是序列的,如果查詢使用者資訊200ms
,查詢banner資訊100ms
,查詢標籤資訊200ms
的話,耗時就是500ms
啦。
其實為了優化效能,我們可以修改為並行呼叫的方式,耗時可以降為200ms
,如下圖所示:
對於上面的例子,如何實現並行呼叫呢?
有小夥伴說,可以使用Future+Callable
實現多個任務的並行呼叫。但是執行緒池執行批次任務時,返回值用Future的get()
獲取是阻塞的,如果前一個任務執行比較耗時的話,get()
方法會阻塞,形成排隊等待的情況。
而CompletionService
是對定義ExecutorService
進行了包裝,可以一邊生成任務,一邊獲取任務的返回值。讓這兩件事分開執行,任務之間不會互相阻塞,可以獲取最先完成的任務結果。
CompletionService
的實現原理比較簡單,底層通過FutureTask+阻塞佇列,實現了任務先完成的話,可優先獲取到。也就是說任務執行結果按照完成的先後順序來排序,先完成可以優化獲取到。內部有一個先進先出的阻塞佇列,用於儲存已經執行完成的Future,你呼叫CompletionService
的poll或take方法即可獲取到一個已經執行完成的Future,進而通過呼叫Future介面實現類的get
方法獲取最終的結果。
接下來,我們來看下,如何用CompletionService
,實現並行查詢APP首頁資訊哈。
思考步驟如下:
我們先把查詢使用者資訊的任務,放到執行緒池,如下:
ExecutorService executor = Executors.newFixedThreadPool(10); //查詢使用者資訊 CompletionService<UserInfoDTO> userDTOCompletionService = new ExecutorCompletionService<UserInfoDTO>(executor); Callable<UserInfoDTO> userInfoDTOCallableTask = () -> { UserInfoParam userInfoParam = buildUserParam(req); return userService.queryUserInfo(userInfoParam); }; userDTOCompletionService.submit(userInfoDTOCallableTask);
banner
資訊的任務,也放到這個執行緒池的話,發現不好放了,因為返回型別不一樣,一個是UserInfoDTO
,另外一個是BannerDTO
。那這時候,我們是不是把泛型宣告為Object即可,因為所有物件都是繼承於Object的?如下:ExecutorService executor = Executors.newFixedThreadPool(10); //查詢使用者資訊 CompletionService<Object> baseDTOCompletionService = new ExecutorCompletionService<Object>(executor); Callable<Object> userInfoDTOCallableTask = () -> { UserInfoParam userInfoParam = buildUserParam(req); return userService.queryUserInfo(userInfoParam); }; //banner資訊任務 Callable<Object> bannerDTOCallableTask = () -> { BannerParam bannerParam = buildBannerParam(req); return bannerService.queryBannerInfo(bannerParam); }; //提交使用者資訊任務 baseDTOCompletionService.submit(userInfoDTOCallableTask); //提交banner資訊任務 baseDTOCompletionService.submit(bannerDTOCallableTask);
Object
是使用者資訊的DTO,哪個是BannerDTO
?怎麼辦呢?這時候,我們可以在引數裡面做個擴充套件嘛,即引數宣告為一個基礎物件BaseRspDTO,再搞個泛型放Object資料的,然後基礎物件BaseRspDTO有個區分是UserDTO還是BannerDTO的唯一標記屬性key。程式碼如下:public class BaseRspDTO<T extends Object> { //區分是DTO返回的唯一標記,比如是UserInfoDTO還是BannerDTO private String key; //返回的data private T data; public String getKey() { return key; } public void setKey(String key) { this.key = key; } public T getData() { return data; } public void setData(T data) { this.data = data; } } //並行查詢App首頁資訊 public AppHeadInfoResponse parallelQueryAppHeadPageInfo(AppInfoReq req) { long beginTime = System.currentTimeMillis(); System.out.println("開始並行查詢app首頁資訊,開始時間:" + beginTime); ExecutorService executor = Executors.newFixedThreadPool(10); CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor); //查詢使用者資訊任務 Callable<BaseRspDTO<Object>> userInfoDTOCallableTask = () -> { UserInfoParam userInfoParam = buildUserParam(req); UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam); BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>(); userBaseRspDTO.setKey("userInfoDTO"); userBaseRspDTO.setData(userInfoDTO); return userBaseRspDTO; }; //banner資訊查詢任務 Callable<BaseRspDTO<Object>> bannerDTOCallableTask = () -> { BannerParam bannerParam = buildBannerParam(req); BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam); BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>(); bannerBaseRspDTO.setKey("bannerDTO"); bannerBaseRspDTO.setData(bannerDTO); return bannerBaseRspDTO; }; //label資訊查詢任務 Callable<BaseRspDTO<Object>> labelDTODTOCallableTask = () -> { LabelParam labelParam = buildLabelParam(req); LabelDTO labelDTO = labelService.queryLabelInfo(labelParam); BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>(); labelBaseRspDTO.setKey("labelDTO"); labelBaseRspDTO.setData(labelDTO); return labelBaseRspDTO; }; //提交使用者資訊任務 baseDTOCompletionService.submit(userInfoDTOCallableTask); //提交banner資訊任務 baseDTOCompletionService.submit(bannerDTOCallableTask); //提交label資訊任務 baseDTOCompletionService.submit(labelDTODTOCallableTask); UserInfoDTO userInfoDTO = null; BannerDTO bannerDTO = null; LabelDTO labelDTO = null; try { //因為提交了3個任務,所以獲取結果次數是3 for (int i = 0; i < 3; i++) { Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(1, TimeUnit.SECONDS); BaseRspDTO baseRspDTO = baseRspDTOFuture.get(); if ("userInfoDTO".equals(baseRspDTO.getKey())) { userInfoDTO = (UserInfoDTO) baseRspDTO.getData(); } else if ("bannerDTO".equals(baseRspDTO.getKey())) { bannerDTO = (BannerDTO) baseRspDTO.getData(); } else if ("labelDTO".equals(baseRspDTO.getKey())) { labelDTO = (LabelDTO) baseRspDTO.getData(); } } } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("結束並行查詢app首頁資訊,總耗時:" + (System.currentTimeMillis() - beginTime)); return buildResponse(userInfoDTO, bannerDTO, labelDTO); }
到這裡為止,一個基於CompletionService
實現並行呼叫的例子已經實現啦。是不是很開心,哈哈。
我們回過來觀察下第2小節,查詢app首頁資訊的demo:CompletionService
實現了並行呼叫。大家有沒有什麼其他想法呢?比如,假設別的業務場景,也想通過並行呼叫優化,那是不是也得搞一套類似第2小節的程式碼。所以,我們是不是可以抽取一個通用的並行方法,讓別的場景也可以用,對吧?這就是後端思維啦!
基於第2小節的程式碼,我們如何抽取通用並行呼叫方法呢。
首先,這個通用的並行呼叫方法,不能跟業務相關的屬性掛鉤,對吧,所以方法的入參應該有哪些呢?
方法的入參,可以有
Callable
對吧。因為並行,肯定是多個Callable任務的。所以,入參應該是一個Callable
的陣列。再然後,基於上面的APP首頁查詢的例子,Callable
裡面得帶BaseRspDTO
泛型,對吧?因此入參就是List<Callable<BaseRspDTO<Object>>> list
。
那並行呼叫的出參呢? 你有多個Callable
的任務,是不是得有多個對應的返回,因此,你的出參可以是List<BaseRspDTO<Object>>
。我們抽取的通用並行呼叫模板,就可以寫成醬紫:
public List<BaseRspDTO<Object>> executeTask(List<Callable<BaseRspDTO<Object>>> taskList) { List<BaseRspDTO<Object>> resultList = new ArrayList<>(); //校驗引數 if (taskList == null || taskList.size() == 0) { return resultList; } ExecutorService executor = Executors.newFixedThreadPool(10); CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor); //提交任務 for (Callable<BaseRspDTO<Object>> task : taskList) { baseDTOCompletionService.submit(task); } try { //遍歷獲取結果 for (int i = 0; i < taskList.size(); i++) { Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(2, TimeUnit.SECONDS); resultList.add(baseRspDTOFuture.get()); } } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return resultList; }
既然我們是抽取通用的並行呼叫方法,那以上的方法是否還有哪些地方需要改進的呢?
executor執行緒池
,比如有些業務場景想用A執行緒池
,有些業務想用B執行緒池
,那麼,這個方法,就不通用啦,對吧。我們可以把執行緒池以引數的實行提供出來,給呼叫方自己控制。CompletionService
的poll
方法獲取時,超時時間是寫死的。因為不同業務場景,超時時間可能不一樣。所以,超時時間也是可以以引數形式放出來,給呼叫方自己控制。我們再次優化一下這個通用的並行呼叫模板,程式碼如下:
public List<BaseRspDTO<Object>> executeTask(List<Callable<BaseRspDTO<Object>>> taskList, long timeOut, ExecutorService executor) { List<BaseRspDTO<Object>> resultList = new ArrayList<>(); //校驗引數 if (taskList == null || taskList.size() == 0) { return resultList; } if (executor == null) { return resultList; } if (timeOut <= 0) { return resultList; } //提交任務 CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor); for (Callable<BaseRspDTO<Object>> task : taskList) { baseDTOCompletionService.submit(task); } try { //遍歷獲取結果 for (int i = 0; i < taskList.size(); i++) { Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(timeOut, TimeUnit.SECONDS); resultList.add(baseRspDTOFuture.get()); } } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return resultList; }
以後別的場景也需要用到並行呼叫的話,直接呼叫你的這個方法即可,是不是有點小小的成就感啦,哈哈。
我們把抽取的那個公用的並行呼叫方法,應用到App首頁資訊查詢
的例子,
程式碼如下:
public AppHeadInfoResponse parallelQueryAppHeadPageInfo1(AppInfoReq req) { long beginTime = System.currentTimeMillis(); System.out.println("開始並行查詢app首頁資訊,開始時間:" + beginTime); //使用者資訊查詢任務 Callable<BaseRspDTO<Object>> userInfoDTOCallableTask = () -> { UserInfoParam userInfoParam = buildUserParam(req); UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam); BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>(); userBaseRspDTO.setKey("userInfoDTO"); userBaseRspDTO.setData(userInfoDTO); return userBaseRspDTO; }; //banner資訊查詢任務 Callable<BaseRspDTO<Object>> bannerDTOCallableTask = () -> { BannerParam bannerParam = buildBannerParam(req); BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam); BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>(); bannerBaseRspDTO.setKey("bannerDTO"); bannerBaseRspDTO.setData(bannerDTO); return bannerBaseRspDTO; }; //label資訊查詢任務 Callable<BaseRspDTO<Object>> labelDTODTOCallableTask = () -> { LabelParam labelParam = buildLabelParam(req); LabelDTO labelDTO = labelService.queryLabelInfo(labelParam); BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>(); labelBaseRspDTO.setKey("labelDTO"); labelBaseRspDTO.setData(labelDTO); return labelBaseRspDTO; }; List<Callable<BaseRspDTO<Object>>> taskList = new ArrayList<>(); taskList.add(userInfoDTOCallableTask); taskList.add(bannerDTOCallableTask); taskList.add(labelDTODTOCallableTask); ExecutorService executor = Executors.newFixedThreadPool(10); List<BaseRspDTO<Object>> resultList = parallelInvokeCommonService.executeTask(taskList, 3, executor); if (resultList == null || resultList.size() == 0) { return new AppHeadInfoResponse(); } UserInfoDTO userInfoDTO = null; BannerDTO bannerDTO = null; LabelDTO labelDTO = null; //遍歷結果 for (int i = 0; i < resultList.size(); i++) { BaseRspDTO baseRspDTO = resultList.get(i); if ("userInfoDTO".equals(baseRspDTO.getKey())) { userInfoDTO = (UserInfoDTO) baseRspDTO.getData(); } else if ("bannerDTO".equals(baseRspDTO.getKey())) { bannerDTO = (BannerDTO) baseRspDTO.getData(); } else if ("labelDTO".equals(baseRspDTO.getKey())) { labelDTO = (LabelDTO) baseRspDTO.getData(); } } System.out.println("結束並行查詢app首頁資訊,總耗時:" + (System.currentTimeMillis() - beginTime)); return buildResponse(userInfoDTO, bannerDTO, labelDTO); }
基於以上程式碼,小夥伴們,是否還有其他方面的優化想法呢? 比如這幾個Callable
查詢任務,我們是不是也可以抽取一下?讓程式碼更加簡潔。
二話不說,現在我們直接建一個BaseTaskCommand
類,實現Callable
介面,把查詢使用者資訊、查詢banner資訊、label標籤資訊的查詢任務放進去。
程式碼如下:
public class BaseTaskCommand implements Callable<BaseRspDTO<Object>> { private String key; private AppInfoReq req; private IUserService userService; private IBannerService bannerService; private ILabelService labelService; public BaseTaskCommand(String key, AppInfoReq req, IUserService userService, IBannerService bannerService, ILabelService labelService) { this.key = key; this.req = req; this.userService = userService; this.bannerService = bannerService; this.labelService = labelService; } @Override public BaseRspDTO<Object> call() throws Exception { if ("userInfoDTO".equals(key)) { UserInfoParam userInfoParam = buildUserParam(req); UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam); BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>(); userBaseRspDTO.setKey("userInfoDTO"); userBaseRspDTO.setData(userInfoDTO); return userBaseRspDTO; } else if ("bannerDTO".equals(key)) { BannerParam bannerParam = buildBannerParam(req); BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam); BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>(); bannerBaseRspDTO.setKey("bannerDTO"); bannerBaseRspDTO.setData(bannerDTO); return bannerBaseRspDTO; } else if ("labelDTO".equals(key)) { LabelParam labelParam = buildLabelParam(req); LabelDTO labelDTO = labelService.queryLabelInfo(labelParam); BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>(); labelBaseRspDTO.setKey("labelDTO"); labelBaseRspDTO.setData(labelDTO); return labelBaseRspDTO; } return null; } private UserInfoParam buildUserParam(AppInfoReq req) { return new UserInfoParam(); } private BannerParam buildBannerParam(AppInfoReq req) { return new BannerParam(); } private LabelParam buildLabelParam(AppInfoReq req) { return new LabelParam(); } }
以上這塊程式碼,建構函式還是有比較多的引數,並且call()
方法中,有多個if...else...
,如果新增一個分支(比如查詢浮層資訊),那又得在call
方法裡修改了,並且BaseTaskCommand的構造器也要修改了。
大家是否有印象,多程式中出現多個if...else...時,我們就可以考慮使用策略模式+工廠模式優化。
我們宣告多個策略實現類,如下:
public interface IBaseTask { //返回每個策略類的key,如 String getTaskType(); BaseRspDTO<Object> execute(AppInfoReq req); } //使用者資訊策略類 @Service public class UserInfoStrategyTask implements IBaseTask { @Autowired private IUserService userService; @Override public String getTaskType() { return "userInfoDTO"; } @Override public BaseRspDTO<Object> execute(AppInfoReq req) { UserInfoParam userInfoParam = userService.buildUserParam(req); UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam); BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>(); userBaseRspDTO.setKey(getTaskType()); userBaseRspDTO.setData(userBaseRspDTO); return userBaseRspDTO; } } /** * banner資訊策略實現類 **/ @Service public class BannerStrategyTask implements IBaseTask { @Autowired private IBannerService bannerService; @Override public String getTaskType() { return "bannerDTO"; } @Override public BaseRspDTO<Object> execute(AppInfoReq req) { BannerParam bannerParam = bannerService.buildBannerParam(req); BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam); BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>(); bannerBaseRspDTO.setKey(getTaskType()); bannerBaseRspDTO.setData(bannerDTO); return bannerBaseRspDTO; } } ...
然後這幾個策略實現類,怎麼交給spring
管理呢? 我們可以實現ApplicationContextAware
介面,把策略的實現類注入到一個map,然後根據請求方不同的策略請求型別(即DTO的型別),去實現不同的策略類呼叫。其實這類似於工廠模式的思想。
程式碼如下:
/** * 策略工廠類 **/ @Component public class TaskStrategyFactory implements ApplicationContextAware { private Map<String, IBaseTask> map = new ConcurrentHashMap<>(); @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map<String, IBaseTask> tempMap = applicationContext.getBeansOfType(IBaseTask.class); tempMap.values().forEach(iBaseTask -> { map.put(iBaseTask.getTaskType(), iBaseTask); }); } public BaseRspDTO<Object> executeTask(String key, AppInfoReq req) { IBaseTask baseTask = map.get(key); if (baseTask != null) { System.out.println("工廠策略實現類執行"); return baseTask.execute(req); } return null; } }
有了策略工廠類TaskStrategyFactory
,我們再回來優化下BaseTaskCommand
類的程式碼。它的構造器已經不需要多個IUserService userService, IBannerService bannerService, ILabelService labelService
啦,只需要策略工廠類TaskStrategyFactory
即可。同時策略也不需要多個if...else...
判斷了,用策略工廠類TaskStrategyFactory
代替即可。
優化後的程式碼如下:
public class BaseTaskCommand implements Callable<BaseRspDTO<Object>> { private String key; private AppInfoReq req; private TaskStrategyFactory taskStrategyFactory; public BaseTaskCommand(String key, AppInfoReq req, TaskStrategyFactory taskStrategyFactory) { this.key = key; this.req = req; this.taskStrategyFactory = taskStrategyFactory; } @Override public BaseRspDTO<Object> call() throws Exception { return taskStrategyFactory.executeTask(key, req); } }
因此整個app首頁資訊並行
查詢,就可以優化成這樣啦,如下:
public AppHeadInfoResponse parallelQueryAppHeadPageInfo2(AppInfoReq req) { long beginTime = System.currentTimeMillis(); System.out.println("開始並行查詢app首頁資訊(最終版本),開始時間:" + beginTime); List<Callable<BaseRspDTO<Object>>> taskList = new ArrayList<>(); //使用者資訊查詢任務 taskList.add(new BaseTaskCommand("userInfoDTO", req, taskStrategyFactory)); //banner查詢任務 taskList.add(new BaseTaskCommand("bannerDTO", req, taskStrategyFactory)); //標籤查詢任務 taskList.add(new BaseTaskCommand("labelDTO", req, taskStrategyFactory)); ExecutorService executor = Executors.newFixedThreadPool(10); List<BaseRspDTO<Object>> resultList = parallelInvokeCommonService.executeTask(taskList, 3, executor); if (resultList == null || resultList.size() == 0) { return new AppHeadInfoResponse(); } UserInfoDTO userInfoDTO = null; BannerDTO bannerDTO = null; LabelDTO labelDTO = null; for (BaseRspDTO<Object> baseRspDTO : resultList) { if ("userInfoDTO".equals(baseRspDTO.getKey())) { userInfoDTO = (UserInfoDTO) baseRspDTO.getData(); } else if ("bannerDTO".equals(baseRspDTO.getKey())) { bannerDTO = (BannerDTO) baseRspDTO.getData(); } else if ("labelDTO".equals(baseRspDTO.getKey())) { labelDTO = (LabelDTO) baseRspDTO.getData(); } } System.out.println("結束並行查詢app首頁資訊(最終版本),總耗時:" + (System.currentTimeMillis() - beginTime)); return buildResponse(userInfoDTO, bannerDTO, labelDTO); }
以上程式碼整體優化下來,已經很簡潔啦。那還有沒有別的優化思路呢。
其實還是有的,比如,把唯一標記的key
定義為列舉,而不是寫死的字串"userInfoDTO"、"bannerDTO","labelDTO"
。還有,除了CompletionService
,有些小夥伴喜歡用CompletableFuture
實行並行呼叫。
本文大家學到了哪些知識呢?
CompletionService
。到此這篇關於利用Java程式碼寫一個並行呼叫模板的文章就介紹到這了,更多相關Java並行呼叫模板內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45