界面卡顿检测

Android 通过从应用生成帧并将其显示在屏幕上来呈现界面。如果您的应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧。发生这种情况时,用户会看到屏幕上不断闪烁,这种情况称为卡顿。

出现卡顿通常是因为界面线程(在大多数应用中是主线程)上存在一些减速或阻塞异步调用。您可以利用系统轨迹找出问题所在。

在 Android 12 及更高版本上检测卡顿情况

对于搭载 Android 12(API 级别 31)或更高版本的设备,CPU 性能分析器中 Display 窗格下的 Janky Frameworks 轨道中会显示捕获的轨迹。

如需检测卡顿情况,请按以下步骤操作:

  1. 在 Android Studio 中,依次选择 View > Tool Windows > Profiler,或点击工具栏中的 Profile 图标 。如果 Select Deployment Target 对话框显示提示,请选择要将您的应用部署到哪个设备以进行性能分析。如果您已通过 USB 连接设备但系统未列出该设备,请确保您已启用 USB 调试
  2. 点击 CPU 时间轴上的任意位置以打开 CPU 性能分析器。
  3. 从 CPU 性能分析器的配置菜单中选择 System Trace,然后点击 Record。完成与应用的交互后,点击 Stop
  4. 您应该会在 Display 下方看到 Janky frames 轨道。默认情况下,性能分析器只会将卡顿帧显示为有待调查的候选对象。在每个卡顿帧中,红色部分突出显示了相应帧超出其渲染截止时间的时长。 卡顿帧轨道的屏幕截图
  5. 发现卡顿帧后,点击该帧;可根据需要按 M 键调整缩放程度以聚焦到所选帧。相关事件会在以下线程中突出显示:主线程、RenderThread 和 GPU completion。 性能分析器的屏幕截图,显示了卡顿帧和主线程
  6. 通过选中或取消选中 All Frames 和 Lifecycle 复选框,您可以根据需要查看所有帧或呈现时间的细分数据。 显示上述内容的性能分析器屏幕截图,但“All Frames”和“Lifecycle”复选框为已选中状态

在 Android 11 上检测卡顿情况

对于搭载 Android 11(API 级别 30)的设备,CPU 性能分析器的 Frame Lifecycle 部分会显示捕获的轨迹。

包含不同轨迹的“Frame Lifecycle”部分

Frame Lifecycle 部分包含层名称和四个轨迹。每个轨迹分别代表帧呈现流水线中的一个阶段。Frame Lifecycle 元素如下:

  1. Frame Lifecycle (层名称):该部分的标题包含用括号括起来的层名称。层是单个组合单元。
  2. Application:此轨迹显示从缓冲区被应用移出队列到重新回到队列的时间。这通常对应于 RenderThread 中的轨迹事件。
  3. Wait for GPU:此轨迹显示 GPU 拥有相应缓冲区的时长。该时长指的是,从相应缓冲区的内容被发送至 GPU,到 GPU 利用相应缓冲区的内容完成其工作,期间所经历的时间。这并不表示 GPU 在此期间仅使用相应缓冲区的内容工作。如需详细了解给定时间内 GPU 执行的工作,您可能需要使用 Android GPU 检查器
  4. Composition:此轨迹显示,从 SurfaceFlinger 占有相应缓冲区并发送相应缓冲区的内容以进行合成,到相应缓冲区的内容被发送到显示屏,期间所经历的时间。
  5. Frames on display:此轨迹显示相应帧在屏幕上的时长。

Frame Lifecycle 部分说明了帧缓冲区在呈现流水线的不同阶段之间的切换方式。帧按帧号进行颜色编码,以便更轻松地跟踪特定帧。

Android Studio 还会在 All Frames 标签页中以表格格式显示轨迹中的所有帧。

“All Frames”标签页内显示轨迹中所有帧的表格

Frame #ApplicationWait for GPU 和 Composition 列表示的数据与上方 Frame Lifecycle 部分的轨迹表示的数据一样。Frame Duration 列表示从 Application 开始到 Frames on Display 开始所经历的时间。这本质上是端到端呈现帧的时长。

您可以按任意列对“Frames”表进行排序,以便快速找到最短或最长的帧。该表还支持分页控件,您可以借助这些控件浏览数以百计的帧。

如需在 Android 11 上检测和调查卡顿情况,请按以下步骤操作:

  1. 按 Application 列对 All Frames 表进行降序排序,使耗时最长的帧首先显示。按降序排序的“Application”列
  2. 找到运行时间最长的帧,然后选择表中的一行。这将在左侧的时间轴视图中放大所选的帧。时间轴视图和“Frames”表
  3. 在 Frame Lifecycle 和 Threads 部分查找相关线程。“Frame Lifecycle”和“Threads”部分

在 Android 10 及更低版本上检测卡顿情况

对于搭载 Android 10(API 级别 29)及更低版本的设备,相关的操作系统图形管道信息会显示在 CPU 性能分析器系统轨迹中的单个部分,称为 Display

“Display”界面窗口
  • Frames:此部分显示应用中的界面线程和 RenderThread 轨迹事件。时长超过 16 毫秒的事件会以红色表示,以突出显示潜在的卡顿帧,因为它们超出了以 60 帧/秒 (fps) 的速度进行呈现的截止时间。
  • SurfaceFlinger:此部分显示 SurfaceFlinger 处理帧缓冲区的时间。SurfaceFlinger 是负责将缓冲区内容发送到显示屏的系统进程。
  • VSYNC:此部分显示 VSYNC,这是一个表示与显示流水线保持同步的信号。该轨迹会显示 VSYNC-app 信号,这个信号会在应用启动时间过晚时显示。通常情况下,发生这种情况是因为界面线程处于忙碌状态。在动画播放期间,它会导致屏幕上出现可见的闪烁,并且在动画或滚动完成之前,会持续带来额外的输入延迟。对于刷新率较高的显示屏,尤其要注意查看该轨迹,因为与刷新率为 60 次/秒或刷新率可变的显示屏相比,这种显示屏更容易出现此类问题。
  • BufferQueue:此部分显示有多少帧缓冲区在排队等待 SurfaceFlinger 使用。对于部署到搭载 Android 9(API 级别 28)或更高版本的设备的应用,此轨迹显示应用 surface BufferQueue 的缓冲区计数(01 或 2)。BufferQueue 可帮助您了解图像缓冲区在 Android 图形组件之间切换时的状态。例如,值 2 表示应用当前处于三重缓冲状态,这会导致额外的输入延迟。

Display 部分会提供有助于检测潜在卡顿的实用信号,例如何时界面线程或 RenderThread 的用时超过 16 毫秒。若要调查导致卡顿的确切细节,您可以查看 Threads 部分,其中会显示与界面呈现有关的线程。

“Display”下方的“Threads”部分

在上图中,Threads 部分显示了界面线程 (java.com.google.samples.apps.iosched)、RenderThread 和 GPU completion 线程。这些线程与界面呈现有关,可能是导致卡顿的原因。

如需在 Android 10 或更低版本上检测卡顿情况,请执行以下操作:

  1. 查看 Display 中的 Frames 轨迹。红色帧是要调查的候选对象。“Display”下方的“Frames”部分
  2. 发现可能存在卡顿的帧后,请按 W,或在按住 Control 键(在 macOS 设备上,则按住 Command 键)的同时滚动鼠标滚轮,以便进行放大。继续放大,直到您看到界面线程和 RenderThread 中的轨迹事件。界面线程和 RenderThread 中的轨迹事件在上图中,Choreographer#doFrame 显示了界面线程何时调用 Choreographer 来协调动画、视图布局、图像绘制和相关进程。DrawFrames 显示了 RenderThread 何时形成并向 GPU 发出实际绘制命令。
  3. 如果您发现某个轨迹事件特别长,可以进一步放大,以便找出可能导致呈现速度缓慢的原因。上图显示了界面线程中的 inflate,这意味着应用正在花时间膨胀布局。当您放大其中一个 inflate 事件时,可以确切了解每个界面组件花费的时间,如下所示。显示界面组件花费的确切时长的菜单

常见的卡顿来源

以下部分介绍了应用中常见的卡顿来源以及解决这些问题的最佳做法。

可滚动列表

ListView 和 RecyclerView (尤其是后者)常用于最易出现卡顿的复杂滚动列表。它们都包含 Systrace 标记,因此您可以使用 Systrace 来判断它们是不是导致应用出现卡顿的因素。请务必传递命令行参数 -a ,以便让 RecyclerView 中的跟踪部分(以及您添加的所有跟踪标记)显示出来。请遵循系统跟踪信息输出中生成的提醒提供的指导(如果有)。在 Systrace 中,您可以点击 RecyclerView 跟踪部分,以查看关于 RecyclerView 正在执行的工作的说明。

RecyclerView:notifyDataSetChanged

如果您在一个帧中看到 RecyclerView 中的每一项都重新绑定(并因此重新布局和重新绘制),请确保您没有调用 notifyDataSetChanged()、setAdapter(Adapter) 或 swapAdapter(Adapter, boolean) 来进行细微更新。这些方法会向系统表明整个列表内容已更改,并会在 Systrace 中显示为 RV FullInvalidate。应改用 SortedList 或 DiffUtil,以便在内容发生更改或添加了内容时生成最少量的更新。

让我们以某个应用为例,该应用可从服务器接收新版本的新闻内容列表。当您将该信息发布到适配器时,可以调用 notifyDataSetChanged(),如下所示:

void onNewDataArrived(List<News> news) {
        myAdapter.setNews(news);
        myAdapter.notifyDataSetChanged();
    }

但这有一个很大的缺点 – 如果是微不足道的更改(可能是单项内容添加到顶部),RecyclerView 将无法检测到这种情况 – 它被告知放弃所有缓存的内容状态,因此需要重新绑定每一项。

使用 DiffUtil 效果会好很多,它会为您计算和派发最少的更新。

void onNewDataArrived(List<News> news) {
        List<News> oldNews = myAdapter.getItems();
        DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
        myAdapter.setNews(news);
        result.dispatchUpdatesTo(myAdapter);
    }

只需将您的 MyCallback 定义为 DiffUtil.Callback 实现,以通知 DiffUtil 如何检查您的列表即可。

RecyclerView:嵌套的 RecyclerView

嵌套 RecyclerView 很常见,对于由水平滚动列表组成的纵向列表(例如 Play 商店主页面上的应用网格),尤其如此。这种方法效果很好,但它也会导致大量来回移动的视图。在首次向下滚动页面时,如果您看到大量内部内容出现扩充,则可能需要检查内部(水平)RecyclerView 之间是否正在共享 RecyclerView.RecycledViewPool。默认情况下,每个 RecyclerView 都将有自己的内容池。然而,在屏幕上同时显示十几个 itemViews 的情况下,如果所有行都显示类型相似的视图,那么当不同的水平列表无法共享 itemViews 时,就会出现问题。

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
        RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

        ...

        @Override
        public void onCreateViewHolder(ViewGroup parent, int viewType) {
            // inflate inner item, find innerRecyclerView by ID…
            LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                    LinearLayoutManager.HORIZONTAL);
            innerRv.setLayoutManager(innerLLM);
            innerRv.setRecycledViewPool(sharedPool);
            return new OuterAdapter.ViewHolder(innerRv);

        }
        ...

如果您希望进一步优化,还可以对内部 RecyclerView 的 LinearLayoutManager 调用 setInitialPrefetchItemCount(int)。例如,如果您始终在某行中显示 3.5 项内容,请调用 innerLLM.setInitialItemPrefetchCount(4);。这将向 RecyclerView 表明,当某个水平行即将显示在屏幕上时,如果界面线程中有空余时间,RecyclerView 应尝试预取该行中的内容。

RecyclerView:膨胀过多/创建过程用时过长

RecyclerView 中的预取功能会在界面线程处于空闲状态时提前执行工作,因此在大多数情况下应该有助于解决膨胀造成的开销问题。如果您在帧中(而不是标记为 RV 预取的部分中)看到了扩充,请确保您是在版本较新的设备上进行测试(预取功能当前仅在 Android 5.0 API 级别 21 及更高版本上受支持),并且使用的是较新版本的支持库。

如果您经常在屏幕上出现新内容时看到导致卡顿的扩充问题,请确认您的视图类型数量没有超出所需要的数量。RecyclerView 内容中的视图类型越少,屏幕上出现新的内容类型时需要进行的扩充就越少。如果可能的话,可以在适当情况下合并视图类型 – 如果不同类型之间只有图标、颜色或文本片段不同,您可以在绑定时进行这项更改,从而避免膨胀(同时减少应用占用的内存)。

如果视图类型看起来合适,请考虑降低膨胀导致的开销。减少不必要的容器和结构视图会有所帮助 – 请考虑使用 ConstraintLayout 构建 itemViews,以便轻松减少结构视图。如果您希望真正进行优化以提升性能,内容的层次结构非常简单,并且您不需要复杂的主题和样式功能,可以考虑自己调用构造函数,但是请注意,通常不值得为此牺牲 XML 的简易性和功能。

RecyclerView:绑定用时过长

绑定(即 onBindViewHolder(VH, int))应该非常简单,并且所有内容(最复杂的内容除外)所需的绑定时间都应远远少于 1 毫秒。它应该只从适配器的内部内容数据获取 POJO 内容,并对 ViewHolder 中的视图调用 setter。如果 RV OnBindView 用时很长,请确认在绑定代码中只执行最少量的工作。

如果您使用简单的 POJO 对象将数据保存在适配器中,可以使用数据绑定库完全避免在 onBindViewHolder 中写入绑定代码。

RecyclerView 或 ListView:布局 / 绘制用时过长

关于绘制和布局方面的问题,请参阅下面有关布局呈现性能的部分。

ListView:扩充

如果不够谨慎,很容易在 ListView 中意外停用回收功能。如果每次有新内容显示到屏幕上时您都会看到扩充,请检查您的 Adapter.getView() 实现是否正在使用、重新绑定并返回 convertView 参数。如果您的 getView() 实现始终会扩充,您的应用将无法在 ListView 中享受到回收的好处。getView() 的结构应该几乎总是与下面的实现类似:

View getView(int position, View convertView, ViewGroup parent) {

        if (convertView == null) {
            // only inflate if no convertView passed
            convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
        }
        // … bind content from position to convertView …
        return convertView;
    }

布局性能

如果 Systrace 表明 Choreographer#doFrame 的布局部分执行的工作过多或者执行工作的频率太高,则意味着您遇到了布局性能问题。应用的布局性能取决于视图层次结构的哪个部分包含会发生改变的布局参数或输入。

布局性能:开销

如果这些部分的用时超过几毫秒,您可能遇到了对 RelativeLayouts 或 weighted-LinearLayouts 来说最糟糕的嵌套性能。这些布局中的每一个都可以触发其子级的多次评测/布局传递,因此嵌套这些布局可能会导致嵌套深度方面出现 O(n^2) 行为。请尝试在层次结构的所有叶节点(最低叶节点除外)中避免使用 RelativeLayout,或避免使用 LinearLayout 的权重功能。您可以采用以下几种方法:

您可以调整结构视图的组织方式。 您可以定义自定义布局逻辑。请参阅优化您的布局指南,查看具体示例。 您可以尝试转换为 ConstraintLayout,该布局提供类似的功能,但不存在性能缺陷。

布局性能:频率

屏幕上出现新内容时,例如当新内容滚动到到 RecyclerView 中的视图上时,应该会进行布局。如果每帧都进行明显布局,则可能是在为布局呈现动画效果,这很可能会导致丢帧。一般来说,动画应以 View 的绘制属性(例如 setTranslationX/Y/Z()、setRotation()、setAlpha() 等等)运行。与布局属性(例如,内边距或外边距)相比,这些属性的更改开销要低得多。更改视图的绘制属性的开销也低得多,通常是调用会触发 invalidate() 的 setter,后跟下一帧中的 draw(Canvas)。这会重新记录已失效视图的绘制操作,并且开销通常也比布局低得多。

呈现性能

Android 界面工作分为两个阶段:界面线程上的 Record View#draw 和 RenderThread 上的 DrawFrame。第一阶段对每个失效的 View 运行draw(Canvas),并可调用自定义视图或代码。第二阶段在原生 RenderThread 上运行,但将根据 Record View#draw 阶段生成的工作运行。

渲染性能:界面线程

如果 Record View#draw 需要很长时间,通常情况下会在界面线程上绘制位图。绘制到位图时使用的是 CPU 呈现,因此通常应尽量避免此操作。结合使用方法跟踪功能和 Android CPU Profiler,看看这是否会带来问题。

当应用希望在显示位图之前对其进行装饰时,通常会执行绘制到位图这一操作。装饰有时候是指像添加圆角这样的操作:

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    // draw a round rect to define shape:
    bitmapCanvas.drawRoundRect(0, 0,
            roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
    // multiply content on top, to make it rounded
    bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
    bitmapCanvas.setBitmap(null);
    // now roundedOutputBitmap has sourceBitmap inside, but as a circle

如果您正在界面线程上执行此类工作,则可以转到后台的解码线程上执行。在某些类似的情况下,您甚至可以在绘制时执行该工作,因此,如果您的 Drawable 或 View 代码如下所示:

void setBitmap(Bitmap bitmap) {
        mBitmap = bitmap;
        invalidate();
    }

    void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBitmap, null, paint);
    }

您可以将其替换为以下代码:

void setBitmap(Bitmap bitmap) {
        shaderPaint.setShader(
                new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
        invalidate();
    }

    void onDraw(Canvas canvas) {
        canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
    }

请注意,这种操作通常也可以用于背景保护(在位图前绘制渐变)和图片过滤(使用 ColorMatrixColorFilter),这是用于修改位图的另外两种常见操作。

如果要出于其他原因而绘制到位图(可能是将其用作缓存),请尝试直接绘制到传递至视图或 Drawable 的硬件加速画布;如果需要,请考虑调用带有 LAYER_TYPE_HARDWARE 的 setLayerType() 来缓存复杂的呈现输出,并仍然充分利用 GPU 呈现功能。

呈现性能:RenderThread

有些画布操作虽然记录开销很低,但会在 RenderThread 上触发开销非常大的计算。Systrace 通常会通过提醒来调用这些操作。

Canvas.saveLayer()

避免 Canvas.saveLayer() – 它可能会触发以开销非常大且未缓存的屏幕外方式呈现每帧。虽然 Android 6.0 中的性能得到了提升(进行了优化以避免 GPU 上的呈现目标切换),但仍然最好尽可能避免使用这个开销非常大的 API,或者至少确保传递 Canvas.CLIP_TO_LAYER_SAVE_FLAG(或调用不带标记的变体)。

为大型路径添加动画效果

对传递至视图的硬件加速画布调用 Canvas.drawPath() 时,Android 会首先在 CPU 上绘制这些路径,然后将它们上传到 GPU。如果路径较大,请避免逐帧修改,以便高效地对其进行缓存和绘制。drawPoints()、drawLines() 和 drawRect/Circle/Oval/RoundRect() 的效率更高 – 即使您最终使用了更多绘制调用,也最好使用它们。

Canvas.clipPath

clipPath(Path) 会触发开销非常大的裁剪行为,因此通常应避免使用它。如果可能,请选择使用绘制形状,而不是裁剪为非矩形。它的效果更好,并支持抗锯齿功能。例如,以下 clipPath 调用:

canvas.save();
    canvas.clipPath(circlePath);
    canvas.drawBitmap(bitmap, 0f, 0f, paint);
    canvas.restore();

可改为表示为:

// one time init:
    paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    // at draw time:
    canvas.drawPath(circlePath, mPaint);

位图上传

Android 会将位图显示为 OpenGL 纹理,并且当位图第一次显示在帧中时,它会上传到 GPU。您可以在 Systrace 中看到此操作显示为上传“宽 x 高”纹理。这可能需要几毫秒的时间(见图 2),但必须使用 GPU 显示图片。

如果这些操作用时较长,请首先检查跟踪信息中的宽度和高度数据。请确保显示的位图不会明显大于其在屏幕上的显示区域,否则会浪费上传时间和内存。通常,位图加载库会提供一些简易的方法来请求大小适当的位图。

在 Android 7.0 中,位图加载代码(通常由库完成)可以调用 prepareToDraw(),以便在需要用到它之前便触发上传。这样,上传操作会在 RenderThread 处于空闲状态时提前进行。只要您知道位图,就可以在解码之后或将位图绑定到视图时执行此操作。理想情况下,您的位图加载库会为您执行此操作,但如果您要自行管理,或者想要确保在更高版本的设备上不会触发上传,则可以在自己的代码中调用 prepareToDraw()。

线程调度延迟

线程调度程序在 Android 操作系统中负责确定系统中的哪些线程应该运行、何时运行以及运行多长时间。有时,出现卡顿是因为应用的界面线程处于阻塞或未运行状态。Systrace 使用不同的颜色(见图 3)来指明线程何时处于休眠状态(灰色)、可运行(蓝色:可以运行,但调度程序尚未选择让它运行)、正在运行(绿色)或处于不可中断休眠状态(红色或橙色)。这对于调试由线程调度延迟引起的卡顿问题非常有用。

注意:更低版本的 Android 会更频繁地遇到不是应用错误导致的调度问题。目前这一方面得到了持续改进,因此请考虑更多地在较新的操作系统版本上调试线程调度问题,因为在这些版本上,未调度的线程更有可能是应用错误导致的。

突出显示界面线程处于休眠状态的时间段。

注意:对于帧的某些部分,界面线程或 RenderThread 预计不会运行。例如,在 RenderThread 的 syncFrameState 正在运行并且位图已上传时,界面线程会处于阻塞状态,这是为了 RenderThread 可以安全地复制界面线程使用的数据。另一个例子是,RenderThread 在使用 IPC 执行下述操作时可能会处于阻塞状态:在帧的开头获取缓冲区,从中查询信息,或者通过 eglSwapBuffers 将缓冲区信息传回给合成器。

应用执行过程中的长时间停顿通常是由 binder 调用(Android 上的进程间通信 (IPC) 机制)引起的。在较新的 Android 版本中,这是导致界面线程停止运行的最常见原因之一。一般来说,解决方法是避免调用进行 binder 调用的函数;如果不可避免,则应该缓存相应值,或将工作转移到后台线程。随着代码库规模越来越大,当您调用一些低级别方法时,很容易会因为不小心而意外添加 binder 调用,但同样很容易通过跟踪找到并修复它们。

如果您有 binder 事务,则可以使用以下 adb 命令捕获其调用堆栈:

$ adb shell am trace-ipc start
    … use the app - scroll/animate ...
    $ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
    $ adb pull /data/local/tmp/ipc-trace.txt

有时看似无害的调用(如 getRefreshRate())可能会触发 binder 事务,如果频繁调用这些事务,还会引发严重问题。定期进行跟踪有助于您在这些问题出现时快速发现并解决它们。

对象分配和垃圾收集

自从 ART 在 Android 5.0 中作为默认运行时引入后,对象分配和垃圾回收 (GC) 问题已显著缓解,但这项额外的工作仍有可能加重线程的负担。您可以针对每秒不会发生多次的罕见事件(例如用户点按一个按钮)进行分配,但请记住,每次分配都会产生开销。如果它处于被频繁调用的紧密循环中,请考虑避免分配以减轻 GC 上的负载。

Systrace 会显示 GC 是否频繁运行,而 Android Memory Profiler 可显示分配来源。如果尽可能避免分配(尤其是在紧密循环中),则应该不会遇到问题。

Android Systrace 基础知识(9)-MainThread 和 RenderThread

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/21913,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?