Android原生PDF功能實現

来源:https://www.cnblogs.com/qixingchao/archive/2019/10/12/11658226.html
-Advertisement-
Play Games

1、背景 近期,公司希望實現安卓原生端的PDF功能,要求:高效、實用。 經過兩天的調研、編碼,實現了一個簡單Demo,如上圖所示。 關於安卓原生端的PDF功能實現,技術點還是很多的,為了咱們安卓開發的同學少走彎路,通過此文章,簡單講解下Demo的實現原理和主要技術點,並附上源碼。 2、安卓PDF現狀 ...


PDF Demo 效果

1、背景

近期,公司希望實現安卓原生端的PDF功能,要求:高效、實用。

經過兩天的調研、編碼,實現了一個簡單Demo,如上圖所示。
關於安卓原生端的PDF功能實現,技術點還是很多的,為了咱們安卓開發的同學少走彎路,通過此文章,簡單講解下Demo的實現原理和主要技術點,並附上源碼。

2、安卓PDF現狀

目前,PDF功能仍然是安卓的一個短板,不像iOS,有官方強大的PDF Kit可供集成。
不過,安卓也有一些主流的方案,不過各有優缺點:

1、google doc 線上閱讀,基於webview,國內需翻牆訪問(不可行)
2、跳轉設備中預設pdf app打開,前提需要手機安裝了pdf 軟體(可按需選擇)
3、內置 android-pdfview,基於原生native, apk增加約15~20M(可行,不過安裝包有點大)
4、內置 mupdf,基於原生native, 集成有點麻煩,增加約9M(可行,不過安裝包稍有點大)
5、內置 pdf.js,功能豐富,apk增加5M(基於Webview,性能低,js實現,功能定製複雜)
6、使用x5內核,需要客戶端完全使用x5內核(基於Webview,性能低,不能定製功能)

查閱官方資料,這些方案雖然能實現基本的PDF閱讀功能,但是多數方案,集成過程較複雜,且性能低下,容易記憶體溢出造成App閃退。

3、方案選擇

經過對各方案的反覆比對,本次實現PDF Demo,決定使用:android-pdfview。
原因:

1、android-pdfview基於PDFium實現(PDFium是谷歌 + 福昕軟體的PDF開源項目);
2、android-pdfview Github仍在維護;
3、android-pdfview Github獲得的星星較多;
4、客戶端集成較方便;

問題分析:
運行android-pdfview官方demo,問題也很多:

1、僅實現了pdf滑動閱讀、手勢伸縮的功能;
2、缺少pdf目錄樹、縮略圖等功能;
3、安裝包過大;
4、UI不美觀;
5、記憶體問題;
6、其他...

不過,不用擔心,解決了這些問題不就沒有問題了嘛,哈、哈、哈(笑聲有點勉強哈)

下麵,咱們開始實現Demo吧。

4、Demo設計

4.1、工程結構

在設計之前,應明確Demo的實現目標:

1、android-pdfview已實現了pdfview,可用於閱讀pdf文件,手勢伸縮pdf頁面、跳轉pdf頁面,
   那麼,咱們基於android-pdfview擴展功能即可,功能包括:目錄樹、縮略圖等;

2、擴展的功能應邏輯解耦,不能影響android-pdfview代碼的可替換性
  (即:如果android-pdfview有新版本,直接替換即可)

3、客戶端應很方便集成
  (如:客戶端僅需要傳遞過來pdf文件,所有的載入、操作、記憶體管理均無需關心)

Demo工程如何設計:
下載android-pdfview最新源碼,可以看到共包含兩個Moudle:

android-pdf-viewer(最新源碼)
sample (示例app)

如果,我們要接管封裝pdf的所有功能,讓sample只傳遞pdf文件即可,且不影響將來替換android-pdf-viewer的源碼,那麼我們創建一個modle即可,如下圖:

sample (依賴pdfui)
pdfui (依賴android-pdf-viewer)
android-pdf-viewer

4.2、PDF功能設計

為了便於用戶閱讀PDF,應該包含以下功能:
1、PDF閱讀(包含:手指滑動pdf頁面、手勢伸縮頁面內容、跳轉pdf指定頁面)
2、PDF目錄導航功能(包含:目錄展示、目錄節點摺疊、展開、點擊跳轉pdf頁面)
3、PDF縮略圖導航功能(包含:縮略圖展示、手指滑動、圖片緩存管理、點擊跳轉pdf頁面)

PDF功能代碼結構

5、編碼之前,先解決安裝包過大的問題

反編譯Demo的安裝包,可以看到,安裝包中預設集成了各cpu平臺對應的so庫文件,安裝包過大的原因也就在這兒。其實正常項目開發中,對於各cpu平臺對應的so庫的保留或捨棄,主要考慮cpu平臺相容性、設備覆蓋率。

通常情況下,僅保留armeabi-v7a可以相容市面上絕大多數安卓設備,那麼,如何編譯時刪除其他的so呢?

