Android -- View

简介:

setContentView                                                                       

只要你使用过Activity,那么你一定使用过setContentView这个方法。一般都是这样调用该方法:

setContentView(R.layout.main);

然后,在手机或者模拟器上就可以看见自己的布局。

如果,你留意的话,setContentView还有很多过载方法:

复制代码
public void setContentView(int layoutResID) {  
    getWindow().setContentView(layoutResID);  
}  
  
public void setContentView(View view) {  
    getWindow().setContentView(view);  
}  
  
public void setContentView(View view, ViewGroup.LayoutParams params) {  
    getWindow().setContentView(view, params);  
}
复制代码

那么,getWindow()方法是做什么的呢?一探究竟:

public Window getWindow() {  
        return mWindow;  
}

可以看出,该方法返回一个Window实例。但是Window是一个抽象类啊,怎么可以有实例对象???原来,Window类有一个子类PhoneWindow,那么如何得知getWindow返回的是PhoneWindow实例呢?来,看下面这张图:

至此,您应该明白setContentView()方法是调用PhoneWindow类的同名方法。源码如下:

复制代码
@Override  
    public void setContentView(int layoutResID) {  
        if (mContentParent == null) {  
            installDecor();  
        } else {  
            mContentParent.removeAllViews();  
        }  
        mLayoutInflater.inflate(layoutResID, mContentParent);  
        final Callback cb = getCallback();  
        if (cb != null) {  
            cb.onContentChanged();  
        }  
    }  
  
    @Override  
    public void setContentView(View view) {  
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));  
    }  
  
    @Override  
    public void setContentView(View view, ViewGroup.LayoutParams params) {  
        if (mContentParent == null) {  
            installDecor();  
        } else {  
            mContentParent.removeAllViews();  
        }  
        mContentParent.addView(view, params);  
        final Callback cb = getCallback();  
        if (cb != null) {  
            cb.onContentChanged();  
        }  
    }
复制代码

每个Activity都会实例化一个Window并且只有一个,而View就像是贴在Window上的装饰品。窗户(Window)只有一个,但是窗花(View)可以有很多。

LayoutInflater                                                                        

获得 LayoutInflater 实例

复制代码
LayoutInflater inflater = getLayoutInflater();

LayoutInflater localinflater =(LayoutInflater)context.getSystemServie
(Context.LAYOUT_INFLATER_SERVICE); 

LayoutInflater inflater = LayoutInflater.from(context);
复制代码

对于第一种,主要是调用 Activity 的 getLayoutInflater() 方法。

继续跟踪研究 android 源码,Activity 中的该方法是调用 PhoneWindow 的 getLayoutInflater()方法!

public PhoneWindow(Context context) {
        super(context);
        mLayoutInflater = LayoutInflater.from(context);
}

可以看出它其实是调用 LayoutInflater.from(context), 那么该方法其实是调用第二种方法,看看源码,如下:

复制代码
/**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }
复制代码

inflate 方法

inflate 原意是充气之类的,在这里主要意思就是,扩张、使之膨胀。换句话说就是将当前视图view补充完整、扩展该视图。

复制代码
public View inflate (int resource, ViewGroup root)

public View inflate (XmlPullParser parser, ViewGroup root)

public View inflate (XmlPullParser parser, ViewGroup root, boolean attachToRoot)

public View inflate (int resource, ViewGroup root, boolean attachToRoot)
复制代码

示例代码:

复制代码
LayoutInflater inflater = (LayoutInflater)
getSystemService(LAYOUT_INFLATER_SERVICE);

/* R.id.test 是 custom.xml 中根(root)布局 LinearLayout 的 id */
View view = inflater.inflate(R.layout.custom,
(ViewGroup)findViewById(R.id.test));

/* 通过该 view 实例化 EditText对象, 否则报错,因为当前视图不是custom.xml.
即没有 setContentView(R.layout.custom) 或者 addView() */
//EditText editText = (EditText)findViewById(R.id.content);// error
EditText editText = (EditText)view.findViewById(R.id.content);
复制代码

对于上面代码,指定了第二个参数 ViewGroup root,当然你也可以设置为 null 值。

注意:该方法与 findViewById 方法不同。

inflater 是用来找 layout 下 xml 布局文件,并且实例化!而 findViewById() 是找具体 xml 下的具体 widget 控件(如: Button,TextView 等)。

postInvalidate()   (参考)                                                      

在子线程中控制UI:

复制代码
@Override  
    protected void onRestart() {  
        super.onRestart();  
         /*onRestart中开启新线程,更新UI*/  
        Thread thread = new Thread(new Runnable() {  
              
            @Override  
            public void run() {  
                System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
                tv.postInvalidate();  
                btn.postInvalidate();  
                tv.setText("update UI is success!");  
                btn.setText("update UI is success!");  
            }});  
        thread.start();  
    }
复制代码

postInvalidate() 方法,源码:

复制代码
public void postInvalidate() {  
        postInvalidateDelayed(0);  
    }  
public void postInvalidateDelayed(long delayMilliseconds) {  
        // We try only with the AttachInfo because there's no point in invalidating  
        // if we are not attached to our window  
        if (mAttachInfo != null) {  
            Message msg = Message.obtain();  
            msg.what = AttachInfo.INVALIDATE_MSG;  
            msg.obj = this;  
            mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);  
        }  
    }
复制代码

其实,是调用了 Handler 的处理消息的机制!该方法可以在子线程中直接用来更新UI。但是在 Button 的事件中开启线程,更新 UI就会报错报异常。

Handler 和 invalidate 方法结合多线程更新 UI                              

方法 invalidate 主要用在主线程中(即UI 线程中),不可以用于子线程如果在子线程中需要使用 postInvalidate 方法。

复制代码
public void invalidate() {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);  
        }  
        if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {  
            mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;  
            final ViewParent p = mParent;  
            final AttachInfo ai = mAttachInfo;  
            if (p != null && ai != null) {  
                final Rect r = ai.mTmpInvalRect;  
                r.set(0, 0, mRight - mLeft, mBottom - mTop);  
                // Don't call invalidate -- we don't want to internally scroll  
                // our own bounds  
                p.invalidateChild(this, r);  
            }  
        }  
    }
复制代码

invalidate 方法如果你直接在主线程中调用,是看不到任何更新的。需要与Handler结合!

Android 在 onDraw 事件处理绘图,而 invalidate() 函数可以再一次触发 onDraw 事件,然后再一次进行绘图动作。

复制代码
public class MasterActivity extends Activity {  
    static int times = 1;  
   
    /** Called when the activity is first created. */  
    @Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
   
        setContentView( new View(null){  
   
            Paint vPaint = new Paint();  //绘制样式物件  
            private int i = 0;           //弧形角度  
   
            @Override  
            protected void onDraw (Canvas canvas) {  
                super.onDraw(canvas);  
                System.out.println("this run " + (times++) +" times!");  
   
                // 设定绘图样式  
                vPaint.setColor( 0xff00ffff ); //画笔颜色  
                vPaint.setAntiAlias( true );   //反锯齿  
                vPaint.setStyle( Paint.Style.STROKE );  
   
                // 绘制一个弧形  
                canvas.drawArc(new RectF(60, 120, 260, 320), 0, i, true, vPaint );  
   
                // 弧形角度  
                if( (i+=10) > 360 )  
                    i = 0;  
   
                // 重绘, 再一次执行onDraw 程序  
                invalidate();  
            }  
        });  
    }  
}
复制代码

经过测试,发现 times 一直在++,说明 onDraw 被多次调用,并且一直在画图!

注释掉的话:

// 重绘, 再一次执行onDraw 程序  
            //invalidate();

可以看出,图像只画了一条线,说明onDraw()方法被调用一次。从log上也可以看出来:

D/mark    (  221): this run onDraw() 1 times!

那么,是什么力量促使onDraw()方法被调用呢?

setContentView()View view方法,其实是调用PhoneWindow的setContentView(View view)方法,调用关系如下:

从而可以看出,invalidate()方法是促使onDraw()方法被调用的力量。

那么,修改代码,将内部类MyView的onDraw()方法中的invalidate()注释取消,再看看运行效果:

控制台:

复制代码
D/mark    (  248): this run onDraw() 5629 times!  
D/mark    (  248): this run onDraw() 5630 times!  
D/mark    (  248): this run onDraw() 5631 times!  
D/mark    (  248): this run onDraw() 5632 times!  
D/mark    (  248): this run onDraw() 5633 times!  
D/mark    (  248): this run onDraw() 5634 times!  
D/mark    (  248): this run onDraw() 5635 times!  
D/mark    (  248): this run onDraw() 5636 times!  
D/mark    (  248): this run onDraw() 5637 times!  
D/mark    (  248): this run onDraw() 5638 times!  
D/mark    (  248): this run onDraw() 5639 times!  
D/mark    (  248): this run onDraw() 5640 times!  
D/mark    (  248): this run onDraw() 5641 times!  
D/mark    (  248): this run onDraw() 5642 times!  
D/mark    (  248): this run onDraw() 5643 times!  
D/mark    (  248): this run onDraw() 5644 times!  
D/mark    (  248): this run onDraw() 5645 times!  
D/mark    (  248): this run onDraw() 5646 times!
复制代码

可以看出,invalidate()方法使onDraw()一直被调用,实现重绘的效果。

在invalidate()方法源码中,有这么一段注释:

/**  
    * Invalidate the whole view. If the view is visible, {@link #onDraw} will  
    * be called at some point in the future. This must be called from a  
    * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.  
    */

这段话,说明了上面的实现(调用onDraw()方法)。但是在子线程中必须使用postInvalidate()方法。

invalidate()源码分析                                                                

复制代码
public class ViewDrawTestActivity extends Activity {  
    // 用于测试  
    static int times = 1;  
  
    @Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        MyView mView = new MyView(this);  
        mView.invalidate();  
        //setContentView(mView);  
    }  
  
    /** 
     * 内部类,继承View 
     *  
     * @author mark 
     */  
    class MyView extends View {  
  
        MyView(Context context) {  
            super(context);  
        }  
  
        Paint vPaint = new Paint(); // 绘制样式物件  
        int i = 0; // 弧形角度  
  
        @Override  
        protected void onDraw(Canvas canvas) {  
            super.onDraw(canvas);  
            Log.d("mark", "this run onDraw() " + (times++) + " times!");  
            // 设定绘图样式  
            vPaint.setColor(0xff00ffff); // 画笔颜色  
            vPaint.setAntiAlias(true); // 反锯齿  
            vPaint.setStyle(Paint.Style.STROKE);  
            // 绘制一个弧形  
            canvas.drawArc(new RectF(60, 120, 260, 320), 0, i, true, vPaint);  
            // 弧形角度  
            if ((i += 10) > 360) {  
                i = 0;  
            }  
            // 重绘, 再一次执行onDraw 程序  
            // invalidate();  
        }  
    }  
}
复制代码

子没有多大的变化,只是在onCreate()方法中直接调用invalidate()方法,如:

mView.invalidate();

这样做的目的主要是想看看,自己调用View的invalidate()方法会不会触发onDraw()方法。运行一下:

nDraw()方法并没有执行!那么是不是因为没有调用setContentVIew()方法呢?修改onCreate()方法:

复制代码
@Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        MyView mView = new MyView(this);  
        mView.invalidate();  
        setContentView(mView);  
        mView.invalidate();  
    }
复制代码

再次运行,效果:

D/mark    (  251): this run onDraw() 1 times!

说明,只有setContentVIew()方法中的invalidate()方法启了作用,自己调用View的invalidate()方法,mView.invalidate()没启任何作用。但是,在MyView的onDraw()方法中调用invalidate()方法可以循环调用onDraw()方法,类似递归。

分析一下,invalidate()方法的源码吧,在这里也许可以找到答案。

复制代码
/** 
 * Invalidate the whole view. If the view is visible, {@link #onDraw} will 
 * be called at some point in the future. This must be called from a 
 * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}. 
 */  
public void invalidate() {  
    if (ViewDebug.TRACE_HIERARCHY) {  
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);  
    }  
  
    if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {  
        mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;  
        final ViewParent p = mParent;  
        final AttachInfo ai = mAttachInfo;  
        if (p != null && ai != null) {  
            final Rect r = ai.mTmpInvalRect;  
            r.set(0, 0, mRight - mLeft, mBottom - mTop);  
            // Don't call invalidate -- we don't want to internally scroll  
            // our own bounds  
            p.invalidateChild(this, r);  
        }  
    }  
}
复制代码

这里可以看到p.invalidateChild(this, r)(看源码只看关键部分,不然你会很晕!),其中p是ViewParent实例对象。ViewParent是一个接口,现在我们关心谁实现了这个接口?

通过千辛万苦的search,终于找到ViewParent的实现类ViewRoot:

复制代码
/** 
 * The top of a view hierarchy, implementing the needed protocol between View 
 * and the WindowManager.  This is for the most part an internal implementation 
 * detail of {@link WindowManagerImpl}. 
 * 
 * {@hide} 
 */  
@SuppressWarnings({"EmptyCatchBlock"})  
public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks { }
复制代码

那么,看看该类实现的invalidateChild()方法:

复制代码
public void invalidateChild(View child, Rect dirty) {  
        checkThread();  
        if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);  
        if (mCurScrollY != 0 || mTranslator != null) {  
            mTempRect.set(dirty);  
            dirty = mTempRect;  
            if (mCurScrollY != 0) {  
               dirty.offset(0, -mCurScrollY);  
            }  
            if (mTranslator != null) {  
                mTranslator.translateRectInAppWindowToScreen(dirty);  
            }  
            if (mAttachInfo.mScalingRequired) {  
                dirty.inset(-1, -1);  
            }  
        }  
        mDirty.union(dirty);  
        if (!mWillDrawSoon) {  
            scheduleTraversals();  
        }  
    }
复制代码

关键代码在这儿:

if (!mWillDrawSoon) {  
            scheduleTraversals();  
}

这个方法是向Handler发送消息:

复制代码
public void scheduleTraversals() {  
        if (!mTraversalScheduled) {  
            mTraversalScheduled = true;  
            sendEmptyMessage(DO_TRAVERSAL);  
        }  
}
复制代码

接下来,看看ViewRoot的Handler的handleMessage的实现:

复制代码
public void handleMessage(Message msg) {  
    switch (msg.what) {  
    // 、、、  
    case DO_TRAVERSAL:  
    // 、、、  
         performTraversals();  
    }  
}
复制代码

performTraversals()方法,调用ViewRoot的私有方法private void draw(boolean fullRedrawNeeded),在该方法中有句代码很关键:

mView.draw(canvas);

其实这句代码,就是调用View的draw()方法 ,关键代码:

if (!dirtyOpaque) onDraw(canvas);

也就是说,满足这个方法,就会回调onDraw()方法。到此为止,您应该明白,当我们自己调用invalidate()方法时,想使onDraw()方法回调,必须满足条件。

调用关系,请看草图!

我是天王盖地虎的分割线                                    




本文转自我爱物联网博客园博客,原文链接:http://www.cnblogs.com/yydcdut/p/3851955.html,如需转载请自行联系原作者

相关文章
|
4月前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
158 0
|
3月前
|
XML 前端开发 Android开发
Android:UI:Drawable:View/ImageView与Drawable
通过本文的介绍,我们详细探讨了Android中Drawable、View和ImageView的使用方法及其相互关系。Drawable作为图像和图形的抽象表示,提供了丰富的子类和自定义能力,使得开发者能够灵活地实现各种UI效果。View和ImageView则通过使用Drawable实现了各种图像和图形的显示需求。希望本文能为您在Android开发中使用Drawable提供有价值的参考和指导。
65 2
|
3月前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
72 5
|
4月前
|
缓存 数据处理 Android开发
在 Android 中使用 RxJava 更新 View
【10月更文挑战第20天】使用 RxJava 来更新 View 可以提供更优雅、更高效的解决方案。通过合理地运用操作符和订阅机制,我们能够轻松地处理异步数据并在主线程中进行 View 的更新。在实际应用中,需要根据具体情况进行灵活运用,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在 Android 中使用 RxJava 更新 View 的技巧和方法,为开发高质量的 Android 应用提供有力支持。
|
4月前
|
缓存 调度 Android开发
Android 在子线程更新 View
【10月更文挑战第21天】在 Android 开发中,虽然不能直接在子线程更新 View,但通过使用 Handler、AsyncTask 或 RxJava 等方法,可以实现子线程操作并在主线程更新 View 的目的。在实际应用中,需要根据具体情况选择合适的方法,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在子线程更新 View 的技巧和方法,为开发高质量的 Android 应用提供支持。
76 2
|
4月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
4月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
43 2
|
4月前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
82 3
|
4月前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
|
5月前
|
Android开发
Android中SurfaceView的双缓冲机制和普通View叠加问题解决办法
本文介绍了 Android 平台上的 SurfaceView,这是一种高效的图形渲染控件,尤其适用于视频播放、游戏和图形动画等场景。文章详细解释了其双缓冲机制,该机制通过前后缓冲区交换来减少图像闪烁,提升视觉体验。然而,SurfaceView 与普通 View 叠加时可能存在 Z-Order 不一致、同步问题及混合渲染难题。文中提供了使用 TextureView、调整 Z-Order 和创建自定义组合控件等多种解决方案。
257 9

热门文章

最新文章

  • 1
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 2
    Android历史版本与APK文件结构
  • 3
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
  • 4
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 5
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 6
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 7
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 8
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 9
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 10
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
  • 1
    Android实战经验之Kotlin中快速实现MVI架构
    18
  • 2
    即时通讯安全篇(一):正确地理解和使用Android端加密算法
    22
  • 3
    escrcpy:【技术党必看】Android开发,Escrcpy 让你无线投屏新体验!图形界面掌控 Android,30-120fps 超流畅!🔥
    39
  • 4
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
    113
  • 5
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    40
  • 6
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    54
  • 7
    Android历史版本与APK文件结构
    147
  • 8
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    46
  • 9
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    39
  • 10
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
    66