Android 边播放边缓存视频框架——AndroidVideoCache简析

作者:xiangzhihong
转载地址:https://juejin.cn/post/7133182104447221791

一、背景

现在的移动应用,视频是一个非常重要的组成部分,好像里面不搞一点视频就不是一个正常的移动App。在视频开发方面,可以分为视频录制和视频播放,视频录制的场景可能还比较少,这方面可以使用Google开源的 grafika。相比于视频录制,视频播放可以选择的方案就要多许多,比如Google的 ExoPlayer,B站的 ijkplayer,以及官方的MediaPlayer。

不过,我们今天要讲的是视频的缓存。最近,由于我们在开发视频方面没有考虑视频的缓存问题,造成了流量的浪费,然后遭到用户的投诉。在视频播放中,一般有两种两种策略:先下载再播放和边播放边缓存。

通常,为了提高用户的体验,我们会选择边播放边缓存的策略,不过市面上大多数的播放器都是只支持视频播放,在视频缓存这块基本上没啥好的方案,比如我们的App使用的是一个自己封装的库,类似于PlayerBase。PlayerBase是一种将解码器和播放视图组件化处理的解决方案框架,也即是一个对ExoPlayer、ijkplayer的包装库。

二、PlayerBase

PlayerBase是一种将解码器和播放视图组件化处理的解决方案框架。您需要什么解码器实现框架定义的抽象引入即可,对于视图,无论是播放器内的控制视图还是业务视图,均可以做到组件化处理。并且,它支持视频跨页面无缝衔接的效果,也是我们选择它的一个原因。

PlayerBase的使用也比较简单,使用的时候需要单独的添加解码器,具体使用哪种解码器,可以根据项目的需要自由的进行配置。

只使用MediaPlayer:

dependencies {
  //该依赖仅包含MediaPlayer解码
  implementation 'com.kk.taurus.playerbase:playerbase:3.4.2'
}

使用ExoPlayer + MediaPlayer

dependencies {
  //该依赖包含exoplayer解码和MediaPlayer解码
  //注意exoplayer的最小支持SDK版本为16
  implementation 'cn.jiajunhui:exoplayer:342_2132_019'
}

使用ijkplayer + MediaPlayer

dependencies {

  //该依赖包含ijkplayer解码和MediaPlayer解码
  implementation 'cn.jiajunhui:ijkplayer:342_088_012'
  //ijk官方的解码库依赖,较少格式版本且不支持HTTPS。
  implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
  # Other ABIs: optional
  implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
  implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
  implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
  implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'

}

使用ijkplayer + ExoPlayer + MediaPlayer

dependencies {

  //该依赖包含exoplayer解码和MediaPlayer解码
  //注意exoplayer的最小支持SDK版本为16
  implementation 'cn.jiajunhui:exoplayer:342_2132_019'

  //该依赖包含ijkplayer解码和MediaPlayer解码
  implementation 'cn.jiajunhui:ijkplayer:342_088_012'
  //ijk官方的解码库依赖,较少格式版本且不支持HTTPS。
  implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
  # Other ABIs: optional
  implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
  implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
  implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
  implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'

}

最后,在进行代码混淆时,还需要在proguard中添加如下混淆规则。

-keep public class * extends android.view.View{*;}

-keep public class * implements com.kk.taurus.playerbase.player.IPlayer{*;}

添加完解码器之后,接下来只需要在应用的Application中初始化解码器,然后就可以使用了。

public class App extends Application {

    @Override
    public void onCreate() {
        //...

        //如果您想使用默认的网络状态事件生产者,请添加此行配置。
        //并需要添加权限 android.permission.ACCESS_NETWORK_STATE
        PlayerConfig.setUseDefaultNetworkEventProducer(true);
        //初始化库
        PlayerLibrary.init(this);

        //如果添加了'cn.jiajunhui:exoplayer:xxxx'该依赖
        ExoMediaPlayer.init(this);

        //如果添加了'cn.jiajunhui:ijkplayer:xxxx'该依赖
        IjkPlayer.init(this);

        //播放记录的配置
        //开启播放记录
        PlayerConfig.playRecord(true);
        PlayRecordManager.setRecordConfig(
                        new PlayRecordManager.RecordConfig.Builder()
                                .setMaxRecordCount(100)
                                //.setRecordKeyProvider()
                                //.setOnRecordCallBack()
                                .build());

    }

}

然后,在业务代码中开始播放即可。

ListPlayer.get().play(DataSource(url))

不过,有一个缺点是,PlayerBase并没有提供缓存方案,即播放过的视频再次播放的时候还是会消耗流量,这就违背了我们的设计初衷,那有没有一种可以支持缓存,同时对PlayerBase侵入性比较小的方案呢?答案是有的,那就是AndroidVideoCache

三、AndroidVideoCache

3.1 基本原理

AndroidVideoCache 通过代理的策略实现一个中间层,然后我们的网络请求会转移到本地实现的代理服务器上,这样我们真正请求的数据就会被代理拿到,接着代理一边向本地写入数据,一边根据我们需要的数据看是读网络数据还是读本地缓存数据,从而实现数据的复用。

经过实际测试,我发现它的流程如下:首次使用时使用的是网络的数据,后面再次使用相同的视频时就会读取本地的。由于,AndroidVideoCache可以配置缓存文件的大小,所以,再加载视频前,它会重复前面的策略,工作原理图如下。

3.2 基本使用

和其他的插件使用流程一样,首先需要我们在项目中添加AndroidVideoCache依赖。

dependencies {
    compile 'com.danikula:videocache:2.7.1'
}

然后,在全局初始化一个本地代理服务器,我们选择在Application的实现类中进行全局初始化。

public class App extends Application {

    private HttpProxyCacheServer proxy;

    public static HttpProxyCacheServer getProxy(Context context) {
        App app = (App) context.getApplicationContext();
        return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
    }

    private HttpProxyCacheServer newProxy() {
        return new HttpProxyCacheServer(this);
    }
}

当然,初始化的代码也可以写到其他的地方,比如我们的公共Module。有了代理服务器之后,我们在使用的地方把网络视频url替换成下面的方式。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

    HttpProxyCacheServer proxy = getProxy();
    String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
    videoView.setVideoPath(proxyUrl);
}

当然,AndroidVideoCache还提供了很多的自定义规则,比如缓存文件的大小、文件的个数,以及缓存位置等。

private HttpProxyCacheServer newProxy() {
    return new HttpProxyCacheServer.Builder(this)
            .maxCacheSize(1024 * 1024 * 1024)       
            .build();
}

private HttpProxyCacheServer newProxy() {
    return new HttpProxyCacheServer.Builder(this)
            .maxCacheFilesCount(20)
            .build();
}

 private HttpProxyCacheServer newProxy() {
        return new HttpProxyCacheServer.Builder(this)
                .cacheDirectory(getVideoFile())
                .maxCacheSize(512 * 1024 * 1024)
                .build();
    }

 /**
* 缓存路径
**/   
 public File getVideoFile() {
        String path = getExternalCacheDir().getPath() + "/video";
        File file = new File(path);
        if (!file.exists()) {
            file.mkdir();
        }
        return file;
    }

当然,我们还可以使用的MD5方式生成一个key作为文件的名称。

public class MyFileNameGenerator implements FileNameGenerator {

    public String generate(String url) {
        Uri uri = Uri.parse(url);
        String videoId = uri.getQueryParameter("videoId");
        return videoId + ".mp4";
    }
}

...
HttpProxyCacheServer proxy = HttpProxyCacheServer.Builder(context)
    .fileNameGenerator(new MyFileNameGenerator())
    .build()

除此之外,AndroidVideoCache还支持添加一个自定义的HeadersInjector,用来在请求时候添加自定义的请求头。

public class UserAgentHeadersInjector implements HeaderInjector {

    @Override
    public Map<String,String> addHeaders(String url) {
        return Maps.newHashMap("User-Agent","Cool app v1.1");
    }
}

private HttpProxyCacheServer newProxy() {
    return new HttpProxyCacheServer.Builder(this)
            .headerInjector(new UserAgentHeadersInjector())
            .build();
}

3.3 源码分析

前面我们说过,AndroidVideoCache 通过代理的策略实现一个中间层,然后再网络请求时通过本地代理服务去实现真正的请求,这样操作的好处是不会产生额外的请求,并且在缓存策略上,AndroidVideoCache使用了LruCache缓存策略算法,不用去手动维护缓存区的大小,真正做到解放双手。首先,我们来看一下HttpProxyCacheServer类。

public class HttpProxyCacheServer {

    private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");
    private static final String PROXY_HOST = "127.0.0.1";

    private final Object clientsLock = new Object();
    private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
    private final Map<String,HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
    private final ServerSocket serverSocket;
    private final int port;
    private final Thread waitConnectionThread;
    private final Config config;
    private final Pinger pinger;

    public HttpProxyCacheServer(Context context) {
        this(new Builder(context).buildConfig());
    }

    private HttpProxyCacheServer(Config config) {
        this.config = checkNotNull(config);
        try {
            InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
            this.serverSocket = new ServerSocket(0,8,inetAddress);
            this.port = serverSocket.getLocalPort();
            IgnoreHostProxySelector.install(PROXY_HOST,port);
            CountDownLatch startSignal = new CountDownLatch(1);
            this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
            this.waitConnectionThread.start();
            startSignal.await(); // freeze thread,wait for server starts
            this.pinger = new Pinger(PROXY_HOST,port);
            LOG.info("Proxy cache server started. Is it alive? " + isAlive());
        } catch (IOException | InterruptedException e) {
            socketProcessor.shutdown();
            throw new IllegalStateException("Error starting local proxy server",e);
        }
    }

  ... 

 public static final class Builder {

        /**
         * Builds new instance of {@link HttpProxyCacheServer}.
         *
         * @return proxy cache. Only single instance should be used across whole app.
         */
        public HttpProxyCacheServer build() {
            Config config = buildConfig();
            return new HttpProxyCacheServer(config);
        }

        private Config buildConfig() {
            return new Config(cacheRoot,fileNameGenerator,diskUsage,sourceInfoStorage,headerInjector);
        }

    }
}

可以看到,构造函数首先使用本地的localhost地址,创建一个 ServerSocket 并随机分配了一个端口,然后通过 getLocalPort 拿到服务器端口,用来和服务器进行通信。接着,创建了一个线程 WaitRequestsRunnable,里面有一个startSignal信号变量。

 @Override
        public void run() {
            startSignal.countDown();
            waitForRequest();
        }

    private void waitForRequest() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                Socket socket = serverSocket.accept();
                LOG.debug("Accept new socket " + socket);
                socketProcessor.submit(new SocketProcessorRunnable(socket));
            }
        } catch (IOException e) {
            onError(new ProxyCacheException("Error during waiting connection",e));
        }
    }

服务器的整个代理的流程是,先构建一个全局的本地代理服务器 ServerSocket,指定一个随机端口,然后新开一个线程,在线程的 run 方法里通过accept() 方法监听服务器socket的入站连接,accept() 方法会一直阻塞,直到有一个客户端尝试建立连接。

有了代码服务器之后,接下来就是客户端的Socket。我们先从代理替换url地方开始看:

    HttpProxyCacheServer proxy = getProxy();
    String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
    videoView.setVideoPath(proxyUrl);

其中,HttpProxyCacheServer 中的 getProxyUrl()方法源码如下。

public String getProxyUrl(String url,boolean allowCachedFileUri) {
        if (allowCachedFileUri && isCached(url)) {
            File cacheFile = getCacheFile(url);
            touchFileSafely(cacheFile);
            return Uri.fromFile(cacheFile).toString();
        }
        return isAlive() ? appendToProxyUrl(url) : url;
    }

可以看到,上面的代码就是AndroidVideoCache的核心的功能:如果本地已经缓存了,就直接使用本地的Uri,并且把时间更新下,因为LruCache是根据文件被访问的时间进行排序的,如果文件没有被缓存那么就调用isAlive() 方法,isAlive()方法会ping一下目标url,确保url是一个有效的。

 private boolean isAlive() {
        return pinger.ping(3,70);   // 70+140+280=max~500ms
    }

如果用户是通过代理访问的话,就会ping不通,这样就还是使用原生的url,最后进入appendToProxyUrl ()方法里面。

    private String appendToProxyUrl(String url) {
        return String.format(Locale.US,"http://%s:%d/%s",PROXY_HOST,port,ProxyCacheUtils.encode(url));
    }

接着,socket会被包裹成一个runnable,发配给线程池。

 socketProcessor.submit(new SocketProcessorRunnable(socket));

private final class SocketProcessorRunnable implements Runnable {

        private final Socket socket;

        public SocketProcessorRunnable(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            processSocket(socket);
        }
    }

    private void processSocket(Socket socket) {
        try {
            GetRequest request = GetRequest.read(socket.getInputStream());
            LOG.debug("Request to cache proxy:" + request);
            String url = ProxyCacheUtils.decode(request.uri);
            if (pinger.isPingRequest(url)) {
                pinger.responseToPing(socket);
            } else {
                HttpProxyCacheServerClients clients = getClients(url);
                clients.processRequest(request,socket);
            }
        } catch (SocketException e) {
            // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
            // So just to prevent log flooding don't log stacktrace
            LOG.debug("Closing socket… Socket is closed by client.");
        } catch (ProxyCacheException | IOException e) {
            onError(new ProxyCacheException("Error processing request",e));
        } finally {
            releaseSocket(socket);
            LOG.debug("Opened connections: " + getClientsCount());
        }
    }

processSocket()方法会处理所有的请求进来的Socket,包括ping的和VideoView.setVideoPath(proxyUrl)的Socket,我们重点看一下 else语句里面的代码。这里的 getClients()方法里面有一个ConcurrentHashMap,重复url返回的是同一个HttpProxyCacheServerClients。

private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
        synchronized (clientsLock) {
            HttpProxyCacheServerClients clients = clientsMap.get(url);
            if (clients == null) {
                clients = new HttpProxyCacheServerClients(url,config);
                clientsMap.put(url,clients);
            }
            return clients;
        }
    }

如果是第一次请求的url,HttpProxyCacheServerClients并被put到ConcurrentHashMap中。而真正的网络请求都在 processRequest ()方法中进行操作,并且需要传递过去一个GetRequest 对象,包括是一个url和rangeoffset以及partial的包装类。

    public void processRequest(GetRequest request,Socket socket) throws ProxyCacheException,IOException {
        startProcessRequest();
        try {
            clientsCount.incrementAndGet();
            proxyCache.processRequest(request,socket);
        } finally {
            finishProcessRequest();
        }
    }

其中,startProcessRequest 方法会得到一个新的HttpProxyCache 类对象。

    private synchronized void startProcessRequest() throws ProxyCacheException {
        proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
    }

    private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
        HttpUrlSource source = new HttpUrlSource(url,config.sourceInfoStorage);
        FileCache cache = new FileCache(config.generateCacheFile(url),config.diskUsage);
        HttpProxyCache httpProxyCache = new HttpProxyCache(source,cache);
        httpProxyCache.registerCacheListener(uiCacheListener);
        return httpProxyCache;
    }

此处,我们构建一个基于原生url的HttpUrlSource ,这个类对象负责持有url,并开启HttpURLConnection来获取一个InputStream,这样就可以使用这个输入流来读取数据了,同时也创建了一个本地的临时文件,一个以.download结尾的临时文件,这个文件在成功下载完后的 FileCache 类中的 complete 方法中被更名。执行完上面的操作之后,然后这个HttpProxyCache 对象就开始 调用processRequest()方法。

    public void processRequest(GetRequest request,Socket socket) throws IOException,ProxyCacheException {
        OutputStream out = new BufferedOutputStream(socket.getOutputStream());
        String responseHeaders = newResponseHeaders(request);
        out.write(responseHeaders.getBytes("UTF-8"));

        long offset = request.rangeOffset;
        if (isUseCache(request)) {
            responseWithCache(out,offset);
        } else {
            responseWithoutCache(out,offset);
        }
    }

拿到一个OutputStream的输出流后,我们就可以往sd卡中写数据了,如果不用缓存就走常规逻辑,这里我们只看走缓存的逻辑,即responseWithCache()。

    private void responseWithCache(OutputStream out,long offset) throws ProxyCacheException,IOException {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int readBytes;
        while ((readBytes = read(buffer,offset,buffer.length)) != -1) {
            out.write(buffer,readBytes);
            offset += readBytes;
        }
        out.flush();
    }

public int read(byte[] buffer,long offset,int length) throws ProxyCacheException {
        ProxyCacheUtils.assertBuffer(buffer,length);

        while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
            readSourceAsync();
            waitForSourceData();
            checkReadSourceErrorsCount();
        }
        int read = cache.read(buffer,length);
        if (cache.isCompleted() && percentsAvailable != 100) {
            percentsAvailable = 100;
            onCachePercentsAvailableChanged(100);
        }
        return read;
    }

在while循环里面,开启了一个新的线程sourceReaderThread,其中封装了一个SourceReaderRunnable的Runnable,这个异步线程用来给cache,也就是本地文件写数据,同时还更新一下当前的缓存进度。

同时,另一个SourceReaderRunnable线程会从cache中去读数据,在缓存结束后会发送一个通知通知缓存完了,外界可以去调用了。

        int sourceAvailable = -1;
        int offset = 0;
        try {
            offset = cache.available();
            source.open(offset);
            sourceAvailable = source.length();
            byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
            int readBytes;
            while ((readBytes = source.read(buffer)) != -1) {
                synchronized (stopLock) {
                    if (isStopped()) {
                        return;
                    }
                    cache.append(buffer,readBytes);
                }
                offset += readBytes;
                notifyNewCacheDataAvailable(offset,sourceAvailable);
            }
            tryComplete();
            onSourceRead();

到此,AndroidVideoCache的核心缓存流程就分析完了。总的来说,AndroidVideoCache在请求时回先使用本地的代理方式,然后开启一系列的缓存逻辑,并在缓存完成后发出通知,当再次请求的时候,如果本地已经进行了文件缓存,就会优先使用本地的数据。

推荐

Android 音视频开发核心知识点笔记整合

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具