可在android gradle中配置,如下:

android{
......
 splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a' //如果想包含其他cpu平臺使用的so,修改這裡即可
        }
    }
}

重新編譯,生成的安裝包,僅剩5M左右了。

註意:如果項目中還有其他so庫,要根據項目實際需求,認真思考如何取捨了。

6、實現PDF閱讀功能

很簡單,因為android-pdf-viewer源碼中已經實現了該功能,我們寫一份精簡版的吧。

6.1、功能點:

1、可載入assets中的pdf文件
2、可載入uri類型的pdf文件(如果是線上的pdf文件,可通過網路庫先下載到本地,取其uri,本次Demo就不寫網路下載了)
3、pdf的基本展示功能(使用android-pdf-viewer的控制項實現:PDFView)
4、可跳轉至目錄頁面(目錄數據可通過intent直接傳遞過去)
5、可跳轉至預覽頁面(pdf文件信息可通過intent直接傳遞過去)
6、根據目錄頁面、預覽頁面帶回的頁碼,跳轉至指定的pdf頁面

PDF閱讀功能效果圖

6.2、代碼實現

重點內容:

1、PDFView控制項的使用;(比較簡單,詳見代碼)
2、如何從PDF文件中獲得目錄信息;(如何獲得目錄信息、什麼時機獲取,詳見代碼)

PDF閱讀頁面的代碼:PDFActivity

/**
 * UI頁面:PDF閱讀
 * <p>
 * 主要功能:
 * 1、接收傳遞過來的pdf文件(包括assets中的文件名、文件uri)
 * 2、顯示PDF文件
 * 3、接收目錄頁面、預覽頁面返回的PDF頁碼,跳轉到指定的頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFActivity extends AppCompatActivity implements
        OnPageChangeListener,
        OnLoadCompleteListener,
        OnPageErrorListener {
    //PDF控制項
    PDFView pdfView;
    //按鈕控制項:返回、目錄、縮略圖
    Button btn_back, btn_catalogue, btn_preview;
    //頁碼
    Integer pageNumber = 0;
    //PDF目錄集合
    List<TreeNodeData> catelogues;

    //pdf文件名(限:assets里的文件)
    String assetsFileName;
    //pdf文件uri
    Uri uri;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//設置沉浸式
        setContentView(R.layout.activity_pdf);

        initView();//初始化view
        setEvent();//設置事件
        loadPdf();//載入PDF文件
    }

    /**
     * 初始化view
     */
    private void initView() {
        pdfView = findViewById(R.id.pdfView);
        btn_back = findViewById(R.id.btn_back);
        btn_catalogue = findViewById(R.id.btn_catalogue);
        btn_preview = findViewById(R.id.btn_preview);
    }

    /**
     * 設置事件
     */
    private void setEvent() {
        //返回
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFActivity.this.finish();
            }
        });
        //跳轉目錄頁面
        btn_catalogue.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(PDFActivity.this, PDFCatelogueActivity.class);
                intent.putExtra("catelogues", (Serializable) catelogues);
                PDFActivity.this.startActivityForResult(intent, 200);
            }
        });
        //跳轉縮略圖頁面
        btn_preview.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(PDFActivity.this, PDFPreviewActivity.class);
                intent.putExtra("AssetsPdf", assetsFileName);
                intent.setData(uri);
                PDFActivity.this.startActivityForResult(intent, 201);
            }
        });
    }

    /**
     * 載入PDF文件
     */
    private void loadPdf() {
        Intent intent = getIntent();
        if (intent != null) {
            assetsFileName = intent.getStringExtra("AssetsPdf");
            if (assetsFileName != null) {
                displayFromAssets(assetsFileName);
            } else {
                uri = intent.getData();
                if (uri != null) {
                    displayFromUri(uri);
                }
            }
        }
    }

    /**
     * 基於assets顯示 PDF 文件
     *
     * @param fileName 文件名稱
     */
    private void displayFromAssets(String fileName) {
        pdfView.fromAsset(fileName)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // 單位 dp
                .onPageError(this)
                .pageFitPolicy(FitPolicy.BOTH)
                .load();
    }

    /**
     * 基於uri顯示 PDF 文件
     *
     * @param uri 文件路徑
     */
    private void displayFromUri(Uri uri) {
        pdfView.fromUri(uri)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // 單位 dp
                .onPageError(this)
                .load();
    }

    /**
     * 當成功載入PDF:
     * 1、可獲取PDF的目錄信息
     *
     * @param nbPages the number of pages in this PDF file
     */
    @Override
    public void loadComplete(int nbPages) {
        //獲得文檔書簽信息
        List<PdfDocument.Bookmark> bookmarks = pdfView.getTableOfContents();
        if (catelogues != null) {
            catelogues.clear();
        } else {
            catelogues = new ArrayList<>();
        }
        //將bookmark轉為目錄數據集合
        bookmarkToCatelogues(catelogues, bookmarks, 1);
    }

    /**
     * 將bookmark轉為目錄數據集合(遞歸)
     *
     * @param catelogues 目錄數據集合
     * @param bookmarks  書簽數據
     * @param level      目錄樹級別(用於控制樹節點位置偏移)
     */
    private void bookmarkToCatelogues(List<TreeNodeData> catelogues, List<PdfDocument.Bookmark> bookmarks, int level) {
        for (PdfDocument.Bookmark bookmark : bookmarks) {
            TreeNodeData nodeData = new TreeNodeData();
            nodeData.setName(bookmark.getTitle());
            nodeData.setPageNum((int) bookmark.getPageIdx());
            nodeData.setTreeLevel(level);
            nodeData.setExpanded(false);
            catelogues.add(nodeData);
            if (bookmark.getChildren() != null && bookmark.getChildren().size() > 0) {
                List<TreeNodeData> treeNodeDatas = new ArrayList<>();
                nodeData.setSubset(treeNodeDatas);
                bookmarkToCatelogues(treeNodeDatas, bookmark.getChildren(), level + 1);
            }
        }
    }

    @Override
    public void onPageChanged(int page, int pageCount) {
        pageNumber = page;
    }

    @Override
    public void onPageError(int page, Throwable t) {
    }

    /**
     * 從縮略圖、目錄頁面帶回頁碼,跳轉到指定PDF頁面
     *
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            int pageNum = data.getIntExtra("pageNum", 0);
            if (pageNum > 0) {
                pdfView.jumpTo(pageNum);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //是否記憶體
        if (pdfView != null) {
            pdfView.recycle();
        }
    }
}

PDF閱讀頁面的佈局文件:activity_pdf.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="10dp"/>

        <Button
            android:id="@+id/btn_catalogue"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="目錄"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_alignParentRight="true"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginRight="10dp"/>

        <Button
            android:id="@+id/btn_preview"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="預覽"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_toLeftOf="@+id/btn_catalogue"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginRight="10dp"/>
    </RelativeLayout>

    <com.github.barteksc.pdfviewer.PDFView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top"/>

</RelativeLayout>

7、PDF目錄樹的實現

目錄樹的數據(目錄名稱、頁碼...),已在上個頁面獲取了,所以此頁面只需考慮目錄樹控制項的實現。

註意:之所以沒在這個頁面單獨獲取目錄樹的數據,主要考慮到android-pdfview、pdfium記憶體占用太大了,不想再次創建Pdf的相關對象。

7.1、PDF目錄樹效果圖

7.2、樹形控制項如何實現?

安卓預設沒有樹形控制項,不過我們可以使用RecyclerView或ListView實現。
如上圖所示:

列表每一行為一條目錄數據,主要包括:名稱、頁碼;
如果有子目錄,則出現箭頭圖片,該項可摺疊、展開,箭頭方向隨之改變;
子目錄的名稱文本隨目錄樹級別遞增向右偏移;

當前Demo實現方式為RecyclerView,應該如何實現上面的效果?
可在adapter中處理頁面效果、事件效果:
1、列表項內容展示

1、使用垂直線性佈局管理器;
2、每個item包含:箭頭圖片(如果有子目錄,則顯示)、命令名稱文本、頁碼文本;

2、摺疊效果

1、控制adapter數據集合的內容即可,如果某節點摺疊了,就把對應的子目錄數據刪除即可,
反之,加上,再notifyDataSetChanged通知數據源改變;
2、除此之外,還需有一個狀態來標記當前節點是展開還是摺疊,用於控制箭頭圖片方向的顯示;

3、目錄文本向右偏移效果

可通過目錄樹層級 * 固定左側間隔(如: 20dp),然後為目錄的textview控制項設置偏移即可;

目錄樹層級樹如何獲取? 可選方案:
1、遞歸集合自動獲取(需要遍歷,效率低一點,如果是可編輯的目錄結構,建議選擇)
2、創建數據的時候,直接寫死(因當前demo的PDF目錄結構不會被編輯,所以直接選擇這個方案吧)

7.3、代碼實現:

樹形控制項的數據對象TreeNodeData:

/**
 * 樹形控制項數據類(會用於頁面間傳輸,所以需實現Serializable 或 Parcelable)
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class TreeNodeData implements Serializable {
    //名稱
    private String name;
    //頁碼
    private int pageNum;
    //是否已展開(用於控制樹形節點圖片顯示,即箭頭朝向圖片)
    private boolean isExpanded;
    //展示級別(1級、2級...,用於控制樹形節點縮進位置)
    private int treeLevel;
    //子集(用於載入子節點,也用於判斷是否顯示箭頭圖片,如集合不為空,則顯示)
    private List<TreeNodeData> subset;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public boolean isExpanded() {
        return isExpanded;
    }

    public void setExpanded(boolean expanded) {
        isExpanded = expanded;
    }

    public int getTreeLevel() {
        return treeLevel;
    }

    public void setTreeLevel(int treeLevel) {
        this.treeLevel = treeLevel;
    }

    public List<TreeNodeData> getSubset() {
        return subset;
    }

    public void setSubset(List<TreeNodeData> subset) {
        this.subset = subset;
    }
}

樹形控制項適配器 : TreeAdapter

/**
 * 樹形控制項適配器
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class TreeAdapter extends RecyclerView.Adapter<TreeAdapter.TreeNodeViewHolder> {
    //上下文
    private Context context;
    //數據
    public List<TreeNodeData> data;
    //展示數據(由層級結構改為平面結構)
    public List<TreeNodeData> displayData;
    //treelevel間隔(dp)
    private int maginLeft;
    //委托對象
    private TreeEvent delegate;

    /**
     * 構造函數
     *
     * @param context 上下文
     * @param data    數據
     */
    public TreeAdapter(Context context, List<TreeNodeData> data) {
        this.context = context;
        this.data = data;
        maginLeft = UIUtils.dip2px(context, 20);
        displayData = new ArrayList<>();

        //數據轉為展示數據
        dataToDiaplayData(data);
    }

    /**
     * 數據轉為展示數據
     *
     * @param data 數據
     */
    private void dataToDiaplayData(List<TreeNodeData> data) {
        for (TreeNodeData nodeData : data) {
            displayData.add(nodeData);
            if (nodeData.isExpanded() && nodeData.getSubset() != null) {
                dataToDiaplayData(nodeData.getSubset());
            }
        }
    }

    /**
     * 數據集合轉為可顯示的集合
     */
    private void reDataToDiaplayData() {
        if (this.data == null || this.data.size() == 0) {
            return;
        }
        if(displayData == null){
            displayData = new ArrayList<>();
        }else{
            displayData.clear();
        }
        dataToDiaplayData(this.data);
        notifyDataSetChanged();
    }

    @Override
    public TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);
        return new TreeNodeViewHolder(view);
    }

    @Override
    public void onBindViewHolder(TreeNodeViewHolder holder, int position) {
        final TreeNodeData data = displayData.get(position);
        //設置圖片
        if (data.getSubset() != null) {
            holder.img.setVisibility(View.VISIBLE);
            if (data.isExpanded()) {
                holder.img.setImageResource(R.drawable.arrow_h);
            } else {
                holder.img.setImageResource(R.drawable.arrow_v);
            }
        } else {
            holder.img.setVisibility(View.INVISIBLE);
        }
        //設置圖片偏移位置
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.img.getLayoutParams();
        int ratio = data.getTreeLevel() <= 0? 0 : data.getTreeLevel()-1;
        params.setMargins(maginLeft * ratio, 0, 0, 0);
        holder.img.setLayoutParams(params);

        //顯示文本
        holder.title.setText(data.getName());
        holder.pageNum.setText(String.valueOf(data.getPageNum()));

        //圖片點擊事件
        holder.img.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //控制樹節點展開、摺疊
                data.setExpanded(!data.isExpanded());
                //刷新數據源
                reDataToDiaplayData();
            }
        });
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //回調結果
                if(delegate!=null){
                    delegate.onSelectTreeNode(data);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return displayData.size();
    }

    /**
     * 定義RecyclerView的ViewHolder對象
     */
    class TreeNodeViewHolder extends RecyclerView.ViewHolder {
        ImageView img;
        TextView title;
        TextView pageNum;

        public TreeNodeViewHolder(View view) {
            super(view);
            img = view.findViewById(R.id.iv_arrow);
            title = view.findViewById(R.id.tv_title);
            pageNum = view.findViewById(R.id.tv_pagenum);
        }
    }

    /**
     * 介面:Tree事件
     */
    public interface TreeEvent{
        /**
         * 當選擇了某tree節點
         * @param data tree節點數據
         */
        void onSelectTreeNode(TreeNodeData data);
    }

    /**
     * 設置Tree的事件
     * @param treeEvent Tree的事件對象
     */
    public void setTreeEvent(TreeEvent treeEvent){
        this.delegate = treeEvent;
    }
}

PDF目錄樹頁面:PDFCatelogueActivity

/**
 * UI頁面:PDF目錄
 * <p>
 * 1、用於顯示Pdf目錄信息
 * 2、點擊tree item,帶回Pdf頁碼到前一個頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFCatelogueActivity extends AppCompatActivity implements TreeAdapter.TreeEvent {

    RecyclerView recyclerView;
    Button btn_back;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
        setContentView(R.layout.activity_catelogue);

        initView();//初始化控制項
        setEvent();//設置事件
        loadData();//載入數據
    }

    /**
     * 初始化控制項
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_tree);
    }

    /**
     * 設置事件
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFCatelogueActivity.this.finish();
            }
        });
    }

    /**
     * 載入數據
     */
    private void loadData() {
        //從intent中獲得傳遞的數據
        Intent intent = getIntent();
        List<TreeNodeData> catelogues = (List<TreeNodeData>) intent.getSerializableExtra("catelogues");

        //使用RecyclerView載入數據
        LinearLayoutManager llm = new LinearLayoutManager(this);
        llm.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(llm);
        TreeAdapter adapter = new TreeAdapter(this, catelogues);
        adapter.setTreeEvent(this);
        recyclerView.setAdapter(adapter);
    }


    /**
     * 點擊tree item,帶回Pdf頁碼到前一個頁面
     *
     * @param data tree節點數據
     */
    @Override
    public void onSelectTreeNode(TreeNodeData data) {
        Intent intent = new Intent();
        intent.putExtra("pageNum", data.getPageNum());
        setResult(Activity.RESULT_OK, intent);
        finish();
    }
}

PDF目錄樹的佈局文件:activity_catelogue.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_marginLeft="10dp"
            android:layout_marginBottom="10dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textColor="#ffffff"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="15dp"
            android:text="目錄列表"
            android:textColor="#ffffff"
            android:textSize="18sp" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_tree"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />

</RelativeLayout>

8、PDF預覽縮略圖

這個功能算是本Demo中最為複雜的一個了:

如何將PDF某頁面的內容轉成圖片?(預設是無法從pdfview中獲得頁面圖片的)
如何減少圖片記憶體的占用?(用戶可能快速滑動列表,實時讀取、顯示多張圖片)
如何優化PDF預覽縮略圖列表的滑動體驗?(圖片的獲取需要一定時間)
如何合理的及時釋放記憶體占用?

8.1、PDF預覽縮略圖列表的效果圖

8.2、功能分析

1、如何將PDF某頁面的內容轉成圖片?

查看android-pdfview的源碼,無法通過PDFView控制項獲得某頁面的圖片,所以只能分析pdfium sdk的API了,如下圖:

pdfium的renderPageBitmap方法可以將頁面渲染成圖片,不過需要傳遞一系列參數,而且要小心OutOfMemoryError。

那麼,我們需要在代碼中獲取或者創建PdfiumCore對象,調用該方法,傳遞PdfDocument等參數,當bitmap使用完後,應及時釋放掉。

2、如何減少記憶體的占用?

記憶體主要包括:
1、pdfium sdk載入pdf文件產生的記憶體(我們無法優化)
2、android-pdfview產生的記憶體(如果有需要,可改其源碼)
3、我們將pdf頁面轉為縮略圖,而產生的記憶體(必須優化,否則,容易oom)

3.1、當PdfiumCore、PdfDocument不再使用時,應及時關閉;
3.2、當縮略圖不再使用時,應及時釋放;
3.3、可使用LruCache臨時緩存縮略圖,防止重覆調用renderPageBitmap獲取圖片;
3.4、LruCache應合理管控,當預覽頁面關閉時,必須清空緩存,以釋放記憶體;
3.5、創建圖片時,應使用RGB_565,能節約記憶體開銷(一個像素點,占2位元組)
3.6、創建圖片時,應儘可能小的指定圖片的寬高,能看清就行(圖片占用的記憶體 = 寬 * 高 * 一個像素點占的位元組數)

3、如何優化PDF預覽縮略圖列表的滑動體驗?

查看pdfium源碼,調用renderPageBitmap方法之前,還必須確保對應的頁面已被打開,即調用了openPage方法。然而,這兩個方法都需要一定時間才能執行完成的。

那麼,如果我們直接在主線程中讓每個RecylerVew的item分別調用renderPageBitmap方法,滑動列表時,會感覺特別卡,所以該方法只能放在子線程中調用了。

那麼問題又來了,那麼多子線程應該如何管控?

1、考慮CPU的占用,應使用線程池控制子線程併發、阻塞;
2、考慮到用戶滑動速度,有可能某線程正執行或者阻塞著呢,頁面已經滑過去了,那麼,即使該線程載入出來了圖片,也無法顯示到列表中。所以對於RecyclerView已不可見的Item項對應的線程,應及時取消,防止做無用功,也節省了記憶體和cpu開銷。

8.3、功能實現

預覽縮略圖工具類:PreviewUtils

/**
 * 預覽縮略圖工具類
 *
 * 1、pdf頁面轉為縮略圖
 * 2、圖片緩存管理(僅保存到記憶體,可使用LruCache,註意空間大小控制)
 * 3、多線程管理(線程併發、阻塞、Future任務取消)
 *
 * 作者:齊行超
 * 日期:2019.08.08
 */
public class PreviewUtils {
    //圖片緩存管理
    private ImageCache imageCache;
    //單例
    private static PreviewUtils instance;
    //線程池
    ExecutorService executorService;
    //線程任務集合(可用於取消任務)
    HashMap<String, Future> tasks;

    /**
     * 單例(僅主線程調用,無需做成線程安全的)
     *
     * @return PreviewUtils實例對象
     */
    public static PreviewUtils getInstance() {
        if (instance == null) {
            instance = new PreviewUtils();
        }
        return instance;
    }

    /**
     * 預設構造函數
     */
    private PreviewUtils() {
        //初始化圖片緩存管理對象
        imageCache = new ImageCache();
        //創建併發線程池(建議最大併發數大於1屏grid item的數量)
        executorService = Executors.newFixedThreadPool(20);
        //創建線程任務集合,用於取消線程執行
        tasks = new HashMap<>();
    }

    /**
     * 從pdf文件中載入圖片
     *
     * @param context     上下文
     * @param imageView   圖片控制項
     * @param pdfiumCore  pdf核心對象
     * @param pdfDocument pdf文檔對象
     * @param pdfName     pdf文件名稱
     * @param pageNum     pdf頁碼
     */
    public void loadBitmapFromPdf(final Context context,
                                  final ImageView imageView,
                                  final PdfiumCore pdfiumCore,
                                  final PdfDocument pdfDocument,
                                  final String pdfName,
                                  final int pageNum) {
        //判斷參數合法性
        if (imageView == null || pdfiumCore == null || pdfDocument == null || pageNum < 0) {
            return;
        }

        try {
            //緩存key
            final String keyPage = pdfName + pageNum;

            //為圖片控制項設置標記
            imageView.setTag(keyPage);

            Log.i("PreViewUtils", "載入pdf縮略圖:" + keyPage);

            //獲得imageview的尺寸(註意:如果使用正常控制項尺寸,太占記憶體了)
            /*int w = imageView.getMeasuredWidth();
            int h = imageView.getMeasuredHeight();
            final int reqWidth = w == 0 ? UIUtils.dip2px(context,100) : w;
            final int reqHeight = h == 0 ? UIUtils.dip2px(context,150) : h;*/

            //記憶體大小= 圖片寬度 * 圖片高度 * 一個像素占的位元組數(RGB_565 所占位元組:2)
            //註意:如果使用正常控制項尺寸,太占記憶體了,所以此處指定四縮略圖看著會模糊一點
            final int reqWidth = 100;
            final int reqHeight = 150;

            //從緩存中取圖片
            Bitmap bitmap = imageCache.getBitmapFromLruCache(keyPage);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }

            //使用線程池管理子線程
            Future future = executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //打開頁面(調用renderPageBitmap方法之前,必須確保頁面已open,重要)
                    pdfiumCore.openPage(pdfDocument, pageNum);

                    //調用native方法,將Pdf頁面渲染成圖片
                    final Bitmap bm = Bitmap.createBitmap(reqWidth, reqHeight, Bitmap.Config.RGB_565);
                    pdfiumCore.renderPageBitmap(pdfDocument, bm, pageNum, 0, 0, reqWidth, reqHeight);

                    //切回主線程,設置圖片
                    if (bm != null) {
                        //將圖片加入緩存
                        imageCache.addBitmapToLruCache(keyPage, bm);

                        //切回主線程載入圖片
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                if (imageView.getTag().toString().equals(keyPage)) {
                                    imageView.setImageBitmap(bm);
                                    Log.i("PreViewUtils", "載入pdf縮略圖:" + keyPage + "......已設置!!");
                                }
                            }
                        });
                    }
                }
            });

            //將任務添加到集合
            tasks.put(keyPage, future);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 取消從pdf文件中載入圖片的任務
     *
     * @param keyPage 頁碼
     */
    public void cancelLoadBitmapFromPdf(String keyPage) {
        if (keyPage == null || !tasks.containsKey(keyPage)) {
            return;
        }
        try {
            Log.i("PreViewUtils", "取消載入pdf縮略圖:" + keyPage);
            Future future = tasks.get(keyPage);
            if (future != null) {
                future.cancel(true);
                Log.i("PreViewUtils", "取消載入pdf縮略圖:" + keyPage + "......已取消!!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 獲得圖片緩存對象
     * @return 圖片緩存
     */
    public ImageCache getImageCache(){
        return imageCache;
    }

    /**
     * 圖片緩存管理
     */
   public class ImageCache {
        //圖片緩存
        private LruCache<String, Bitmap> lruCache;

        //構造函數
        public ImageCache() {
            //初始化 lruCache
            //int maxMemory = (int) Runtime.getRuntime().maxMemory();
            //int cacheSize = maxMemory/8;
            int cacheSize = 1024 * 1024 * 30;//暫時設定30M
            lruCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }

        /**
         * 從緩存中取圖片
         * @param key 鍵
         * @return 圖片
         */
        public synchronized Bitmap getBitmapFromLruCache(String key) {
            if(lruCache!= null) {
                return lruCache.get(key);
            }
            return null;
        }

        /**
         * 向緩存中加圖片
         * @param key 鍵
         * @param bitmap 圖片
         */
        public synchronized void addBitmapToLruCache(String key, Bitmap bitmap) {
            if (getBitmapFromLruCache(key) == null) {
                if (lruCache!= null && bitmap != null)
                    lruCache.put(key, bitmap);
            }
        }

        /**
         * 清空緩存
         */
        public void clearCache(){
            if(lruCache!= null){
                lruCache.evictAll();
            }
        }
    }
}

grid列表適配器: GridAdapter

/**
 * grid列表適配器
 * 作者:齊行超
 * 日期:2019.08.08
 */
public class GridAdapter extends RecyclerView.Adapter<GridAdapter.GridViewHolder> {

    Context context;
    PdfiumCore pdfiumCore;
    PdfDocument pdfDocument;
    String pdfName;
    int totalPageNum;


    public GridAdapter(Context context, PdfiumCore pdfiumCore, PdfDocument pdfDocument, String pdfName, int totalPageNum) {
        this.context = context;
        this.pdfiumCore = pdfiumCore;
        this.pdfDocument = pdfDocument;
        this.pdfName = pdfName;
        this.totalPageNum = totalPageNum;
    }

    @Override
    public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
        return new GridViewHolder(view);
    }

    @Override
    public void onBindViewHolder(GridViewHolder holder, int position) {
        //設置PDF圖片
        final int pageNum = position;
        PreviewUtils.getInstance().loadBitmapFromPdf(context, holder.iv_page, pdfiumCore, pdfDocument, pdfName, pageNum);
        //設置PDF頁碼
        holder.tv_pagenum.setText(String.valueOf(position));
        //設置Grid事件
        holder.iv_page.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(delegate!=null){
                    delegate.onGridItemClick(pageNum);
                }
            }
        });
        return;
    }

    @Override
    public void onViewDetachedFromWindow(GridViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        try {
            //item不可見時,取消任務
            if(holder.iv_page!=null){
                PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
            }

            //item不可見時,釋放bitmap  (註意:本Demo使用了LruCache緩存來管理圖片,此處可註釋掉)
            /*Drawable drawable = holder.iv_page.getDrawable();
            if (drawable != null) {
                Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
                if (bitmap != null && !bitmap.isRecycled()) {
                    bitmap.recycle();
                    bitmap = null;
                    Log.i("PreViewUtils","銷毀pdf縮略圖:"+holder.iv_page.getTag().toString());
                }
            }*/
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    @Override
    public int getItemCount() {
        return totalPageNum;
    }

    class GridViewHolder extends RecyclerView.ViewHolder {
        ImageView iv_page;
        TextView tv_pagenum;

        public GridViewHolder(View itemView) {
            super(itemView);
            iv_page = itemView.findViewById(R.id.iv_page);
            tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
        }
    }

    /**
     * 介面:Grid事件
     */
    public interface GridEvent{
        /**
         * 當選擇了某Grid項
         * @param position tree節點數據
         */
        void onGridItemClick(int position);
    }

    /**
     * 設置Grid事件
     * @param event Grid事件對象
     */
    public void setGridEvent(GridEvent event){
        this.delegate = event;
    }

    //Grid事件委托
    private GridEvent delegate;
}

PDF預覽縮略圖頁面:PDFPreviewActivity

/**
 * UI頁面:PDF預覽縮略圖(註意:此頁面,需多關註記憶體管控)
 * <p>
 * 1、用於顯示Pdf縮略圖信息
 * 2、點擊縮略圖,帶回Pdf頁碼到前一個頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFPreviewActivity extends AppCompatActivity implements GridAdapter.GridEvent {

    RecyclerView recyclerView;
    Button btn_back;
    PdfiumCore pdfiumCore;
    PdfDocument pdfDocument;
    String assetsFileName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
        setContentView(R.layout.activity_preview);

        initView();//初始化控制項
        setEvent();
        loadData();
    }

    /**
     * 初始化控制項
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_grid);
    }

    /**
     * 設置事件
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //回收記憶體
                recycleMemory();

                PDFPreviewActivity.this.finish();
            }
        });

    }

    /**
     * 載入數據
     */
    private void loadData() {
        //載入pdf文件
        loadPdfFile();

        //獲得pdf總頁數
        int totalCount = pdfiumCore.getPageCount(pdfDocument);

        //綁定列表數據
        GridAdapter adapter = new GridAdapter(this, pdfiumCore, pdfDocument, assetsFileName, totalCount);
        adapter.setGridEvent(this);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        recyclerView.setAdapter(adapter);
    }

    /**
     * 載入pdf文件
     */
    private void loadPdfFile() {
        Intent intent = getIntent();
        if (intent != null) {
            assetsFileName = intent.getStringExtra("AssetsPdf");
            if (assetsFileName != null) {
                loadAssetsPdfFile(assetsFileName);
            } else {
                Uri uri = intent.getData();
                if (uri != null) {
                    loadUriPdfFile(uri);
                }
            }
        }
    }

    /**
     * 載入assets中的pdf文件
     */
    void loadAssetsPdfFile(String assetsFileName) {
        try {
            File f = FileUtils.fileFromAsset(this, assetsFileName);
            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
            pdfiumCore = new PdfiumCore(this);
            pdfDocument = pdfiumCore.newDocument(pfd);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 基於uri載入pdf文件
     */
    void loadUriPdfFile(Uri uri) {
        try {
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
            pdfiumCore = new PdfiumCore(this);
            pdfDocument = pdfiumCore.newDocument(pfd);
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    /**
     * 點擊縮略圖,帶回Pdf頁碼到前一個頁面
     *
     * @param position 頁碼
     */
    @Override
    public void onGridItemClick(int position) {
        //回收記憶體
        recycleMemory();

        //返回前一個頁碼
        Intent intent = new Intent();
        intent.putExtra("pageNum", position);
        setResult(Activity.RESULT_OK, intent);
        finish();
    }

    /**
     * 回收記憶體
     */
    private void recycleMemory(){
        //關閉pdf對象
        if (pdfiumCore != null && pdfDocument != null) {
            pdfiumCore.closeDocument(pdfDocument);
            pdfiumCore = null;
        }
        //清空圖片緩存,釋放記憶體空間
        PreviewUtils.getInstance().getImageCache().clearCache();
    }
}

PDF預覽縮略圖頁面的佈局文件:activity_preview.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_marginLeft="10dp"
            android:layout_marginBottom="10dp"
            android:background="@drawable/shape_button"
            android:text="返回"
            android:textColor="#ffffff"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="15dp"
            android:text="預覽縮略圖列表"
            android:textColor="#ffffff"
            android:textSize="18sp" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_grid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />
</RelativeLayout>

總結

文檔中涉及的功能點較多,難點也較多,尤其是記憶體管理、多線程管理,有不明白的建議下載Demo,多看下源碼。也歡迎留言咨詢,就是不一定有時間解答,哈哈。。。。

如果希望把該demo用到項目中,建議多測試一下,因為時間關係,我這邊僅做了基本測試。

Demo下載地址(github + 百度網盤):
https://github.com/qxcwanxss/AndroidPdfViewerDemo
https://pan.baidu.com/s/1_Py36avgQqcJ5C87BaS5Iw


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前面的文章中我們講道,像趣頭條類的APP對於收徒和閱讀行為給予用戶現金獎勵的方式勢必會受到大量羊毛黨黑產的註意,其實單個用戶能薅到的錢是沒有多少的,為了達到利益最大化,黑產肯定會利用各種手段構建大量賬號來薅APP運營企業的羊毛,因為收徒的獎勵遠高於閱讀,所以賺取收徒獎勵就成了最嚴重的薅羊毛手段。前文 ...
  • 早上剛睜眼,看到了一堆資料庫告警的簡訊,其中一個內容如下: 眼看這是剛從其他DBA交接過來的資料庫,不敢怠慢,立馬起來查看從庫日誌信息如下: 即非正常停止。 再登錄主庫機器查看主庫錯誤日誌,信息如下 從主庫日誌可以看出,2個從庫是主庫主動斷開的,而給出的信息也指出了原因failed on flush ...
  • Microsoft SQL Server 2012安裝說明 環境:Windows8, Windows7, WinVista, Win2003, WinXP Microsoft SQL Server 2012是一款強大的MySQL資料庫管理和開發工具。新版的Microsoft SQL Server 2 ...
  • 1. Objective C語言使用的是"消息結構"而非"函數調用"。 "消息結構"和"函數調用"之間的區別 "消息結構"的語言: 運行時由運行環境決定所應執行的代碼 "函數調用"的語言: 由編譯器決定 記憶體模型:Objective C語言中的指針是用來指示對象的。 Objective C為C語言添 ...
  • flutterBoost使用筆記 新一代Flutter-Native混合解決方案。 FlutterBoost是一個Flutter插件,它可以輕鬆地為現有原生應用程式提供Flutter混合集成方案。FlutterBoost的理念是將Flutter像Webview那樣來使用。在現有應用程式中同時管理Na ...
  • 如需轉載,請註明出處:Flutter學習筆記(29)--Flutter如何與native進行通信 前言:在我們開發Flutter項目的時候,難免會遇到需要調用native api或者是其他的情況,這時候就需要處理Flutter與native的通信問題,一般常用的Flutter與native的通信方式 ...
  • 文本控制項 Text 支持兩種類型的文本展示,一個是預設的展示單一樣式文本 Text,另一個是支持多種混合樣式的富文本 Text.rich。 單一樣式文本 Text 單一樣式文本 Text 的初始化,是要傳入需要展示的字元串。而這個字元串的具體展示效果,受構造函數中的其他參數控制。這些參數大致可以分為 ...
  • 如需轉載,請註明出處:Flutter學習筆記(28)--使用第三方jar包 1.打開一個Flutter項目,點擊編碼視窗右上角的Open for Editing in Android Studio,這時候你的Flutter項目會轉換成一個Android結構的項目。 2.項目的目錄結構選擇projec ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...