iOS 单元测试和用户界面测试 (1)
学习重点
单元&UI测试的意义
单元&UI测试执行流程探究及优化
单元&UI测试原理初探
1. 单元&UI测试
1.1 什么是单元测试
单元测试是检查每个代码单元(例如类或函数)是否能产生预期的结果,单元测试是独立运行的,不依赖于其他模块或组件。
1.2 什么是UI测试
UI
测试是属于端到端的测试,是从应用程序启动到结束的测试过程,完全按照用户与应用程序交互的方式来复制与应用程序的交互,比单元测试慢的多,运行起来也更消耗资源。
1.3 需要进行测试的内容
&emsp测试应涵盖以下的内容:
-
核心功能:模型类和方法及其与控制器的交互
-
UI
工作流程 -
特殊的边界条件
-
Bug处理
1.4 测试原则(FIRST)
-
Fast
:测试模块应该是快速高效的 -
Independent/Isolated
:测试模块应该是独立、相互不影响的 -
Repeatable
:测试实例应该是可以重复使用的,测试结果应该是相同的 -
Self-validating
:测试应完全自动化。输出结果要么是“成功”,要么是“失败” -
Timely
:理想情况下,应该在编写要测试的生产代码之前编写测试(测试驱动开发)
2. 单元&UI测试执行流程探究
首先,使用Xcode
创建一个iOS
工程,勾选上Include Tests
选项,如下图所示:
在测试代码运行之前,先来抛出几个问题?
-
- 测试代码运行之前需不需要启动
APP
?
- 测试代码运行之前需不需要启动
-
- 需不需要调用
AppDelegate
中的didFinishLaunchingWithOptions
方法?
- 需不需要调用
为了验证这两个问题的确切答案,在测试工程打上如下的几个断点,如下图所示:
接着运行测试工程中TestAppDemoTests.m
文件中的testExample
方法,如下图所示:
首先,可以看到,App
在模拟器中被启动了,并且程序执行到了main.m
文件中设置的断点处,如下图所示:
过掉断点,程序执行到了AppDelegate.m
文件中设置的断点处,如下图所示:
过掉断点,打印了在测试方法中输出的日志信息,如下图所示:
其中:
-
红框1
:中的时间表示的是测试方法执行的时间 -
红框2
:表示的是测试方法的名字 -
红框3
:表示测试通过并且耗时0.001
秒。
根据以上的运行结果,可以很清楚的看到程序执行测试方法的时候是会启动APP
并且会调用执行didFinishLaunchingWithOptions
方法的,在一些大型项目中,通常会在didFinishLaunchingWithOptions
方法中执行一些耗时的方法,那么这样就不能快速进行测试了,为了解决这个问题,可以选择创建一个FakeAppDelegate
,只要在测试的时候在main
函数中返回这个FakeAppDelegate
对象就可以了,代码如下所示:
//FakeAppDelegate.h文件中代码
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FakeAppDelegate : UIResponder <UIApplicationDelegate>
@end
NS_ASSUME_NONNULL_END
//FakeAppDelegate.m文件中代码
#import "FakeAppDelegate.h"
@interface FakeAppDelegate ()
@end
@implementation FakeAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions {
return YES;
}
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
}
@end
//main.m文件中代码
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "FakeAppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
BOOL isShouldReturnFakeAppDelegate = NO;
appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
你也许会注意到isShouldReturnFakeAppDelegate
这个局部变量的值为NO
,这样的话不就没什么作用了吗?这也正是我想抛出的问题,应该如何设置isShouldReturnFakeAppDelegate
的值呢?在运行的过程中,如何知道此时是测试方法的调用还是实际APP
的运行呢?读者不妨来思考一下这个问题,我会在接下来的讨论中分享两种解决方案。
3. 单元&UI测试执行流程优化
3.1 OC工程执行流程优化
3.1.1 使用runtime API
进行判断
首先你应该注意到的是测试工程中类的继承顺序为:TestAppDemoTests
-->XCTestCase
-->XCTest
,那么可以从这里入手,判断XCTest
这个类是否存在就可以了,在main.m
文件中编写如下的代码:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "FakeAppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
BOOL isShouldReturnFakeAppDelegate = NSClassFromString(@"XCTest") != nil;
appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
然后分别在AppDelegate.m
文件以及FakeAppDelegate.m
文件中打印如下日志信息:
command+r
运行App
,输出日志信息如下图所示:
运行TestAppDemoTests.m
文件中的testExample
方法,输出日志信息如下图所示:
3.1.2 使用环境变量进行判断
在工程Scheme
下的Test
中的Debug
模式下添加环境变量IS_TESTING
,如下图所示:
然后在main.m
文件中编写如下代码,获取环境变量IS_TESTING
的值
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "FakeAppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
BOOL isShouldReturnFakeAppDelegate = [[NSProcessInfo processInfo].environment[@"IS_TESTING"] boolValue];;
appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
command+r
运行App
,输出日志信息如下图所示:
运行TestAppDemoTests.m
文件中的testExample
方法,输出日志信息如下图所示:
3.2 Swift
工程执行流程优化
- 创建一个简单的
Swift
工程TestSwiftDemo
,如下图所示:
- 会发现在
Swift
工程中并没有main.swift
文件
- 创建
main.swift
文件并在这个文件中编写如下代码:
import UIKit
var appDelegateClsName = NSStringFromClass(AppDelegate.self)
//两种判断方式任选其一
//1.根据XCTest是否存在判断是否正在执行测试方法
if NSClassFromString("XCTest") != nil {
appDelegateClsName = NSStringFromClass(FakeAppDelegate.self)
}
//2.根据环境变量判断是否正在执行测试方法
if ProcessInfo.processInfo.environment["IS_TESTING"] == "true" {
appDelegateClsName = NSStringFromClass(FakeAppDelegate.self)
}
let argv = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<CChar>.self, capacity: Int(CommandLine.argc))
_ = UIApplicationMain(CommandLine.argc, argv, nil, appDelegateClsName)
- 在
Test
中配置如下图所示环境变量
- 运行结果如下图所示:
4. 单元&UI测试原理初探
在使用command + u
运行测试工程之后,按照如下图所示的方式打开编译之后产生的应用程序,会发现会多出AutoTestingDemoUITests-Runner
这个应用程序。
而模拟器中也会安装这个应用程序,如下图所示:
进入到AutoTestingDemo
这个APP
包中,查看其Frameworks
中的文件,发现多了以下几个文件:
也就是说,是不是在一个ipa
包中只要包含了这几个动态库,就可以在项目中运行测试代码了呢?而XCTest.framework
又是从哪里拷贝进来的呢?其实是从Xcode
中的以下路径中获取的:
-
XCTest.framework
(模拟器设备):/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework
-
XCTest.framework
(真机设备):/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework
只要在工程中添加了XCTest.framework
,就可以不创建测试target
也能在工程中进行代码测试了,首先创建一个xcconfig
文件,如下图所示:
接着在这个xcconfig
文件中按照如下的方式进行配置:
//1.设置动态库头文件路径
HEADER_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework/Headers"
//2.链接动态库
//2.1传统方式
OTHER_LDFLAGS = $(inherited) -F "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" -framework "XCTest"
//2.2
//3.配置rpath 解决 崩溃Reason: image not found
LD_RUNPATH_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks"
然后主工程引用这个xcconfig
文件,如下图所示:
&emsp创建一个AppTests类,其代码如下所示:
//AppTests.h文件中的代码
#import <XCTest/XCTest.h>
NS_ASSUME_NONNULL_BEGIN
@interface AppTests : XCTestCase
- (void)testExample1;
- (void)testExample2;
@end
NS_ASSUME_NONNULL_END
//AppTests.m文件中的代码
#import "AppTests.h"
@implementation AppTests
- (void)testExample1 {
NSLog(@"-------testExample1-------");
}
- (void)testExample2 {
NSLog(@"-------testExample2-------");
}
@end
然后在ViewController.m文件中编写如下所示的代码:
#import "ViewController.h"
#import <XCTest/XCTest.h>
#import "AppTests.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//管理者 XCTestSuite,用来管理测试用例
XCTestSuite *suite = [XCTestSuite defaultTestSuite];
//测试用例
AppTests *testCase = [AppTests testCaseWithSelector:@selector(testExample1)];
[suite addTest:testCase];
//遍历其中所有的测试用例,调用其所有的测试方法
for (XCTest *test in suite.tests) {
[test runTest];
}
}
@end
接着运行程序,然后点击屏幕,日志输出如下图所示:
但以上代码有个问题,当再次点击屏幕的时候,应用程序会直接奔溃,如下图所示:
如果想要多次运行测试用例,可以通过以下的方式进行调用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//管理者 XCTestSuite,用来管理测试用例
XCTestSuite *suite = [XCTestSuite testSuiteForTestCaseClass:AppTests.class];
//测试用例
AppTests *testCase = [AppTests new];
[suite addTest:testCase];
//遍历其中所有的测试用例,调用其所有的测试方法
for (XCTest *test in suite.tests) {
[test runTest];
}
}
运行程序,点击屏幕,控制台输出信息如下图所示:
未完待续...
推荐阅读
-
微信 "扫一扫 "物联网,全面揭秘 "扫一扫 "背后的扫盲技术!-1.1 扫一扫感知物体是做什么的? 1.1 微信扫一扫是做什么的? 扫一扫识物是指以图片或视频(商品图片:鞋/包/美妆/服饰/家电/玩具/图书/食品/珠宝/家具/其他商品)为输入媒介,挖掘微信内容生态中的有价值信息(电商+百科+资讯,如图1所示),并展示给用户。这里的电商基本涵盖了微信小程序覆盖上亿SKU的全量优质电商,可以支持用户货比N家并直接下单购买,百科和资讯则聚合了微信内的头部自媒体如搜狗、搜搜、百度等,向用户展示和分享拍摄商品相关的内容资讯。 图 1 扫一扫识别功能示意图 欢迎大家更新iOS新版微信→扫一扫→识货,亲自体验,也欢迎大家通过识货界面的反馈按钮向我们提交反馈意见。 扫一扫识物实景图展示 1.2 扫一扫识物有哪些使用场景? 扫一扫识物的目的是为用户访问微信内部生态内容开辟一个新窗口,以用户扫图片为输入形式,为用户提供微信生态内容中的百科、资讯、电商等作为展示页面。除了用户熟悉的扫一扫操作外,我们还将进一步拓展长按操作,让用户更方便地进行扫一扫操作。"扫一扫知事 "的落地场景主要涵盖三大部分: a. 科普知识: a.科普知识。用户通过扫一扫,可以在微信生态圈中获取该对象的百科、资讯等常识或趣闻,帮助用户更好地了解该对象; b.购物场景。同样的搜索功能支持用户看到喜欢的商品立即检索到微信小程序电商中的同款商品,支持用户即扫即购; c.广告场景。扫一扫识别物体可以辅助公众号文章、视频更好地理解其中蕴含的图片信息,从而更好地投放匹配广告,提高点击率。 1.3 Sweep Sense 为 Sweep 家族带来了哪些新技术? 对于扫一扫来说,大家耳熟能详的应该就是扫一扫二维码、扫一扫小程序码、扫一扫条形码、扫一扫翻译了。无论是各种形式的编码还是文字字符,都可以看作是图片的一种特定编码形式,而物的识别则是对自然场景图片的识别,这对于扫一扫家族来说是一个质的飞跃,我们希望从物的识别入手,进一步拓展扫一扫对自然场景图片的理解能力,比如扫酒、扫车、扫植物、扫人脸等服务,如下图3所示。 图 3 Sweep 家族
-
基于 POSTMAN 的自动界面测试流程和方法(单元测试)
-
iOS 单元测试和用户界面测试 (1)
-
什么是可用性测试?有效性(Effectiveness)-- 用户完成特定任务和实现特定目标的正确性和完整性程度;效率(Efficiency)-- 用户完成任务的正确性和完整性程度与所用资源(如时间)之比;满意度(Satisfaction)-- 用户在使用产品时的主观满意度和接受程度。 2.如何获得可用性? 可以参考以下原则:Gould、Boies 和 Lewis(1991 年)为以用户为中心的设计定义了 4 个重要原则: 早期以用户为中心:设计者应在设计过程的早期就努力了解用户的需求。 综合设计:设计的所有方面都应同步发展,而不是按顺序进行。使产品的内部设计始终与用户界面的需求保持一致。 早期和持续测试:当今唯一可行的软件测试方法是经验主义方法,即如果实际用户认为设计可行,该设计就是可行的。通过在整个开发过程中引入可用性测试,用户就有机会在产品推出之前对设计提出反馈意见。 迭代设计:大问题往往掩盖了小问题的存在。设计人员和开发人员应在整个测试过程中对设计进行迭代。 3...什么是可用性测试? 可用性测试是根据可用性标准对图形用户界面进行的系统评估。 可用性测试是衡量用户与系统(网站、软件应用程序、移动技术或任何用户操作设备)交互时的体验质量。4.如何进行可用性测试? l 实验室实验
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为:
-
Jmeter 多服务混合场景设置并发率(非图形用户界面模式) - 1. 编写测试脚本
-
Android 开发中 nodpi、xhdpi、hdpi、mdpi、ldpi 的概念 - 术语和概念 屏幕尺寸 屏幕的物理尺寸,基于屏幕的对角线长度(如 2.8 英寸、3.5 英寸)。 简而言之,安卓系统将所有屏幕尺寸简化为三大类:大、普通和小。 程序可以为这三种屏幕尺寸提供三种不同的布局选项,然后系统会以合适的方式将布局选项呈现到相应的屏幕上,这个过程不需要程序员用代码进行干预。 屏幕纵横比 屏幕的物理长度与物理宽度之比。程序只需使用系统提供的资源分类器 long(长)和 notlong(不长),就能为具有特定长宽比的屏幕提供配制材料。 分辨率 屏幕的像素总数。请注意,分辨率并不意味着长宽比,尽管在大多数情况下,分辨率表示为 "宽度 x 长度"。在安卓系统中,程序一般不直接处理分辨率。 密度 根据屏幕分辨率,沿屏幕宽度和长度排列的像素数量。 密度较低的屏幕在长度和宽度方向上的像素都相对较少,而密度较高的屏幕通常会在同一区域内排列很多甚至非常非常多的像素。屏幕的密度非常重要;例如,一个界面元素(如按钮)的长度和宽度以像素为单位,在低密度屏幕上会显得很大,但在高密度屏幕上就会显得很小。 独立于密度的像素(DIP)是指程序用来定义界面元素的抽象意义上的像素。它作为一个与实际密度无关的单位,帮助程序员构建布局方案(界面元素的宽度、高度和位置)。 与密度无关的像素在逻辑上与像素密度为 160 DPI 的屏幕上的像素大小相同,而 160 DPI 是安卓平台默认的显示设备。在运行时,平台会以目标屏幕的密度为基准,"透明 "地处理所有所需的 DIP 缩放操作。要将与密度无关的像素转换为屏幕像素,可以使用一个简单的公式:像素 = DIP * (密度 / 160)。例如,在 240 DPI 的屏幕上,1 个 DIP 等于 1.5 个物理像素。强烈建议使用 DIP 来定义程序界面的布局,因为这样可以确保用户界面在所有分辨率的屏幕上都能正常显示。 为了简化程序员在面对各种分辨率时的麻烦,也为了让各种分辨率的平台都能直接运行这些程序,Android 平台将所有屏幕以密度和分辨率作为分类方式,分别分为三类:- 三大尺寸:大、普通、小;- 三种不同密度:高(hdpi)、中(mdpi)和低(ldpi)。DPI 表示 "每英寸点数",即每英寸的像素数。如果需要,程序可以为不同的屏幕尺寸提供不同的资源(主要是布局),为不同的屏幕密度提供不同的资源(主要是位图)。除此之外,程序无需对屏幕尺寸或密度进行任何额外处理。执行时,平台会根据屏幕本身的尺寸和密度特性自动加载相应的资源,并将其从逻辑像素(DIP,用于定义界面布局)转换为屏幕上的物理像素。
-
探探应用实战解析:专为寻找附近异性与兴趣相投者设计" 用户需求与烦恼: 1. 身边异性朋友寥寥,扩大交友圈子无从下手 2. 初来乍到新城市,渴望交到本地朋友或异性知己 3. 遇到心仪对象,想提前了解彼此是否志趣相投 4. 对心动异性不敢主动搭讪,害怕被拒场面尴尬 产品界面: - a: 设置选项 - b: 主页 - c: 配对成功的通知 探探的核心功能: - 基于位置和兴趣标签,推荐附近的高匹配度陌生人,显示真人照片、基本信息(如昵称、年龄、职业、距离)、共同点(如通讯录好友、兴趣爱好)。 - 双方需同时向右滑动表示“喜欢”才能配对成功,可进行文字、表情、真心话问答、图片/视频等多种交流方式。 产品特点与独特之处: - 陌生人间的选择操作直观易懂,左滑忽略、右滑喜欢。 - 系统不会记录“喜欢”历史,仅当双方都喜欢才会成功配对并展开后续交流。 - 每个陌生人只会展示一次,确保每一次抉择都有决定性意义。 - 提供屏蔽手机联系人的功能,满足用户既想脱单又不愿让熟人知晓的需求,符合探探专注陌生人社交平台定位。 商业模式探讨: