HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

通过Flink实现个推海量消息数据的实时统计

背景

消息报表主要用于统计消息任务的下发情况。比如,单条推送消息下发APP用户总量有多少,成功推送到手机的数量有多少,又有多少APP用户点击了弹窗通知并打开APP等。通过消息报表,我们可以很直观地看到消息推送的流转情况、消息下发到达成功率、用户对消息的点击情况等。

个推在提供消息推送服务时,为了更好地了解每天的推送情况,会从不同的维度进行数据统计,生成消息报表。个推每天下发的消息推送数巨大,可以达到数百亿级别,原本我们采用的离线统计系统已不能满足业务需求。随着业务能力的不断提升,我们选择了Flink作为数据处理引擎,以满足对海量消息推送数据的实时统计。

本文将主要阐述选择Flink的原因、Flink的重要特性以及优化后的实时计算方法。

离线计算平台架构

在消息报表系统的初期,我们采用的是离线计算的方式,主要采用spark作为计算引擎,原始数据存放在HDFS中,聚合数据存放在Solr、Hbase和Mysql中:

查询的时候,先根据筛选条件,查询的维度主要有三个:

  1. appId
  2. 下发时间
  3. taskGroupName

根据不同维度可以查询到taskId的列表,然后根据task查询hbase获取相应的结果,获取下发、展示和点击相应的指标数据。在我们考虑将其改造为实时统计时,会存在着一系列的难点:

  1. 原始数据体量巨大,每天数据量达到几百亿规模,需要支持高吞吐量;
  2. 需要支持实时的查询;
  3. 需要对多份数据进行关联;
  4. 需要保证数据的完整性和数据的准确性。

Why Flink

Flink是什么

Flink 是一个针对流数据和批数据的分布式处理引擎。它主要是由 Java 代码实现。目前主要还是依靠开源社区的贡献而发展。

对 Flink 而言,其所要处理的主要场景就是流数据。Flink 的前身是柏林理工大学一个研究性项目, 在 2014 被 Apache 孵化器所接受,然后迅速地成为了 ASF(Apache Software Foundation)的顶级项目之一。

方案对比

为了实现个推消息报表的实时统计,我们之前考虑使用spark streaming作为我们的实时计算引擎,但是我们在考虑了spark streaming、storm和flink的一些差异点后,还是决定使用Flink作为计算引擎:

针对上面的业务痛点,Flink能够满足以下需要:

  1. Flink以管道推送数据的方式,可以让Flink实现高吞吐量。

  2. Flink是真正意义上的流式处理,延时更低,能够满足我们消息报表统计的实时性要求。

  3. Flink可以依靠强大的窗口功能,实现数据的增量聚合;同时,可以在窗口内进行数据的join操作。

  4. 我们的消息报表涉及到金额结算,因此对于不允许存在误差,Flink依赖自身的exact once机制,保证了我们数据不会重复消费和漏消费。

Flink的重要特性

下面我们来具体说说Flink中一些重要的特性,以及实现它的原理:

1)低延时、高吞吐

Flink速度之所以这么快,主要是在于它的流处理模型。

Flink 采用 Dataflow 模型,和 Lambda 模式不同。Dataflow 是纯粹的节点组成的一个图,图中的节点可以执行批计算,也可以是流计算,也可以是机器学习算法。流数据在节点之间流动,被节点上的处理函数实时 apply 处理,节点之间是用 netty 连接起来,两个 netty 之间 keepalive,网络 buffer 是自然反压的关键。

经过逻辑优化和物理优化,Dataflow 的逻辑关系和运行时的物理拓扑相差不大。这是纯粹的流式设计,时延和吞吐理论上是最优的。

简单来说,当一条数据被处理完成后,序列化到缓存中,然后立刻通过网络传输到下一个节点,由下一个节点继续处理。

2)Checkpoint

Flink是通过分布式快照来实现checkpoint,能够支持Exactly-Once语义。

分布式快照是基于Chandy和Lamport在1985年设计的一种算法,用于生成分布式系统当前状态的一致性快照,不会丢失信息且不会记录重复项。

Flink使用的是Chandy Lamport算法的一个变种,定期生成正在运行的流拓扑的状态快照,并将这些快照存储到持久存储中(例如:存储到HDFS或内存中文件系统)。检查点的存储频率是可配置的。

3)backpressure

back pressure出现的原因是为了应对短期数据尖峰。

旧版本Spark Streaming的back pressure通过限制最大消费速度实现,对于基于Receiver 形式,我们可以通过配置spark.streaming. receiver.maxRate参数来限制每个 receiver 每秒最大可以接收的记录的数据。

对于 Direct Approach 的数据接收,我们可以通过配置spark.streaming. kafka.maxRatePerPartition 参数来限制每次作业中每个 Kafka 分区最多读取的记录条数。

但这样是非常不方便的,在实际上线前,还需要对集群进行压测,来决定参数的大小。

Flink运行时的构造部件是operators以及streams。每一个operator消费一个中间/过渡状态的流,对它们进行转换,然后生产一个新的流。

描述这种机制最好的类比是:Flink使用有效的分布式阻塞队列来作为有界的缓冲区。如同Java里通用的阻塞队列跟处理线程进行连接一样,一旦队列达到容量上限,一个相对较慢的接受者将拖慢发送者。

消息报表的实时计算

优化之后,架构升级成如下:

可以看出,我们做了以下几点优化:

  1. Flink替换了之前的spark,进行消息报表的实时计算;
  2. ES替换了之前的Solr。

对于Flink进行实时计算,我们的关注点主要有以下4个方面:

  1. ExactlyOnce保证了数据只会被消费一次
  2. 状态管理的能力
  3. 强大的时间窗口
  4. 流批一体

为了实现我们实时统计报表的需求,主要依靠Flink的增量聚合功能。

首先,我们设置了Event Time作为时间窗口的类型,保证了只会计算当天的数据;同时,我们每隔一分钟增量统计当日的消息报表,因此分配1分钟的时间窗口。

然后我们使用.aggregate (AggregateFunction af, WindowFunction wf) 做增量的聚合操作,它能使用AggregateFunction提前聚合掉数据,减少 state 的存储压力。之后,我们将增量聚合后的数据写入到ES和Hbase中。

流程如下所示:

同时,在查询的时候,我们通过taskID、日期等维度进行查询,先从ES中获取taskID的集合,之后通过taskID查询hbase,得出统计结果。

总结

通过使用Flink,我们实现了对消息推送数据的实时统计,能够实时查看消息下发、展示、点击等数据指标,同时,借助FLink强大的状态管理功能,服务的稳定性也得到了一定的保障。未来,个推也将持续优化消息推送服务,并将Flink引入到其他的业务线中,以满足一些实时性要求高的业务场景需求。

继续阅读 »

背景

消息报表主要用于统计消息任务的下发情况。比如,单条推送消息下发APP用户总量有多少,成功推送到手机的数量有多少,又有多少APP用户点击了弹窗通知并打开APP等。通过消息报表,我们可以很直观地看到消息推送的流转情况、消息下发到达成功率、用户对消息的点击情况等。

个推在提供消息推送服务时,为了更好地了解每天的推送情况,会从不同的维度进行数据统计,生成消息报表。个推每天下发的消息推送数巨大,可以达到数百亿级别,原本我们采用的离线统计系统已不能满足业务需求。随着业务能力的不断提升,我们选择了Flink作为数据处理引擎,以满足对海量消息推送数据的实时统计。

本文将主要阐述选择Flink的原因、Flink的重要特性以及优化后的实时计算方法。

离线计算平台架构

在消息报表系统的初期,我们采用的是离线计算的方式,主要采用spark作为计算引擎,原始数据存放在HDFS中,聚合数据存放在Solr、Hbase和Mysql中:

查询的时候,先根据筛选条件,查询的维度主要有三个:

  1. appId
  2. 下发时间
  3. taskGroupName

根据不同维度可以查询到taskId的列表,然后根据task查询hbase获取相应的结果,获取下发、展示和点击相应的指标数据。在我们考虑将其改造为实时统计时,会存在着一系列的难点:

  1. 原始数据体量巨大,每天数据量达到几百亿规模,需要支持高吞吐量;
  2. 需要支持实时的查询;
  3. 需要对多份数据进行关联;
  4. 需要保证数据的完整性和数据的准确性。

Why Flink

Flink是什么

Flink 是一个针对流数据和批数据的分布式处理引擎。它主要是由 Java 代码实现。目前主要还是依靠开源社区的贡献而发展。

对 Flink 而言,其所要处理的主要场景就是流数据。Flink 的前身是柏林理工大学一个研究性项目, 在 2014 被 Apache 孵化器所接受,然后迅速地成为了 ASF(Apache Software Foundation)的顶级项目之一。

方案对比

为了实现个推消息报表的实时统计,我们之前考虑使用spark streaming作为我们的实时计算引擎,但是我们在考虑了spark streaming、storm和flink的一些差异点后,还是决定使用Flink作为计算引擎:

针对上面的业务痛点,Flink能够满足以下需要:

  1. Flink以管道推送数据的方式,可以让Flink实现高吞吐量。

  2. Flink是真正意义上的流式处理,延时更低,能够满足我们消息报表统计的实时性要求。

  3. Flink可以依靠强大的窗口功能,实现数据的增量聚合;同时,可以在窗口内进行数据的join操作。

  4. 我们的消息报表涉及到金额结算,因此对于不允许存在误差,Flink依赖自身的exact once机制,保证了我们数据不会重复消费和漏消费。

Flink的重要特性

下面我们来具体说说Flink中一些重要的特性,以及实现它的原理:

1)低延时、高吞吐

Flink速度之所以这么快,主要是在于它的流处理模型。

Flink 采用 Dataflow 模型,和 Lambda 模式不同。Dataflow 是纯粹的节点组成的一个图,图中的节点可以执行批计算,也可以是流计算,也可以是机器学习算法。流数据在节点之间流动,被节点上的处理函数实时 apply 处理,节点之间是用 netty 连接起来,两个 netty 之间 keepalive,网络 buffer 是自然反压的关键。

经过逻辑优化和物理优化,Dataflow 的逻辑关系和运行时的物理拓扑相差不大。这是纯粹的流式设计,时延和吞吐理论上是最优的。

简单来说,当一条数据被处理完成后,序列化到缓存中,然后立刻通过网络传输到下一个节点,由下一个节点继续处理。

2)Checkpoint

Flink是通过分布式快照来实现checkpoint,能够支持Exactly-Once语义。

分布式快照是基于Chandy和Lamport在1985年设计的一种算法,用于生成分布式系统当前状态的一致性快照,不会丢失信息且不会记录重复项。

Flink使用的是Chandy Lamport算法的一个变种,定期生成正在运行的流拓扑的状态快照,并将这些快照存储到持久存储中(例如:存储到HDFS或内存中文件系统)。检查点的存储频率是可配置的。

3)backpressure

back pressure出现的原因是为了应对短期数据尖峰。

旧版本Spark Streaming的back pressure通过限制最大消费速度实现,对于基于Receiver 形式,我们可以通过配置spark.streaming. receiver.maxRate参数来限制每个 receiver 每秒最大可以接收的记录的数据。

对于 Direct Approach 的数据接收,我们可以通过配置spark.streaming. kafka.maxRatePerPartition 参数来限制每次作业中每个 Kafka 分区最多读取的记录条数。

但这样是非常不方便的,在实际上线前,还需要对集群进行压测,来决定参数的大小。

Flink运行时的构造部件是operators以及streams。每一个operator消费一个中间/过渡状态的流,对它们进行转换,然后生产一个新的流。

描述这种机制最好的类比是:Flink使用有效的分布式阻塞队列来作为有界的缓冲区。如同Java里通用的阻塞队列跟处理线程进行连接一样,一旦队列达到容量上限,一个相对较慢的接受者将拖慢发送者。

消息报表的实时计算

优化之后,架构升级成如下:

可以看出,我们做了以下几点优化:

  1. Flink替换了之前的spark,进行消息报表的实时计算;
  2. ES替换了之前的Solr。

对于Flink进行实时计算,我们的关注点主要有以下4个方面:

  1. ExactlyOnce保证了数据只会被消费一次
  2. 状态管理的能力
  3. 强大的时间窗口
  4. 流批一体

为了实现我们实时统计报表的需求,主要依靠Flink的增量聚合功能。

首先,我们设置了Event Time作为时间窗口的类型,保证了只会计算当天的数据;同时,我们每隔一分钟增量统计当日的消息报表,因此分配1分钟的时间窗口。

然后我们使用.aggregate (AggregateFunction af, WindowFunction wf) 做增量的聚合操作,它能使用AggregateFunction提前聚合掉数据,减少 state 的存储压力。之后,我们将增量聚合后的数据写入到ES和Hbase中。

流程如下所示:

同时,在查询的时候,我们通过taskID、日期等维度进行查询,先从ES中获取taskID的集合,之后通过taskID查询hbase,得出统计结果。

总结

通过使用Flink,我们实现了对消息推送数据的实时统计,能够实时查看消息下发、展示、点击等数据指标,同时,借助FLink强大的状态管理功能,服务的稳定性也得到了一定的保障。未来,个推也将持续优化消息推送服务,并将Flink引入到其他的业务线中,以满足一些实时性要求高的业务场景需求。

收起阅读 »

面试官:同学,说说 Applink 的使用以及原理

推送

简介

通过 Link这个单词我们可以看出这个是一种链接,使用此链接可以直接跳转到 APP,常用于应用拉活,跨应用启动,推送通知启动等场景。

流程

在AS 上其实已经有详细的使用步骤解析了,这里给大家普及下


快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。
在 AS 中已经有详细的使用步骤了,总共分为 4 步

add URL intent filters

创建一个 URL


或者也可以点击 “How it works” 按钮

Add logic to handle the intent

选择通过 applink 启动的入口 activity。
点击完成后,AS 会自动在两个地方进行修改,一个是 AndroidManifest

 <activity android:name=".TestActivity">  
            <intent-filter>  
                <action android:name="android.intent.action.VIEW" />  

                <category android:name="android.intent.category.DEFAULT" />  
                <category android:name="android.intent.category.BROWSABLE" />  

                <data  
                    android:scheme="http"  
                    android:host="geyan.getui.com" />  
            </intent-filter>  
        </activity>

此处多了一个 data,看到这个 data 标签,我们可以大胆的猜测,也许这个 applink 的是一个隐式启动。
另外一个改动点是

    protected void onCreate(@Nullable Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_test);  
        // ATTENTION: This was auto-generated to handle app links.  
        Intent appLinkIntent = getIntent();  
        String appLinkAction = appLinkIntent.getAction();  
        Uri appLinkData = appLinkIntent.getData();  
    }

applink 的值即为之前配置的 url 链接,此处是为了接收数据用的,不再多说了。

Associate website

这一步最关键了,需要根据 APP 的证书生成一个 json 文件, APP 安装的时候会去联网进行校验。选择你的线上证书,然后点击生成会得到一个 assetlinks.json 的文件,需要把这个文件放到服务器指定的目录下


基于安全原因,这个文件必须通过 SSL 的 GET 请求获取,JSON 格式如下:

[{  
  "relation": ["delegate_permission/common.handle_all_urls"],  
  "target": {  
    "namespace": "android_app",  
    "package_name": "com.lenny.myapplication",  
    "sha256_cert_fingerprints":  
    ["E7:E8:47:2A:E1:BF:63:F7:A3:F8:D1:A5:E1:A3:4A:47:88:0F:B5:F3:EA:68:3F:5C:D8:BC:0B:BA:3E:C2:D2:61"]  
  }  
}]

sha256_cert_fingerprints 这个参数可以通过 keytool 命令获取,这里不再多说了。
最后把这个文件上传到 你配置的地址/.well-know/statements/json,为了避免今后每个 app 链接请求都访问网络,安卓只会在 app 安装的时候检查这个文件。,如果你能在请求 https://yourdomain.com/.well-known/statements.json 的时候看到这个文件(替换成自己的域名),那么说明服务端的配置是成功的。目前可以通过 http 获得这个文件,但是在M最终版里则只能通过 HTTPS 验证。确保你的 web 站点支持 HTTPS 请求。
若一个host需要配置多个app,assetlinks.json添加多个app的信息。
若一个 app 需要配置多个 host,每个 host 的 .well-known 下都要配置assetlinks.json

有没有想过 url 的后缀是不是一定要写成 /.well-know/statements/json 的?
后续讲原理的时候会涉及到,这里先不细说。

Test device

最后我们本质仅是拿到一个 URL,大多数的情况下,我们会在 url 中拼接一些参数,比如

https://yourdomain.com/products/123?coupon=save90

其中 ./products/123?coupon=save90 是我们之前在第二步填写的 path。
那测试方法多种多样,可以使用通知,也可以使用短信,或者使用 adb 直接模拟,我这边图省事就直接用 adb 模拟了

adb shell am start  
-W -a android.intent.action.VIEW  
-d "https://yourdomain.com/products/123?coupon=save90"  
[包名]

使用这个命令就会自动打开 APP。前提是 yourdomain.com 网站上存在了 web-app 关联文件。

原理

上述这些都简单的啦,依葫芦画瓢就行,下面讲些深层次的东西,不仅要知道会用,还得知道为什么可以这么用,不然和咸鱼有啥区别。

上诉也说了,我们配置的域名是在 activity 的 data 标签的,那是否是可以认为 applink 是一种隐式启动,应用安装的时候根据 data 的内容到这个网页下面去获取 assetlinks.json 进行校验,如果符合条件则把 这个 url 保存在本地,当点击 webview 或者短信里面的 url的时候,系统会自动与本地库中的域名相匹配, 如果匹配失败则会被自动认为是 deeplink 的连接。确认过眼神对吧~~~
也就说在第一次安装 APP 的时候是会去请求 data 标签下面的域名的,并且去请求所获得的域名,那 安装->初次启动 的体验自然会想到是在源码中 PackageManagerService 实现。
一个 APk 的安装过程是极其复杂的,涉及到非常多的底层知识,这里不细说,直接找到校验 APPLink 的入口 PackageManagerService 的 installPackageLI 方法。

PackageMmanagerService.class

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {  
    final int installFlags = args.installFlags;  
    <!--开始验证applink-->  
    startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);  
    ...  

    }  

    private void startIntentFilterVerifications(int userId, boolean replacing,  
        PackageParser.Package pkg) {  
    ...  

    mHandler.removeMessages(START_INTENT_FILTER_VERIFICATIONS);  
    final Message msg = mHandler.obtainMessage(START_INTENT_FILTER_VERIFICATIONS);  
    msg.obj = new IFVerificationParams(pkg, replacing, userId, verifierUid);  
    mHandler.sendMessage(msg);  
}

可以看到这边发送了一个 message 为 START_INTENT_FILTER_VERIFICATIONS 的 handler 消息,在 handle 的 run 方法里又会接着调用 verifyIntentFiltersIfNeeded。

private void verifyIntentFiltersIfNeeded(int userId, int verifierUid, boolean replacing,  
        PackageParser.Package pkg) {  
        ...  
        <!--检查是否有Activity设置了AppLink-->  
        final boolean hasDomainURLs = hasDomainURLs(pkg);  
        if (!hasDomainURLs) {  
            if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,  
                    "No domain URLs, so no need to verify any IntentFilter!");  
            return;  
        }  
        <!--是否autoverigy-->  
        boolean needToVerify = false;  
        for (PackageParser.Activity a : pkg.activities) {  
            for (ActivityIntentInfo filter : a.intents) {  
            <!--needsVerification是否设置autoverify -->  
                if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {  
                    needToVerify = true;  
                    break;  
                }  
            }  
        }  
      <!--如果有搜集需要验证的Activity信息及scheme信息-->  
        if (needToVerify) {  
            final int verificationId = mIntentFilterVerificationToken++;  
            for (PackageParser.Activity a : pkg.activities) {  
                for (ActivityIntentInfo filter : a.intents) {  
                    if (filter.handlesWebUris(true) && needsNetworkVerificationLPr(filter)) {  
                        if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,  
                                "Verification needed for IntentFilter:" + filter.toString());  
                        mIntentFilterVerifier.addOneIntentFilterVerification(  
                                verifierUid, userId, verificationId, filter, packageName);  
                        count++;  
                    }    }   } }  }  
   <!--开始验证-->  
    if (count > 0) {  
        mIntentFilterVerifier.startVerifications(userId);  
    }   
}

对 APPLink 进行了检查,搜集,验证,主要是对 scheme 的校验是否是 http/https,以及是否有 flag 为 Intent.ACTION_DEFAULT与Intent.ACTION_VIEW 的参数,接着是开启验证

PMS#IntentVerifierProxy.class

public void startVerifications(int userId) {  
        ...  
            sendVerificationRequest(userId, verificationId, ivs);  
        }  
        mCurrentIntentFilterVerifications.clear();  
    }  

    private void sendVerificationRequest(int userId, int verificationId,  
            IntentFilterVerificationState ivs) {  

        Intent verificationIntent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION);  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID,  
                verificationId);  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME,  
                getDefaultScheme());  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS,  
                ivs.getHostsString());  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,  
                ivs.getPackageName());  
        verificationIntent.setComponent(mIntentFilterVerifierComponent);  
        verificationIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);  

        UserHandle user = new UserHandle(userId);  
        mContext.sendBroadcastAsUser(verificationIntent, user);  
    }

目前 Android 的实现是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),发出去的广播会被 IntentFilterVerificationReceiver 接收到。这个类又会再次 start DirectStatementService,在这个 service 里面又会去调用 DirectStatementRetriever 类。在此类的 retrieveStatementFromUrl 方法中才是真正请求网络的地方

DirectStatementRetriever.class

  @Override  
    public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {  
        if (source instanceof AndroidAppAsset) {  
            return retrieveFromAndroid((AndroidAppAsset) source);  
        } else if (source instanceof WebAsset) {  
            return retrieveFromWeb((WebAsset) source);  
        } else {  
            throw new AssociationServiceException("Namespace is not supported.");  
        }  
    }  
  private Result retrieveFromWeb(WebAsset asset)  
            throws AssociationServiceException {  
        return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);  
    }  
    private String computeAssociationJsonUrl(WebAsset asset) {  
        try {  
            return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),  
                    WELL_KNOWN_STATEMENT_PATH)  
                    .toExternalForm();  
        } catch (MalformedURLException e) {  
            throw new AssertionError("Invalid domain name in database.");  
        }  
    }  
private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel,  
                                        AbstractAsset source)  
        throws AssociationServiceException {  
    List<Statement> statements = new ArrayList<Statement>();  
    if (maxIncludeLevel < 0) {  
        return Result.create(statements, DO_NOT_CACHE_RESULT);  
    }  

    WebContent webContent;  
    try {  
        URL url = new URL(urlString);  
        if (!source.followInsecureInclude()  
                && !url.getProtocol().toLowerCase().equals("https")) {  
            return Result.create(statements, DO_NOT_CACHE_RESULT);  
        }  
        <!--通过网络请求获取配置-->  
        webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,  
                HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,  
                HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);  
    } catch (IOException | InterruptedException e) {  
        return Result.create(statements, DO_NOT_CACHE_RESULT);  
    }  

    try {  
        ParsedStatement result = StatementParser  
                .parseStatementList(webContent.getContent(), source);  
        statements.addAll(result.getStatements());  
        <!--如果有一对多的情况,或者说设置了“代理”,则循环获取配置-->  
        for (String delegate : result.getDelegates()) {  
            statements.addAll(  
                    retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)  
                            .getStatements());  
        }  
        <!--发送结果-->  
        return Result.create(statements, webContent.getExpireTimeMillis());  
    } catch (JSONException | IOException e) {  
        return Result.create(statements, DO_NOT_CACHE_RESULT);  
    }  
}

到了这里差不多就全部讲完了,本质就是通过 HTTPURLConnection 去发起来一个请求。之前还留了个问题,是不是一定要要 /.well-known/assetlinks.json,到这里是不是可以完全明白了,就是 WELL_KNOWN_STATEMENT_PATH 参数

    private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";  

缺点

  1. 只能在 Android M 系统上支持
    在配置好了app对App Links的支持之后,只有运行Android M的用户才能正常工作。之前安卓版本的用户无法直接点击链接进入app,而是回到浏览器的web页面。
  2. 要使用App Links开发者必须维护一个与app相关联的网站
    对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。
  3. 对 ink 域名不太友善
    在测试中发现,国内各大厂商对 .ink 域名不太友善,很多的是被支持了 .com 域名,但是不支持 .ink 域名。
机型 版本 是否识别ink 是否识别com
小米 MI6 Android 8.0 MIUI 9.5
小米 MI5 Android 7.0 MIUI 9.5
魅族 PRO 7 Android 7.0 Flyme 6.1.3.1A
三星 S8 Android 7.0 是,弹框
华为 HonorV10 Android 8.0 EMUI 8.0
oppo R11s Android 7.1.1 ColorOS 3.2
oppo A59s Android 5.1 ColorOS 3.0 是,不能跳转到app 是,不能跳转到app
vivo X6Plus A Android 5.0.2 Funtouch OS_2.5
vivo 767 Android 6.0 Funtouch OS_2.6 是,不能跳转到app 是,不能跳转到app
vivo X9 Android 7.1.1 Funtouch OS_3.1 是,不能跳转到app 是,不能跳转到app

参考

1.官方文档: https://developer.android.com/studio/write/app-link-indexing.html

作者:哈哈将

行业前沿、移动开发、数据建模等干货内容,尽在公众号:个推技术学院

继续阅读 »

简介

通过 Link这个单词我们可以看出这个是一种链接,使用此链接可以直接跳转到 APP,常用于应用拉活,跨应用启动,推送通知启动等场景。

流程

在AS 上其实已经有详细的使用步骤解析了,这里给大家普及下


快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。
在 AS 中已经有详细的使用步骤了,总共分为 4 步

add URL intent filters

创建一个 URL


或者也可以点击 “How it works” 按钮

Add logic to handle the intent

选择通过 applink 启动的入口 activity。
点击完成后,AS 会自动在两个地方进行修改,一个是 AndroidManifest

 <activity android:name=".TestActivity">  
            <intent-filter>  
                <action android:name="android.intent.action.VIEW" />  

                <category android:name="android.intent.category.DEFAULT" />  
                <category android:name="android.intent.category.BROWSABLE" />  

                <data  
                    android:scheme="http"  
                    android:host="geyan.getui.com" />  
            </intent-filter>  
        </activity>

此处多了一个 data,看到这个 data 标签,我们可以大胆的猜测,也许这个 applink 的是一个隐式启动。
另外一个改动点是

    protected void onCreate(@Nullable Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_test);  
        // ATTENTION: This was auto-generated to handle app links.  
        Intent appLinkIntent = getIntent();  
        String appLinkAction = appLinkIntent.getAction();  
        Uri appLinkData = appLinkIntent.getData();  
    }

applink 的值即为之前配置的 url 链接,此处是为了接收数据用的,不再多说了。

Associate website

这一步最关键了,需要根据 APP 的证书生成一个 json 文件, APP 安装的时候会去联网进行校验。选择你的线上证书,然后点击生成会得到一个 assetlinks.json 的文件,需要把这个文件放到服务器指定的目录下


基于安全原因,这个文件必须通过 SSL 的 GET 请求获取,JSON 格式如下:

[{  
  "relation": ["delegate_permission/common.handle_all_urls"],  
  "target": {  
    "namespace": "android_app",  
    "package_name": "com.lenny.myapplication",  
    "sha256_cert_fingerprints":  
    ["E7:E8:47:2A:E1:BF:63:F7:A3:F8:D1:A5:E1:A3:4A:47:88:0F:B5:F3:EA:68:3F:5C:D8:BC:0B:BA:3E:C2:D2:61"]  
  }  
}]

sha256_cert_fingerprints 这个参数可以通过 keytool 命令获取,这里不再多说了。
最后把这个文件上传到 你配置的地址/.well-know/statements/json,为了避免今后每个 app 链接请求都访问网络,安卓只会在 app 安装的时候检查这个文件。,如果你能在请求 https://yourdomain.com/.well-known/statements.json 的时候看到这个文件(替换成自己的域名),那么说明服务端的配置是成功的。目前可以通过 http 获得这个文件,但是在M最终版里则只能通过 HTTPS 验证。确保你的 web 站点支持 HTTPS 请求。
若一个host需要配置多个app,assetlinks.json添加多个app的信息。
若一个 app 需要配置多个 host,每个 host 的 .well-known 下都要配置assetlinks.json

有没有想过 url 的后缀是不是一定要写成 /.well-know/statements/json 的?
后续讲原理的时候会涉及到,这里先不细说。

Test device

最后我们本质仅是拿到一个 URL,大多数的情况下,我们会在 url 中拼接一些参数,比如

https://yourdomain.com/products/123?coupon=save90

其中 ./products/123?coupon=save90 是我们之前在第二步填写的 path。
那测试方法多种多样,可以使用通知,也可以使用短信,或者使用 adb 直接模拟,我这边图省事就直接用 adb 模拟了

adb shell am start  
-W -a android.intent.action.VIEW  
-d "https://yourdomain.com/products/123?coupon=save90"  
[包名]

使用这个命令就会自动打开 APP。前提是 yourdomain.com 网站上存在了 web-app 关联文件。

原理

上述这些都简单的啦,依葫芦画瓢就行,下面讲些深层次的东西,不仅要知道会用,还得知道为什么可以这么用,不然和咸鱼有啥区别。

上诉也说了,我们配置的域名是在 activity 的 data 标签的,那是否是可以认为 applink 是一种隐式启动,应用安装的时候根据 data 的内容到这个网页下面去获取 assetlinks.json 进行校验,如果符合条件则把 这个 url 保存在本地,当点击 webview 或者短信里面的 url的时候,系统会自动与本地库中的域名相匹配, 如果匹配失败则会被自动认为是 deeplink 的连接。确认过眼神对吧~~~
也就说在第一次安装 APP 的时候是会去请求 data 标签下面的域名的,并且去请求所获得的域名,那 安装->初次启动 的体验自然会想到是在源码中 PackageManagerService 实现。
一个 APk 的安装过程是极其复杂的,涉及到非常多的底层知识,这里不细说,直接找到校验 APPLink 的入口 PackageManagerService 的 installPackageLI 方法。

PackageMmanagerService.class

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {  
    final int installFlags = args.installFlags;  
    <!--开始验证applink-->  
    startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);  
    ...  

    }  

    private void startIntentFilterVerifications(int userId, boolean replacing,  
        PackageParser.Package pkg) {  
    ...  

    mHandler.removeMessages(START_INTENT_FILTER_VERIFICATIONS);  
    final Message msg = mHandler.obtainMessage(START_INTENT_FILTER_VERIFICATIONS);  
    msg.obj = new IFVerificationParams(pkg, replacing, userId, verifierUid);  
    mHandler.sendMessage(msg);  
}

可以看到这边发送了一个 message 为 START_INTENT_FILTER_VERIFICATIONS 的 handler 消息,在 handle 的 run 方法里又会接着调用 verifyIntentFiltersIfNeeded。

private void verifyIntentFiltersIfNeeded(int userId, int verifierUid, boolean replacing,  
        PackageParser.Package pkg) {  
        ...  
        <!--检查是否有Activity设置了AppLink-->  
        final boolean hasDomainURLs = hasDomainURLs(pkg);  
        if (!hasDomainURLs) {  
            if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,  
                    "No domain URLs, so no need to verify any IntentFilter!");  
            return;  
        }  
        <!--是否autoverigy-->  
        boolean needToVerify = false;  
        for (PackageParser.Activity a : pkg.activities) {  
            for (ActivityIntentInfo filter : a.intents) {  
            <!--needsVerification是否设置autoverify -->  
                if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {  
                    needToVerify = true;  
                    break;  
                }  
            }  
        }  
      <!--如果有搜集需要验证的Activity信息及scheme信息-->  
        if (needToVerify) {  
            final int verificationId = mIntentFilterVerificationToken++;  
            for (PackageParser.Activity a : pkg.activities) {  
                for (ActivityIntentInfo filter : a.intents) {  
                    if (filter.handlesWebUris(true) && needsNetworkVerificationLPr(filter)) {  
                        if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,  
                                "Verification needed for IntentFilter:" + filter.toString());  
                        mIntentFilterVerifier.addOneIntentFilterVerification(  
                                verifierUid, userId, verificationId, filter, packageName);  
                        count++;  
                    }    }   } }  }  
   <!--开始验证-->  
    if (count > 0) {  
        mIntentFilterVerifier.startVerifications(userId);  
    }   
}

对 APPLink 进行了检查,搜集,验证,主要是对 scheme 的校验是否是 http/https,以及是否有 flag 为 Intent.ACTION_DEFAULT与Intent.ACTION_VIEW 的参数,接着是开启验证

PMS#IntentVerifierProxy.class

public void startVerifications(int userId) {  
        ...  
            sendVerificationRequest(userId, verificationId, ivs);  
        }  
        mCurrentIntentFilterVerifications.clear();  
    }  

    private void sendVerificationRequest(int userId, int verificationId,  
            IntentFilterVerificationState ivs) {  

        Intent verificationIntent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION);  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID,  
                verificationId);  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME,  
                getDefaultScheme());  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS,  
                ivs.getHostsString());  
        verificationIntent.putExtra(  
                PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,  
                ivs.getPackageName());  
        verificationIntent.setComponent(mIntentFilterVerifierComponent);  
        verificationIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);  

        UserHandle user = new UserHandle(userId);  
        mContext.sendBroadcastAsUser(verificationIntent, user);  
    }

目前 Android 的实现是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),发出去的广播会被 IntentFilterVerificationReceiver 接收到。这个类又会再次 start DirectStatementService,在这个 service 里面又会去调用 DirectStatementRetriever 类。在此类的 retrieveStatementFromUrl 方法中才是真正请求网络的地方

DirectStatementRetriever.class

  @Override  
    public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {  
        if (source instanceof AndroidAppAsset) {  
            return retrieveFromAndroid((AndroidAppAsset) source);  
        } else if (source instanceof WebAsset) {  
            return retrieveFromWeb((WebAsset) source);  
        } else {  
            throw new AssociationServiceException("Namespace is not supported.");  
        }  
    }  
  private Result retrieveFromWeb(WebAsset asset)  
            throws AssociationServiceException {  
        return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);  
    }  
    private String computeAssociationJsonUrl(WebAsset asset) {  
        try {  
            return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),  
                    WELL_KNOWN_STATEMENT_PATH)  
                    .toExternalForm();  
        } catch (MalformedURLException e) {  
            throw new AssertionError("Invalid domain name in database.");  
        }  
    }  
private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel,  
                                        AbstractAsset source)  
        throws AssociationServiceException {  
    List<Statement> statements = new ArrayList<Statement>();  
    if (maxIncludeLevel < 0) {  
        return Result.create(statements, DO_NOT_CACHE_RESULT);  
    }  

    WebContent webContent;  
    try {  
        URL url = new URL(urlString);  
        if (!source.followInsecureInclude()  
                && !url.getProtocol().toLowerCase().equals("https")) {  
            return Result.create(statements, DO_NOT_CACHE_RESULT);  
        }  
        <!--通过网络请求获取配置-->  
        webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,  
                HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,  
                HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);  
    } catch (IOException | InterruptedException e) {  
        return Result.create(statements, DO_NOT_CACHE_RESULT);  
    }  

    try {  
        ParsedStatement result = StatementParser  
                .parseStatementList(webContent.getContent(), source);  
        statements.addAll(result.getStatements());  
        <!--如果有一对多的情况,或者说设置了“代理”,则循环获取配置-->  
        for (String delegate : result.getDelegates()) {  
            statements.addAll(  
                    retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)  
                            .getStatements());  
        }  
        <!--发送结果-->  
        return Result.create(statements, webContent.getExpireTimeMillis());  
    } catch (JSONException | IOException e) {  
        return Result.create(statements, DO_NOT_CACHE_RESULT);  
    }  
}

到了这里差不多就全部讲完了,本质就是通过 HTTPURLConnection 去发起来一个请求。之前还留了个问题,是不是一定要要 /.well-known/assetlinks.json,到这里是不是可以完全明白了,就是 WELL_KNOWN_STATEMENT_PATH 参数

    private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";  

缺点

  1. 只能在 Android M 系统上支持
    在配置好了app对App Links的支持之后,只有运行Android M的用户才能正常工作。之前安卓版本的用户无法直接点击链接进入app,而是回到浏览器的web页面。
  2. 要使用App Links开发者必须维护一个与app相关联的网站
    对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。
  3. 对 ink 域名不太友善
    在测试中发现,国内各大厂商对 .ink 域名不太友善,很多的是被支持了 .com 域名,但是不支持 .ink 域名。
机型 版本 是否识别ink 是否识别com
小米 MI6 Android 8.0 MIUI 9.5
小米 MI5 Android 7.0 MIUI 9.5
魅族 PRO 7 Android 7.0 Flyme 6.1.3.1A
三星 S8 Android 7.0 是,弹框
华为 HonorV10 Android 8.0 EMUI 8.0
oppo R11s Android 7.1.1 ColorOS 3.2
oppo A59s Android 5.1 ColorOS 3.0 是,不能跳转到app 是,不能跳转到app
vivo X6Plus A Android 5.0.2 Funtouch OS_2.5
vivo 767 Android 6.0 Funtouch OS_2.6 是,不能跳转到app 是,不能跳转到app
vivo X9 Android 7.1.1 Funtouch OS_3.1 是,不能跳转到app 是,不能跳转到app

参考

1.官方文档: https://developer.android.com/studio/write/app-link-indexing.html

作者:哈哈将

行业前沿、移动开发、数据建模等干货内容,尽在公众号:个推技术学院

收起阅读 »

uni-app h5使用wxjssdk开发中遇到的各种坑---持续更新,如果有可以解决的在评论区回复我,谢谢分享

微信JSSDK 浏览器兼容 uniapp插件
  1. h5网页IOS内input无法自动聚焦,获取焦点
  2. h5网页内textarea的placeholder-class设置正常显示,class放在app.vue,打包成小程序之后placeholder-class没有生效,输入文字删除后才会生效。(真机可以,开发者工具不行)
  3. h5网页不能复制粘贴板(uni.setClipboardData) 使用插件市场提供的插件可以,页面内引入就行H5对齐API https://ext.dcloud.net.cn/plugin?id=415
继续阅读 »
  1. h5网页IOS内input无法自动聚焦,获取焦点
  2. h5网页内textarea的placeholder-class设置正常显示,class放在app.vue,打包成小程序之后placeholder-class没有生效,输入文字删除后才会生效。(真机可以,开发者工具不行)
  3. h5网页不能复制粘贴板(uni.setClipboardData) 使用插件市场提供的插件可以,页面内引入就行H5对齐API https://ext.dcloud.net.cn/plugin?id=415
收起阅读 »

关于ios运行自定义基座 安装失败 return code=-402620395

自定义基座

一定要使用 开发(Development)证书及profile文件打包生成自定义基座
并且需要添加测试机的UDID
使用发布证书不可以

一定要使用 开发(Development)证书及profile文件打包生成自定义基座
并且需要添加测试机的UDID
使用发布证书不可以

uni-ad App端打包注意及plus.ad使用指南

uni-ad preload

HBuilderX 2.5.3 版本起,DCloud广告联盟升级为uni-ad。uni-app、5+ App、wap2app等项目全都支持使用uni-ad。

概述

uni-ad聚合目前国内流行的广告平台优量汇、穿山甲、快手、百度等广告渠道,支持开屏广告和信息流、Banner广告。

使用uni-ad前,需登录uni-ad广告联盟申请开通
参考教程:https://ask.dcloud.net.cn/article/36769

注意问题

Android平台

Android权限问题

我们目前测试发现在华为Android8以上手机,使用穿山甲(今日头条)广告时如果没有给应用读取设备信息权限,只会显示抖音广告。
建议开发者开通广告时,最好能引导用户允许读取设备信息权限,这样给用户下发的广告会更精准,可以获得更高的CPM。
由于2019年底开始工业和信息化部展开App侵犯用户权益专项整治行动,不允许App在用户拒绝授权后应用退出或关闭。因此建议开发者根据以下方案进行选择:

  • 如果不提交到各应用商店
    建议开发者配置应用每次启动都申请读取设备信息权限,并且用户必须允许,即permissionPhoneState下的request配置为"always",详情参考:https://ask.dcloud.net.cn/article/36549#phonestate
  • 如果提交应用市场
    建议开发者申请穿山甲广告,同时申请开通优量汇、快手、百度等渠道广告,uni-ad会自动优化选择展现效果好的广告,最大化提升广告的CPM。

华为应用市场审核问题

目前已知开通广告后提交华为应用市场可能无法通过审核
反馈信息:“未通过原因:您的应用点闪屏广告或者弹窗广告立即下载,请修复
这是因为穿山甲和优量汇的app推广广告,用户点击后不会弹窗提示确认,直接下载apk导致的。如果要修改为提示用户确认后再下载,需人工向穿山甲和优量汇广告平台申请操作。
如碰到此问题,请邮件联系:uniad@dcloud.io
邮件标题:华为应用市场点击广告立即下载问题
并提供以下内容:

应用标识:__UNI__XXXXXX

谷歌应用(google play)市场审核问题

谷歌应用市场最新政策要求应用中不能包含直接下载apk的逻辑,只能通过google play更新/安装应用。目前国内广告厂商(穿山甲、优量汇)都是直接下载apk安装,所以无法通过谷歌应用市场的审核。
如果要提交谷歌应用市场,请不要勾选第三方广告联盟(穿山甲、优量汇),广告基础功能不受影响
后续我们会接入Google Ads、Facebook Ads等。

iOS平台

为了提升广告效果,如果勾选了优量汇、穿山甲、快手中的任何一个广告平台,则一定会使用广告标识(IDFA)
此时提交AppStore审核时注意需要勾选使用广告标识符,详情参考:https://ask.dcloud.net.cn/article/36107
注意:从HBuilder X- 3.1.13版本之后,iOS14.5的手机会在流量中显示 App Tracking Transparency 授权来获取IDFA,所以需要在manifest.json -> "App权限配置"中配置NSUserTrackingUsageDescription,描述获取IDFA的用途,否则会导致App Store审核不过或者导致应用运行闪退。例如:

配置uni-ad广告模块

云端打包时选择要集成的广告平台sdk。

  • 方式1:
    打开项目的manifest.json文件,在“App模块配置”页的“uni-ad”项下勾选需要集成的广告平台:

  • 方式2:云打包界面直接选中,然后打包

勾选App需要支持的广告平台,提交云端打包生效

不勾选广告平台,打包将不会把对应的广告SDK打进去,也就无法显示对应平台的广告

使用广告

开屏广告

uni-ad广告联盟申请开通“开屏广告”后重新提交云端打包即可。
开屏广告无需编程,可直接使用。

<a id="splash"/>

自定义开屏广告界面

开屏广告界面屏幕顶部85%区域显示广告内容,屏幕底部15%区域默认显示应用图标及名称,支持自定义底部区域显示内容:

  • uni-app项目
    在manifest.json文件的 "app-plus" -> "splashscreen" 下添加ads节点
  • 5+ APp项目
    在manifest.json文件的 "plus" -> "splashscreen" 下添加ads节点
        "splashscreen" : {  
            "ads" : {  
                "background" : "#FF0000",  
                "image" : "static/logo.png"  
            },  
            //...  
        },

其中 background 配置背景颜色,格式为“#RRGGBB”;
image 配置底部区域显示的图片,配置不再显示应用图标及名称,图片路径相对应用资源目录路径,不支持网络地址,建议分辨率720x256(要求png格式,背景透明,留出边距,在不同分辨率手机上会自动等比例缩放处理。

注意:

  • Android平台
    自定义开屏广告界面更新应用资源生效
  • iOS平台
    开屏广告显示在默认开屏界面splash上,仅在使用通用启动界面才支持自定义底部区域显示内容,且需提交云端打包后生效。
    如果使用自定义storyboard启动界面,则以上配置失效,显示自定义storyboard启动界面底部15%区域。

更多开屏界面配置项,请登录uni-ad广告联盟,在应用详情的开屏配置页面修改:

<a id="splash_fs"/>

开屏广告是否全屏展示

HBuilderX2.8.12及以下版本,应用仅支持费全屏开屏广告。
HBuilderX2.8.7及以上版本,新增支持应用设置开屏广告全屏显示,默认非全屏显示,如需全屏显示,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中打开“是否全屏展示”开关。

<a id="splash_fr"/>

应用从后台切回到前台是否显示广告

HBuilderX2.8.12及以下版本,应用仅在启动时显示开屏广告,应用从后台切换到前台不会显示开屏广告。
HBuilderX2.9.0及以上版本,应用支持从后台切换到前台时显示开屏广告,如果应用开通了开屏广告功能,默认也会开通后台切换到前台显示开屏广告,如需关闭此功能,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中关闭“从后台切回后是否展示”开关。

信息流、Banner广告

信息流和banner,需要编程。
使用信息流前,需在uni-ad广告联盟申请获取广告位标识(adpid)
HBuilderX标准基座真机运行测试信息流广告位标识(adpid)为:1111111111

注意:AdView为原生组件,仅支持页面级的滚动。

uni-app项目

使用已经封装好的 ad 组件。
详情参考:https://uniapp.dcloud.io/component/ad

5+ App(WAP2APP)项目

5+ APP中使用原生AdView原生控件渲染广告数据,其层级要高于所有DOM元素,使用时务必注意层级问题。
使用信息流、banner广告时,按以下步骤操作:

  • 在DOM中创建占位div
    var adDom = null;  
    // DOMContentloaded事件处理  
    document.addEventListener('DOMContentLoaded', function(){  
    //获取占位div元素  
    adDom = document.getElementById('ad');  
    }, false);  
    //在html中要显示广告的位置放置占位div  
    <div id="ad" style="width:100%;height:0px;"></div>
  • 获取广告数据
    var adata = null;  
    function getAdData(){  
    //获取广告数据  
    plus.ad.getAds({adpid:'1111111111',  //替换为自己申请获取的广告位标识,此广告位标识仅在HBuilderX标准基座中有效  
      width:'100%',  //广告将要显示的宽度  
      count:1           //注意实际业务中建议一次请求3-5条广告,避免请求到相同的广告  
    }, function(e){  
    console.log('获取广告成功: '+JSON.stringify(e));  
    if(!e || !e.ads || e.ads.length<1){  
      console.log('无广告数据!');  
    }else{  
      console.log('更新广告数据!');  
      adata = e.ads[0];  //这里只使用一条广告数据  
    }  
    }, function(e){  
    console.log('获取广告失败: '+JSON.stringify(e));  
    });  
    }

    注意:获取广告数据时需传入广告展现时真实的宽度,便于向广告平台获取合适的广告数据

  • 创建广告控件,监听渲染和关闭事件,绑定并渲染广告数据
    var adView = null;  
    function showAdView(){  
    //创建AdView控件  
    adView = plus.ad.createAdView({  
    top:adDom.offsetTop+'px',  
    left:'0px',  
    width:'100%',  
    height:'0px',  
    position: 'static'  
    });  
    //将AdView添加到Webview窗口中  
    plus.webview.currentWebview().append(adView);  
    //监听AdView渲染完成事件,动态调整高度  
    adView.setRenderingListener(function(e){  
    console.log('渲染广告完成: '+JSON.stringify(e));  
    if(0 != e.result){  
      console.log('渲染失败!');  
    }else{  
      //调整广告控件高度,显示广告内容;调整广告控件的top值,避免渲染过程中top值发生变化导致的广告位置不对的问题。  
      adView.setStyle({top: adDom.offsetTop + 'px', height: e.height + 'px' });  
      //调整占位div高度,避免被广告控件盖住DOM元素  
      adDom.style.height = e.height+'px';  
    }  
    });  
    //监听用户关闭广告控件事件  
    adView.setDislikeListener(function(e){  
    console.log('用户关闭广告: '+JSON.stringify(e));  
    adView.close(),adView=null;  
    //调整占位div高度,避免关闭广告控件后显示空白区域  
    adDom.style.height = '0px';  
    });  
    //绑定并渲染广告数据  
    adView.renderingBind(adata);  
    }

激励视频广告

HBuilderX标准基座真机运行测试激励视频广告位标识(adpid)为:1507000689

uni-app项目

使用已经封装成uni API。
详情参考:https://uniapp.dcloud.io/api/ad/rewarded-video-ad

5+ App(WAP2APP)项目

激励视频广告由RewardedVideoAd管理。
操作步骤如下:

  • 创建激励视频广告对象 plus.ad.createRewardedVideoAd,需传入在uni-ad平台申请的广告位标识adpid
  • 监听激励视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
  • 监听激励视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
  • 监听激励视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告,发放奖励
  • 加载激励视频广告:调用广告对象load

示例如下:

//视频激励广告  
var adReward = null;  
function rewardedVideoAd(){  
  if(adReward){  
    outLine('正在加载激励视频广告');  
    return;  
  }  
  console.log('#视频激励广告#');  
  adReward = plus.ad.createRewardedVideoAd({adpid:'1507000689'});  // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效  
  adReward.onLoad(function(){  
    console.log('加载成功')  
    adReward.show();  
  });  
  adReward.onError(function(e){  
    console.log('加载失败: '+JSON.stringify(e));  
    adReward.destroy();  
    adReward = null;  
  });  
  adReward.onClose(function(e){  
    if(e.isEnded){  
      console.log('激励视频播放完成');  
      plus.nativeUI.toast('激励视频播放完成');  
    }else{  
      console.log('激励视频未播放完成关闭!')  
    }  
    adReward.destroy();  
    adReward = null;  
  });  
  adReward.load();  
}

<a id="fullscreenvideo"/>

全屏视频广告

HBuilderX标准基座真机运行测试全屏视频广告位标识(adpid)为:1507000611
全屏视频广告与激励视频广告效果类似,在应用中可以在激励视频广告填充不足时用全屏视频广告来代替。
全屏视频与激励视频广告的差别:

  1. 不支持视频服务器回调校验事件
  2. 全屏视频在播放结束前(通常播放6-10秒)可以关闭,用户关闭视频不会提醒用户无法获取奖励

uni-app项目

使用已经封装成uni API。
详情参考:https://uniapp.dcloud.net.cn/api/a-d/full-screen-video

5+ App(WAP2APP)项目

全屏视频广告由FullScreenVideoAd管理。
操作步骤如下:

  • 创建全屏视频广告对象 plus.ad.createFullScreenVideoAd,需传入在uni-ad平台申请的广告位标识adpid
  • 监听全屏视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
  • 监听全屏视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
  • 监听全屏视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告
  • 加载激励视频广告:调用广告对象load

示例如下:

//视全屏激励广告  
var adFull = null;  
function fullVideoAd(){  
  if(adFull){  
    outLine('正在加载全屏视频广告');  
    return;  
  }  
  console.log('#全屏视频广告#');  
  adFull = plus.ad.createFullScreenVideoAd({adpid:'1507000611'});  // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效  
  adFull.onLoad(function(){  
    console.log('加载成功')  
    adFull.show();  
  });  
  adFull.onError(function(e){  
    console.log('加载失败: '+JSON.stringify(e));  
    adFull.destroy();  
    adFull = null;  
  });  
  adFull.onClose(function(e){  
    if(e.isEnded){  
      console.log('全屏视频播放完成');  
      plus.nativeUI.toast('全屏视频播放完成');  
    }else{  
      console.log('全屏视频未播放完成关闭!')  
    }  
    adFull.destroy();  
    adFull = null;  
  });  
  adFull.load();  
}

沉浸视频流广告

也称为Draw视频信息流广告

沉浸视频流广告为媒体提供了竖屏视频信息流广告样式,适合在全屏的竖屏视频中使用。目前仅提供了nvue的方式使用,使用方式可以参考文档:https://uniapp.dcloud.net.cn/component/ad-draw

HBuilderX3.0.0及以上版本开始支持。

内容联盟

内容联盟提供了单独的接口接入视频流供接入方接入。是一种支持用户上下滑动的切换视频的内容形式。

使用步骤:

示例:

        plus.ad.showContentPage({  
            adpid:"1111111112", // 1111111112为测试广告位,使用时请替换成自己的。  
        }, function(e) {  
            console.log("成功")  
        }, function(e) {  
            console.log(JSON.stringify(e))  
        });  

<a id="rewarderror"/>
<a id="videoerror"/>

广告错误码

激励视频及全屏视频广告常见错误及处理建议:

  • "-5001"
    广告位标识adpid为空,请传入有效的adpid
    请到广告平台申请广告位并获取adpid。
  • "-5002"
    无效的广告位标识adpid,请使用正确的adpid
    请到广告平台确认使用的广告位标识adpid是否正确。
  • "-5003"
    未开通广告,请在广告平台申请并确保已审核通过
    当前应用还没有开通广告,或者广告还没有审核通过,请到广告平台查看申请状态。
  • "-5004"
    无广告模块,打包时请配置要使用的广告模块
    云端打包时没有勾选广告平台SDK,请参考前面“配置uni-ad广告模块”方法勾选后重新提交云端打包。
  • "-5005"
    广告加载失败,请稍后重试
    加载视频激励广告失败,返回此错误时建议过一段时间再重新加载一次。
  • "-5006"
    广告未加载完成无法播放,请加载完成后再调show播放
  • "-5007"
    无法获取广告配置数据,请尝试重试
    返回此错误时建议重新加载一次。
  • "-5100"
    其他错误,聚合广告商内部错误。
  • 其他错误码及详细介绍请参考[https://uniapp.dcloud.net.cn/uni-ad/ad-error-code.html]

    本地离线打包

    Android平台参考:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/androidModuleConfig/uniad
    iOS平台参数:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/iOSModuleConfig/uniad

继续阅读 »

HBuilderX 2.5.3 版本起,DCloud广告联盟升级为uni-ad。uni-app、5+ App、wap2app等项目全都支持使用uni-ad。

概述

uni-ad聚合目前国内流行的广告平台优量汇、穿山甲、快手、百度等广告渠道,支持开屏广告和信息流、Banner广告。

使用uni-ad前,需登录uni-ad广告联盟申请开通
参考教程:https://ask.dcloud.net.cn/article/36769

注意问题

Android平台

Android权限问题

我们目前测试发现在华为Android8以上手机,使用穿山甲(今日头条)广告时如果没有给应用读取设备信息权限,只会显示抖音广告。
建议开发者开通广告时,最好能引导用户允许读取设备信息权限,这样给用户下发的广告会更精准,可以获得更高的CPM。
由于2019年底开始工业和信息化部展开App侵犯用户权益专项整治行动,不允许App在用户拒绝授权后应用退出或关闭。因此建议开发者根据以下方案进行选择:

  • 如果不提交到各应用商店
    建议开发者配置应用每次启动都申请读取设备信息权限,并且用户必须允许,即permissionPhoneState下的request配置为"always",详情参考:https://ask.dcloud.net.cn/article/36549#phonestate
  • 如果提交应用市场
    建议开发者申请穿山甲广告,同时申请开通优量汇、快手、百度等渠道广告,uni-ad会自动优化选择展现效果好的广告,最大化提升广告的CPM。

华为应用市场审核问题

目前已知开通广告后提交华为应用市场可能无法通过审核
反馈信息:“未通过原因:您的应用点闪屏广告或者弹窗广告立即下载,请修复
这是因为穿山甲和优量汇的app推广广告,用户点击后不会弹窗提示确认,直接下载apk导致的。如果要修改为提示用户确认后再下载,需人工向穿山甲和优量汇广告平台申请操作。
如碰到此问题,请邮件联系:uniad@dcloud.io
邮件标题:华为应用市场点击广告立即下载问题
并提供以下内容:

应用标识:__UNI__XXXXXX

谷歌应用(google play)市场审核问题

谷歌应用市场最新政策要求应用中不能包含直接下载apk的逻辑,只能通过google play更新/安装应用。目前国内广告厂商(穿山甲、优量汇)都是直接下载apk安装,所以无法通过谷歌应用市场的审核。
如果要提交谷歌应用市场,请不要勾选第三方广告联盟(穿山甲、优量汇),广告基础功能不受影响
后续我们会接入Google Ads、Facebook Ads等。

iOS平台

为了提升广告效果,如果勾选了优量汇、穿山甲、快手中的任何一个广告平台,则一定会使用广告标识(IDFA)
此时提交AppStore审核时注意需要勾选使用广告标识符,详情参考:https://ask.dcloud.net.cn/article/36107
注意:从HBuilder X- 3.1.13版本之后,iOS14.5的手机会在流量中显示 App Tracking Transparency 授权来获取IDFA,所以需要在manifest.json -> "App权限配置"中配置NSUserTrackingUsageDescription,描述获取IDFA的用途,否则会导致App Store审核不过或者导致应用运行闪退。例如:

配置uni-ad广告模块

云端打包时选择要集成的广告平台sdk。

  • 方式1:
    打开项目的manifest.json文件,在“App模块配置”页的“uni-ad”项下勾选需要集成的广告平台:

  • 方式2:云打包界面直接选中,然后打包

勾选App需要支持的广告平台,提交云端打包生效

不勾选广告平台,打包将不会把对应的广告SDK打进去,也就无法显示对应平台的广告

使用广告

开屏广告

uni-ad广告联盟申请开通“开屏广告”后重新提交云端打包即可。
开屏广告无需编程,可直接使用。

<a id="splash"/>

自定义开屏广告界面

开屏广告界面屏幕顶部85%区域显示广告内容,屏幕底部15%区域默认显示应用图标及名称,支持自定义底部区域显示内容:

  • uni-app项目
    在manifest.json文件的 "app-plus" -> "splashscreen" 下添加ads节点
  • 5+ APp项目
    在manifest.json文件的 "plus" -> "splashscreen" 下添加ads节点
        "splashscreen" : {  
            "ads" : {  
                "background" : "#FF0000",  
                "image" : "static/logo.png"  
            },  
            //...  
        },

其中 background 配置背景颜色,格式为“#RRGGBB”;
image 配置底部区域显示的图片,配置不再显示应用图标及名称,图片路径相对应用资源目录路径,不支持网络地址,建议分辨率720x256(要求png格式,背景透明,留出边距,在不同分辨率手机上会自动等比例缩放处理。

注意:

  • Android平台
    自定义开屏广告界面更新应用资源生效
  • iOS平台
    开屏广告显示在默认开屏界面splash上,仅在使用通用启动界面才支持自定义底部区域显示内容,且需提交云端打包后生效。
    如果使用自定义storyboard启动界面,则以上配置失效,显示自定义storyboard启动界面底部15%区域。

更多开屏界面配置项,请登录uni-ad广告联盟,在应用详情的开屏配置页面修改:

<a id="splash_fs"/>

开屏广告是否全屏展示

HBuilderX2.8.12及以下版本,应用仅支持费全屏开屏广告。
HBuilderX2.8.7及以上版本,新增支持应用设置开屏广告全屏显示,默认非全屏显示,如需全屏显示,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中打开“是否全屏展示”开关。

<a id="splash_fr"/>

应用从后台切回到前台是否显示广告

HBuilderX2.8.12及以下版本,应用仅在启动时显示开屏广告,应用从后台切换到前台不会显示开屏广告。
HBuilderX2.9.0及以上版本,应用支持从后台切换到前台时显示开屏广告,如果应用开通了开屏广告功能,默认也会开通后台切换到前台显示开屏广告,如需关闭此功能,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中关闭“从后台切回后是否展示”开关。

信息流、Banner广告

信息流和banner,需要编程。
使用信息流前,需在uni-ad广告联盟申请获取广告位标识(adpid)
HBuilderX标准基座真机运行测试信息流广告位标识(adpid)为:1111111111

注意:AdView为原生组件,仅支持页面级的滚动。

uni-app项目

使用已经封装好的 ad 组件。
详情参考:https://uniapp.dcloud.io/component/ad

5+ App(WAP2APP)项目

5+ APP中使用原生AdView原生控件渲染广告数据,其层级要高于所有DOM元素,使用时务必注意层级问题。
使用信息流、banner广告时,按以下步骤操作:

  • 在DOM中创建占位div
    var adDom = null;  
    // DOMContentloaded事件处理  
    document.addEventListener('DOMContentLoaded', function(){  
    //获取占位div元素  
    adDom = document.getElementById('ad');  
    }, false);  
    //在html中要显示广告的位置放置占位div  
    <div id="ad" style="width:100%;height:0px;"></div>
  • 获取广告数据
    var adata = null;  
    function getAdData(){  
    //获取广告数据  
    plus.ad.getAds({adpid:'1111111111',  //替换为自己申请获取的广告位标识,此广告位标识仅在HBuilderX标准基座中有效  
      width:'100%',  //广告将要显示的宽度  
      count:1           //注意实际业务中建议一次请求3-5条广告,避免请求到相同的广告  
    }, function(e){  
    console.log('获取广告成功: '+JSON.stringify(e));  
    if(!e || !e.ads || e.ads.length<1){  
      console.log('无广告数据!');  
    }else{  
      console.log('更新广告数据!');  
      adata = e.ads[0];  //这里只使用一条广告数据  
    }  
    }, function(e){  
    console.log('获取广告失败: '+JSON.stringify(e));  
    });  
    }

    注意:获取广告数据时需传入广告展现时真实的宽度,便于向广告平台获取合适的广告数据

  • 创建广告控件,监听渲染和关闭事件,绑定并渲染广告数据
    var adView = null;  
    function showAdView(){  
    //创建AdView控件  
    adView = plus.ad.createAdView({  
    top:adDom.offsetTop+'px',  
    left:'0px',  
    width:'100%',  
    height:'0px',  
    position: 'static'  
    });  
    //将AdView添加到Webview窗口中  
    plus.webview.currentWebview().append(adView);  
    //监听AdView渲染完成事件,动态调整高度  
    adView.setRenderingListener(function(e){  
    console.log('渲染广告完成: '+JSON.stringify(e));  
    if(0 != e.result){  
      console.log('渲染失败!');  
    }else{  
      //调整广告控件高度,显示广告内容;调整广告控件的top值,避免渲染过程中top值发生变化导致的广告位置不对的问题。  
      adView.setStyle({top: adDom.offsetTop + 'px', height: e.height + 'px' });  
      //调整占位div高度,避免被广告控件盖住DOM元素  
      adDom.style.height = e.height+'px';  
    }  
    });  
    //监听用户关闭广告控件事件  
    adView.setDislikeListener(function(e){  
    console.log('用户关闭广告: '+JSON.stringify(e));  
    adView.close(),adView=null;  
    //调整占位div高度,避免关闭广告控件后显示空白区域  
    adDom.style.height = '0px';  
    });  
    //绑定并渲染广告数据  
    adView.renderingBind(adata);  
    }

激励视频广告

HBuilderX标准基座真机运行测试激励视频广告位标识(adpid)为:1507000689

uni-app项目

使用已经封装成uni API。
详情参考:https://uniapp.dcloud.io/api/ad/rewarded-video-ad

5+ App(WAP2APP)项目

激励视频广告由RewardedVideoAd管理。
操作步骤如下:

  • 创建激励视频广告对象 plus.ad.createRewardedVideoAd,需传入在uni-ad平台申请的广告位标识adpid
  • 监听激励视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
  • 监听激励视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
  • 监听激励视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告,发放奖励
  • 加载激励视频广告:调用广告对象load

示例如下:

//视频激励广告  
var adReward = null;  
function rewardedVideoAd(){  
  if(adReward){  
    outLine('正在加载激励视频广告');  
    return;  
  }  
  console.log('#视频激励广告#');  
  adReward = plus.ad.createRewardedVideoAd({adpid:'1507000689'});  // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效  
  adReward.onLoad(function(){  
    console.log('加载成功')  
    adReward.show();  
  });  
  adReward.onError(function(e){  
    console.log('加载失败: '+JSON.stringify(e));  
    adReward.destroy();  
    adReward = null;  
  });  
  adReward.onClose(function(e){  
    if(e.isEnded){  
      console.log('激励视频播放完成');  
      plus.nativeUI.toast('激励视频播放完成');  
    }else{  
      console.log('激励视频未播放完成关闭!')  
    }  
    adReward.destroy();  
    adReward = null;  
  });  
  adReward.load();  
}

<a id="fullscreenvideo"/>

全屏视频广告

HBuilderX标准基座真机运行测试全屏视频广告位标识(adpid)为:1507000611
全屏视频广告与激励视频广告效果类似,在应用中可以在激励视频广告填充不足时用全屏视频广告来代替。
全屏视频与激励视频广告的差别:

  1. 不支持视频服务器回调校验事件
  2. 全屏视频在播放结束前(通常播放6-10秒)可以关闭,用户关闭视频不会提醒用户无法获取奖励

uni-app项目

使用已经封装成uni API。
详情参考:https://uniapp.dcloud.net.cn/api/a-d/full-screen-video

5+ App(WAP2APP)项目

全屏视频广告由FullScreenVideoAd管理。
操作步骤如下:

  • 创建全屏视频广告对象 plus.ad.createFullScreenVideoAd,需传入在uni-ad平台申请的广告位标识adpid
  • 监听全屏视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
  • 监听全屏视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
  • 监听全屏视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告
  • 加载激励视频广告:调用广告对象load

示例如下:

//视全屏激励广告  
var adFull = null;  
function fullVideoAd(){  
  if(adFull){  
    outLine('正在加载全屏视频广告');  
    return;  
  }  
  console.log('#全屏视频广告#');  
  adFull = plus.ad.createFullScreenVideoAd({adpid:'1507000611'});  // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效  
  adFull.onLoad(function(){  
    console.log('加载成功')  
    adFull.show();  
  });  
  adFull.onError(function(e){  
    console.log('加载失败: '+JSON.stringify(e));  
    adFull.destroy();  
    adFull = null;  
  });  
  adFull.onClose(function(e){  
    if(e.isEnded){  
      console.log('全屏视频播放完成');  
      plus.nativeUI.toast('全屏视频播放完成');  
    }else{  
      console.log('全屏视频未播放完成关闭!')  
    }  
    adFull.destroy();  
    adFull = null;  
  });  
  adFull.load();  
}

沉浸视频流广告

也称为Draw视频信息流广告

沉浸视频流广告为媒体提供了竖屏视频信息流广告样式,适合在全屏的竖屏视频中使用。目前仅提供了nvue的方式使用,使用方式可以参考文档:https://uniapp.dcloud.net.cn/component/ad-draw

HBuilderX3.0.0及以上版本开始支持。

内容联盟

内容联盟提供了单独的接口接入视频流供接入方接入。是一种支持用户上下滑动的切换视频的内容形式。

使用步骤:

示例:

        plus.ad.showContentPage({  
            adpid:"1111111112", // 1111111112为测试广告位,使用时请替换成自己的。  
        }, function(e) {  
            console.log("成功")  
        }, function(e) {  
            console.log(JSON.stringify(e))  
        });  

<a id="rewarderror"/>
<a id="videoerror"/>

广告错误码

激励视频及全屏视频广告常见错误及处理建议:

  • "-5001"
    广告位标识adpid为空,请传入有效的adpid
    请到广告平台申请广告位并获取adpid。
  • "-5002"
    无效的广告位标识adpid,请使用正确的adpid
    请到广告平台确认使用的广告位标识adpid是否正确。
  • "-5003"
    未开通广告,请在广告平台申请并确保已审核通过
    当前应用还没有开通广告,或者广告还没有审核通过,请到广告平台查看申请状态。
  • "-5004"
    无广告模块,打包时请配置要使用的广告模块
    云端打包时没有勾选广告平台SDK,请参考前面“配置uni-ad广告模块”方法勾选后重新提交云端打包。
  • "-5005"
    广告加载失败,请稍后重试
    加载视频激励广告失败,返回此错误时建议过一段时间再重新加载一次。
  • "-5006"
    广告未加载完成无法播放,请加载完成后再调show播放
  • "-5007"
    无法获取广告配置数据,请尝试重试
    返回此错误时建议重新加载一次。
  • "-5100"
    其他错误,聚合广告商内部错误。
  • 其他错误码及详细介绍请参考[https://uniapp.dcloud.net.cn/uni-ad/ad-error-code.html]

    本地离线打包

    Android平台参考:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/androidModuleConfig/uniad
    iOS平台参数:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/iOSModuleConfig/uniad

收起阅读 »

用Apple Developer申请苹果开发者账号新方式(支持支付宝微信付款申请)

iOS iOS打包

因为近期苹果开发者账号在网页申请无法付款,所以苹果推出了Apple Developer 应用,支持在苹果手机申请开发者账号并且付款,支付宝微信银行卡都可以付款!

无法在网页申请的可以尝试这个方式去申请,亲测是可以申请成功,陆陆续续也有人申请成功,也有部分反馈说付款不了,具体可以看下面教程直接尝试下!

相关说明

1、需要更新到12.4以上,并且手机开通了密码或者指纹解锁

2、之前在网页提交申请但无法付款的apple id无法使用,需要新注册的开好双重未提交申请的苹果账号。

3、需要身份证号、人脸识别、绑定支付信息

4、付款成功后账号无需审核,马上生效使用。

5、以前旧的开发者续费也可以使用这个方式尝试下

详细申请步骤流程请查看

继续阅读 »

因为近期苹果开发者账号在网页申请无法付款,所以苹果推出了Apple Developer 应用,支持在苹果手机申请开发者账号并且付款,支付宝微信银行卡都可以付款!

无法在网页申请的可以尝试这个方式去申请,亲测是可以申请成功,陆陆续续也有人申请成功,也有部分反馈说付款不了,具体可以看下面教程直接尝试下!

相关说明

1、需要更新到12.4以上,并且手机开通了密码或者指纹解锁

2、之前在网页提交申请但无法付款的apple id无法使用,需要新注册的开好双重未提交申请的苹果账号。

3、需要身份证号、人脸识别、绑定支付信息

4、付款成功后账号无需审核,马上生效使用。

5、以前旧的开发者续费也可以使用这个方式尝试下

详细申请步骤流程请查看

收起阅读 »

VUE搭建脚手架Cli入门新手篇

一、那么我们就从最简单的环境搭建开始:

安装node.js,从node.js官网下载并安装node,安装过程很简单,一路“下一步”就可以了(傻瓜式安装)。安装完成之后,打开命令行工具(win+r,然后输入cmd),输入 node -v,如下图,如果出现相应的版本号,则说明安装成功。

这里需要说明下,因为在官网下载安装node.js后,就已经自带npm(包管理工具)了,另需要注意的是npm的版本最好是3.x.x以上,以免对后续产生影响。
安装淘宝镜像,打开命令行工具,把这个(npm install -g cnpm --registry= https://registry.npm.taobao.org)复制(这里要手动复制就是用鼠标右键那个,具体为啥不多解释),安装这里是因为我们用的npm的服务器是外国,有的时候我们安装“依赖”的时候很很慢很慢超级慢,所以就用这个cnpm来安装我们说需要的“依赖”。安装完成之后输入 cnpm -v,如下图,如果出现相应的版本号,则说明安装成功。

安装webpack,打开命令行工具输入:npm install webpack -g,安装完成之后输入 webpack -v,如下图,如果出现相应的版本号,则说明安装成功。

安装vue-cli脚手架构建工具,打开命令行工具输入:npm install vue-cli -g,安装完成之后输入 vue -V(注意这里是大写的“V”),如下图,如果出现相应的版本号,则说明安装成功。

二、通过以上四步,我们需要准备的环境和工具都准备好了,接下来就开始使用vue-cli来构建项目

在硬盘上找一个文件夹放工程用的。这里有两种方式指定到相关目录:①cd 目录路径 ②如果以安装git的,在相关目录右键选择Git Bash Here
安装vue脚手架输入:vue init webpack exprice ,注意这里的“exprice” 是项目的名称可以说是随便的起名,但是需要主要的是“不能用中文”。
$ vue init webpack exprice --------------------- 这个是那个安装vue脚手架的命令
This will install Vue 2.x version of the template. ---------------------这里说明将要创建一个vue 2.x版本的项目
For Vue 1.x use: vue init webpack#1.0 exprice
? Project name (exprice) ---------------------项目名称
? Project name exprice
? Project description (A Vue.js project) ---------------------项目描述
? Project description A Vue.js project
? Author Datura --------------------- 项目创建者
? Author Datura
? Vue build (Use arrow keys)
? Vue build standalone
? Install vue-router? (Y/n) --------------------- 是否安装Vue路由,也就是以后是spa(但页面应用需要的模块)
? Install vue-router? Yes
? Use ESLint to lint your code? (Y/n) n ---------------------是否启用eslint检测规则,这里个人建议选no
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? (Y/n)
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? (Y/n)
? Setup e2e tests with Nightwatch? Yes
vue-cli · Generated "exprice".
To get started: --------------------- 这里说明如何启动这个服务
cd exprice
npm install
npm run dev
如下图:

cd 命令进入创建的工程目录,首先cd exprice(这里是自己建工程的名字);
安装项目依赖:npm install,因为自动构建过程中已存在package.json文件,所以这里直接安装依赖就行。不要从国内镜像cnpm安装(会导致后面缺了很多依赖库),但是但是如果真的安装“个把”小时也没成功那就用:cnpm install 吧
安装 vue 路由模块 vue-router 和网络请求模块 vue-resource,输入:cnpm install vue-router vue-resource --save。
创建完成的“exprice”目录如下:

下面我简单的说明下各个目录都是干嘛的:

启动项目,输入:npm run dev。服务启动成功后浏览器会默认打开一个“欢迎页面”,如下图:

注意:这里是默认服务启动的是本地的8080端口,所以请确保你的8080端口不被别的程序所占用。

继续阅读 »

一、那么我们就从最简单的环境搭建开始:

安装node.js,从node.js官网下载并安装node,安装过程很简单,一路“下一步”就可以了(傻瓜式安装)。安装完成之后,打开命令行工具(win+r,然后输入cmd),输入 node -v,如下图,如果出现相应的版本号,则说明安装成功。

这里需要说明下,因为在官网下载安装node.js后,就已经自带npm(包管理工具)了,另需要注意的是npm的版本最好是3.x.x以上,以免对后续产生影响。
安装淘宝镜像,打开命令行工具,把这个(npm install -g cnpm --registry= https://registry.npm.taobao.org)复制(这里要手动复制就是用鼠标右键那个,具体为啥不多解释),安装这里是因为我们用的npm的服务器是外国,有的时候我们安装“依赖”的时候很很慢很慢超级慢,所以就用这个cnpm来安装我们说需要的“依赖”。安装完成之后输入 cnpm -v,如下图,如果出现相应的版本号,则说明安装成功。

安装webpack,打开命令行工具输入:npm install webpack -g,安装完成之后输入 webpack -v,如下图,如果出现相应的版本号,则说明安装成功。

安装vue-cli脚手架构建工具,打开命令行工具输入:npm install vue-cli -g,安装完成之后输入 vue -V(注意这里是大写的“V”),如下图,如果出现相应的版本号,则说明安装成功。

二、通过以上四步,我们需要准备的环境和工具都准备好了,接下来就开始使用vue-cli来构建项目

在硬盘上找一个文件夹放工程用的。这里有两种方式指定到相关目录:①cd 目录路径 ②如果以安装git的,在相关目录右键选择Git Bash Here
安装vue脚手架输入:vue init webpack exprice ,注意这里的“exprice” 是项目的名称可以说是随便的起名,但是需要主要的是“不能用中文”。
$ vue init webpack exprice --------------------- 这个是那个安装vue脚手架的命令
This will install Vue 2.x version of the template. ---------------------这里说明将要创建一个vue 2.x版本的项目
For Vue 1.x use: vue init webpack#1.0 exprice
? Project name (exprice) ---------------------项目名称
? Project name exprice
? Project description (A Vue.js project) ---------------------项目描述
? Project description A Vue.js project
? Author Datura --------------------- 项目创建者
? Author Datura
? Vue build (Use arrow keys)
? Vue build standalone
? Install vue-router? (Y/n) --------------------- 是否安装Vue路由,也就是以后是spa(但页面应用需要的模块)
? Install vue-router? Yes
? Use ESLint to lint your code? (Y/n) n ---------------------是否启用eslint检测规则,这里个人建议选no
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? (Y/n)
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? (Y/n)
? Setup e2e tests with Nightwatch? Yes
vue-cli · Generated "exprice".
To get started: --------------------- 这里说明如何启动这个服务
cd exprice
npm install
npm run dev
如下图:

cd 命令进入创建的工程目录,首先cd exprice(这里是自己建工程的名字);
安装项目依赖:npm install,因为自动构建过程中已存在package.json文件,所以这里直接安装依赖就行。不要从国内镜像cnpm安装(会导致后面缺了很多依赖库),但是但是如果真的安装“个把”小时也没成功那就用:cnpm install 吧
安装 vue 路由模块 vue-router 和网络请求模块 vue-resource,输入:cnpm install vue-router vue-resource --save。
创建完成的“exprice”目录如下:

下面我简单的说明下各个目录都是干嘛的:

启动项目,输入:npm run dev。服务启动成功后浏览器会默认打开一个“欢迎页面”,如下图:

注意:这里是默认服务启动的是本地的8080端口,所以请确保你的8080端口不被别的程序所占用。

收起阅读 »

iOS tabbar毛玻璃适配

uniapp iOS Android flutter

一直对iOS的毛玻璃效果很热衷,体验也确实不错。Android平台虽然也能实现,但是效果感觉一直没有iOS好,而且用的App好像也不多。
最近在用flutter写地图应用,flutter对毛玻璃支持较好。但是我的应用是一个地图应用,想在地图上面覆盖一层毛玻璃,显示一些图标文字,想想挺漂亮,居然实现不了。估计和地图是platformview有关系。

看到官方在最新的版本中加入了iOS tabbar毛玻璃效果,立马拿过来用一用。
发现一个问题:之前全局设置了安全区域safearea,iPhoneX等刘海机型能很好的适配。
现在为了毛玻璃效果,需要去掉safeara底部适配,这是全局性质的,导致其他页面都没有safeara底部适配了,每个页面都需要单独设置,挺麻烦。

没办法,只能每个页面单独设置了,下面是我的方法,有点麻烦。小伙伴有更好的方法希望能分享一下哦~
最外面的一个container加个padding,如果底部有悬浮按钮,为了避免在按钮下面穿透,需要也加个padding。

<view class="container"></view>  

.container {  
  padding-bottom: 0;  
  padding-bottom: constant(safe-area-inset-bottom);  
  padding-bottom: env(safe-area-inset-bottom);  
}  

# 如果底部有悬浮按钮,需要这样处理,加一个padding  

.bottom-review {  
  background-color: white;  
  position: fixed;  
  bottom: 0;  
  left: 0;  
  right: 0;  
  display: flex;  
  align-items: center;  
  padding: 10px 10px;  
  padding-bottom: constant(safe-area-inset-bottom);  
  padding-bottom: calc(env(safe-area-inset-bottom) + 10px);  
  border-top: #f6f6f6 solid 1px;  
}

这是效果图:

继续阅读 »

一直对iOS的毛玻璃效果很热衷,体验也确实不错。Android平台虽然也能实现,但是效果感觉一直没有iOS好,而且用的App好像也不多。
最近在用flutter写地图应用,flutter对毛玻璃支持较好。但是我的应用是一个地图应用,想在地图上面覆盖一层毛玻璃,显示一些图标文字,想想挺漂亮,居然实现不了。估计和地图是platformview有关系。

看到官方在最新的版本中加入了iOS tabbar毛玻璃效果,立马拿过来用一用。
发现一个问题:之前全局设置了安全区域safearea,iPhoneX等刘海机型能很好的适配。
现在为了毛玻璃效果,需要去掉safeara底部适配,这是全局性质的,导致其他页面都没有safeara底部适配了,每个页面都需要单独设置,挺麻烦。

没办法,只能每个页面单独设置了,下面是我的方法,有点麻烦。小伙伴有更好的方法希望能分享一下哦~
最外面的一个container加个padding,如果底部有悬浮按钮,为了避免在按钮下面穿透,需要也加个padding。

<view class="container"></view>  

.container {  
  padding-bottom: 0;  
  padding-bottom: constant(safe-area-inset-bottom);  
  padding-bottom: env(safe-area-inset-bottom);  
}  

# 如果底部有悬浮按钮,需要这样处理,加一个padding  

.bottom-review {  
  background-color: white;  
  position: fixed;  
  bottom: 0;  
  left: 0;  
  right: 0;  
  display: flex;  
  align-items: center;  
  padding: 10px 10px;  
  padding-bottom: constant(safe-area-inset-bottom);  
  padding-bottom: calc(env(safe-area-inset-bottom) + 10px);  
  border-top: #f6f6f6 solid 1px;  
}

这是效果图:

收起阅读 »

vue多个组件合并到一个页面,组件共

把test1.vue,test2合并到test3中一起显示
test1.vue

<template>  
    <view>  
        test1  
    </view>  
</template>  

<script>  
    export default {  
        data() {  
            return {  

            }  
        },  
        methods: {  

        }  
    }  
</script>  

<style>  

</style>

test2.vue

<template>  
    <view>  
        test2  
    </view>  
</template>  

<script>  
    export default {  
        data() {  
            return {  

            }  
        },  
        methods: {  

        }  
    }  
</script>  

<style>  

</style>

test3.vue

<template>  
    <view>  
        test333       
        <test1></test1>  
        <test2></test2>  
    </view>  

</template>  

<script>  
    import test1 from '../test1/test1'  
    import test2 from '../test2/test2'  
    export default {  
        components: {  
                    test1,  
                    test2  
                },  
        data() {  
            return {  

            }  
        },  
        methods: {  

        }  
    }  
</script>  

<style>  

</style>

继续阅读 »

把test1.vue,test2合并到test3中一起显示
test1.vue

<template>  
    <view>  
        test1  
    </view>  
</template>  

<script>  
    export default {  
        data() {  
            return {  

            }  
        },  
        methods: {  

        }  
    }  
</script>  

<style>  

</style>

test2.vue

<template>  
    <view>  
        test2  
    </view>  
</template>  

<script>  
    export default {  
        data() {  
            return {  

            }  
        },  
        methods: {  

        }  
    }  
</script>  

<style>  

</style>

test3.vue

<template>  
    <view>  
        test333       
        <test1></test1>  
        <test2></test2>  
    </view>  

</template>  

<script>  
    import test1 from '../test1/test1'  
    import test2 from '../test2/test2'  
    export default {  
        components: {  
                    test1,  
                    test2  
                },  
        data() {  
            return {  

            }  
        },  
        methods: {  

        }  
    }  
</script>  

<style>  

</style>

收起阅读 »

ios 获取本地文件失败

mui

<body>
<video onclick="aa()" class='my-video' playsinline="" webkit-playsinline="" autoplay='autoplay' controls='controls' style='height: 300px !important;width: 100%;'>
<source src='file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV' type='video/mp4'></source>
</video>
</body>
<script type="text/javascript">
function aa(){
var filepath = "file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV";
//var filepath = "_doc/filecache/124APPLE/IMG_4156.MOV";
var fpath = decodeURI(filepath);
alert(fpath);
docompress(fpath);
}

function docompress(path) {  
    plus.zip.compress(path,path,function(){  
        alert("success:"+path);  
        uploadVideoZip(path);  
    },function(error) {  
        alert("fail:"+JSON.stringify(error));  
    });  
}  

</script>

[结果]

{"code":-4,"message":"文件不存在"}

继续阅读 »

<body>
<video onclick="aa()" class='my-video' playsinline="" webkit-playsinline="" autoplay='autoplay' controls='controls' style='height: 300px !important;width: 100%;'>
<source src='file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV' type='video/mp4'></source>
</video>
</body>
<script type="text/javascript">
function aa(){
var filepath = "file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV";
//var filepath = "_doc/filecache/124APPLE/IMG_4156.MOV";
var fpath = decodeURI(filepath);
alert(fpath);
docompress(fpath);
}

function docompress(path) {  
    plus.zip.compress(path,path,function(){  
        alert("success:"+path);  
        uploadVideoZip(path);  
    },function(error) {  
        alert("fail:"+JSON.stringify(error));  
    });  
}  

</script>

[结果]

{"code":-4,"message":"文件不存在"}

收起阅读 »

uni微信app支付

微信支付
            //微信app支付 demo   
             var orderinfo = dat.data;  

                uni.requestPayment({  
                    provider:"wxpay",  
                    orderInfo:JSON.stringify(orderinfo),  
                    success:function(res){  
                        uni.showToast({  
                            title:"支付成功",  
                            icon:"success",  
                            duration:2000,  
                            complete:function(){  
                              vm.goback();  

                            }  
                        });  
                    },  
                    fail:function(res){  
                        uni.showToast({  
                            title: '支付失败,请重新支付',  
                            icon: "none",  
                            duration: 2000,  
                        });  
                        console.log(JSON.stringify(res));  

                    }  
                });  

            // 返回数据为  
            "data": {  
                  "code": 1,  
                   "message": "Success",  
                  "timestamp": 1576656961,  
                   "data": {  
                      "package": "Sign=WXPay",  
                      "out_trade_no": "02ci00110000i",  
                      "appid": "*************************",  
                      "sign": "0DC67CB535F781E6DAF5D809281C5725",  
                      "partnerid": "1480005102",  
                      "prepayid": "wx18161601019540994e6f2cf31556221700",  
                      "noncestr": "ce4dda6d083686058663bf27cb58f704",  
                      "timestamp": "1576656961"  
                     }  
             },  

           注:本地测试可能包名有问题 ,建议云打包测试,包名与微信申请的包名填写一致就可以了。  
            
继续阅读 »
            //微信app支付 demo   
             var orderinfo = dat.data;  

                uni.requestPayment({  
                    provider:"wxpay",  
                    orderInfo:JSON.stringify(orderinfo),  
                    success:function(res){  
                        uni.showToast({  
                            title:"支付成功",  
                            icon:"success",  
                            duration:2000,  
                            complete:function(){  
                              vm.goback();  

                            }  
                        });  
                    },  
                    fail:function(res){  
                        uni.showToast({  
                            title: '支付失败,请重新支付',  
                            icon: "none",  
                            duration: 2000,  
                        });  
                        console.log(JSON.stringify(res));  

                    }  
                });  

            // 返回数据为  
            "data": {  
                  "code": 1,  
                   "message": "Success",  
                  "timestamp": 1576656961,  
                   "data": {  
                      "package": "Sign=WXPay",  
                      "out_trade_no": "02ci00110000i",  
                      "appid": "*************************",  
                      "sign": "0DC67CB535F781E6DAF5D809281C5725",  
                      "partnerid": "1480005102",  
                      "prepayid": "wx18161601019540994e6f2cf31556221700",  
                      "noncestr": "ce4dda6d083686058663bf27cb58f704",  
                      "timestamp": "1576656961"  
                     }  
             },  

           注:本地测试可能包名有问题 ,建议云打包测试,包名与微信申请的包名填写一致就可以了。  
            
收起阅读 »

关于HBuilderX自定义字体的说明

HBuilderX 字体

设置字体

点击菜单【工具】【设置】【常用配置】,然后选择相应字体

如何自定义字体?

字体列表,包含了操作系统内所有已安装的字体。

自定义,只能填写操作系统内已有的字体;

如需要自定义,则需要先安装相应字体。

示例

source-code-pro为例。

下载字体后,点击安装,安装成功后就会出现字体列表中。

继续阅读 »

设置字体

点击菜单【工具】【设置】【常用配置】,然后选择相应字体

如何自定义字体?

字体列表,包含了操作系统内所有已安装的字体。

自定义,只能填写操作系统内已有的字体;

如需要自定义,则需要先安装相应字体。

示例

source-code-pro为例。

下载字体后,点击安装,安装成功后就会出现字体列表中。

收起阅读 »